merge and input done

This commit is contained in:
Reduan204 2026-08-24 22:10:02 +01:00
parent c22c4b7883
commit d756ce5284
4 changed files with 219 additions and 21 deletions

View file

@ -1,6 +1,8 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
// Both of these read flags from the command line, e.g.:
// zig build -Dtarget=x86_64-windows -Doptimize=ReleaseSafe
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
@ -17,19 +19,25 @@ pub fn build(b: *std.Build) void {
}),
});
// Plain `zig build` runs this: copies the binary into zig-out/bin.
b.installArtifact(exe);
// `zig build run`: execute the freshly built program.
const run_step = b.step("run", "Run the app");
const run_cmd = b.addRunArtifact(exe);
run_step.dependOn(&run_cmd.step);
// Chaining run onto install guarantees the exe is rebuilt first.
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
// `zig build test`: builds a special binary whose "main" collects every
// `test` block reachable from the root source file (main.zig) which is
// exactly why the `_ = Board;` bridge block lives there and not in board.zig.
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
});

View file

@ -1,3 +1,6 @@
//! board.zig pure game rules: packing, merging, sliding, spawning.
//! No I/O, no colors, no player input: this file knows NOTHING about the
//! terminal. That separation is what makes it easy to unit-test.
const std = @import("std");
const Self = @This();
@ -8,12 +11,13 @@ const Self = @This();
cells: [4][4]u16 = .{.{0} ** 4} ** 4,
/// ----------------------------------------------------
/// Packs one row LEFT: non-zero tiles slide toward index 0,
/// gaps close up. NO merging here that's mergeRow's job.
/// (This is the old slideRowLeft, renamed for honesty.)
/// ----------------------------------------------------
fn slideRowLeft(row: *[4]u16) void {
fn packRow(row: *[4]u16) void {
var write: usize = 0; // where the next tile should land
// Pass 1: copy every non-zero tile to the next free slot.
// `cell` = WHAT we're carrying, `write` = WHERE it lands.
for (row) |cell| {
if (cell != 0) {
row[write] = cell;
@ -21,12 +25,45 @@ fn slideRowLeft(row: *[4]u16) void {
}
}
// Pass 2: everything after the last packed tile becomes 0.
for (write..4) |i| {
row[i] = 0;
}
}
/// ----------------------------------------------------
/// Merges equal adjacent tiles, ONCE per move.
/// Scan leftright: if row[i] == row[i+1] (both non-zero),
/// double row[i] and zero row[i+1].
///
/// Why no "already merged" flag is needed: zeroing the right
/// partner puts a 0 BETWEEN the new tile and anything after it,
/// so the same tile can never be merged twice in one scan.
/// The rule enforces itself.
///
/// Loop stops at 3 because we read row[i+1] at i=3 that
/// would be out of bounds and Zig would panic (in Debug/ReleaseSafe).
/// ----------------------------------------------------
fn mergeRow(row: *[4]u16) void {
for (0..3) |i| {
if (row[i] != 0 and row[i] == row[i + 1]) {
row[i] *= 2;
row[i + 1] = 0;
}
}
}
/// ----------------------------------------------------
/// Full 2048 slide LEFT = pack merge pack.
/// Pass 1 makes equal tiles adjacent (so merges are visible),
/// pass 2 merges them, pass 3 closes the gap a merge opened.
/// Example: [2,2,4,0] [2,2,4,0] [4,0,4,0] [4,4,0,0]
/// ----------------------------------------------------
fn slideRowLeft(row: *[4]u16) void {
packRow(row);
mergeRow(row);
packRow(row);
}
/// ----------------------------------------------------
/// Reverses one row in place: [2,0,4,0] becomes [0,4,0,2].
/// Only visits the first half (i < 2) otherwise every swap
@ -95,6 +132,8 @@ pub fn slideDown(self: *Self) void {
/// ----------------------------------------------------
/// Spawns a new tile (2 or 4) in a random empty cell.
/// Silent no-op if the board is full callers who need
/// to know must check for space themselves (Stage 5 will).
/// ----------------------------------------------------
pub fn spawn(self: *Self, rand: std.Random) void {
// --- Phase 1: find every empty cell -------------------
@ -133,3 +172,56 @@ pub fn spawn(self: *Self, rand: std.Random) void {
self.cells[row][col] = value;
}
//
// TESTS
// Run with: zig build test --summary all
// Silence = success. Failures print file, line, and both values.
//
const testing = std.testing;
test "packRow slides tiles left and closes gaps" {
var row = [4]u16{ 2, 0, 0, 2 };
packRow(&row);
try testing.expectEqual([4]u16{ 2, 2, 0, 0 }, row);
}
test "mergeRow doubles one pair" {
var row = [4]u16{ 2, 2, 0, 0 };
mergeRow(&row);
try testing.expectEqual([4]u16{ 4, 0, 0, 0 }, row);
}
test "mergeRow never chains into an 8" {
var row = [4]u16{ 2, 2, 2, 2 };
mergeRow(&row);
// After merging at i=0 the zero at index 1 acts as a barrier,
// so the second pair merges separately: [4,0,4,0], never [8,...].
try testing.expectEqual([4]u16{ 4, 0, 4, 0 }, row);
}
test "mergeRow leaves unequal neighbors alone" {
var row = [4]u16{ 2, 4, 0, 0 };
mergeRow(&row);
try testing.expectEqual([4]u16{ 2, 4, 0, 0 }, row);
}
test "slideRowLeft packs before AND after merging" {
// Needs pack #1 so the 2s become adjacent,
// and pack #2 to close the gap the merge opened.
var row = [4]u16{ 2, 2, 4, 0 };
slideRowLeft(&row);
try testing.expectEqual([4]u16{ 4, 4, 0, 0 }, row);
}
test "slideLeft merges every row on the board" {
var b = Self{}; // cells defaults to all zeros via its field default
b.cells[0] = .{ 2, 2, 2, 2 };
b.cells[1] = .{ 4, 2, 2, 0 };
b.cells[2] = .{ 2, 0, 0, 2 };
b.slideLeft();
try testing.expectEqual([4]u16{ 4, 4, 0, 0 }, b.cells[0]);
try testing.expectEqual([4]u16{ 4, 4, 0, 0 }, b.cells[1]);
try testing.expectEqual([4]u16{ 4, 0, 0, 0 }, b.cells[2]);
}

View file

@ -1,27 +1,125 @@
//! main.zig program entry point: sets up a board and shows it.
//! main.zig entry point: rendering, input, and the game loop.
const Board = @import("board.zig");
const std = @import("std");
pub fn main() !void {
var board: Board = .{};
/// The four directions a player can swipe.
const Move = enum { up, down, left, right };
// Random number generator. The seed (42) makes every run
// identical for now handy for debugging, we'll fix it later.
var prng = std.Random.DefaultPrng.init(42);
const rand = prng.random();
board.spawn(rand); // drop one starting tile onto the board
board.cells[0] = .{ 2, 0, 2, 0 };
board.cells[1] = .{ 0, 4, 4, 0 };
board.cells[2] = .{ 2, 2, 2, 2 };
board.cells[3] = .{ 0, 2, 0, 4 };
board.slideUp(); // swap in any direction to test it
/// Maps a tile value to an ANSI foreground-color escape sequence.
/// "\x1b[" opens every ANSI escape, a number picks the color, 'm' closes it;
/// render() resets back to normal with "\x1b[0m" after each tile.
/// Values 128+ (128, 256, ... 2048) all land in the bold-yellow `else`.
/// Note: the `0 =>` arm is currently unreachable render() special-cases
/// empty cells before ever calling this. Kept for safety if that changes.
fn tileColor(value: u16) []const u8 {
return switch (value) {
0 => "\x1b[90m",
2 => "\x1b[37m",
4 => "\x1b[33m",
8 => "\x1b[31m",
16 => "\x1b[35m",
32 => "\x1b[36m",
64 => "\x1b[34m",
else => "\x1b[1;33m",
};
}
/// Draws the whole board to `out`. `*const Board` is a read-only view:
/// rendering physically cannot mutate game state.
/// `{d:4}` prints each number right-aligned in a 4-column field so
/// columns stay lined up once tiles grow to 34 digits.
fn render(board: *const Board, out: *std.Io.Writer) !void {
try out.print("\x1b[2J\x1b[H", .{}); // clear screen + cursor home
for (board.cells) |row| {
for (row) |cell| {
std.debug.print("\x1b[31m{d:4}", .{cell});
}
std.debug.print("\n", .{});
if (cell == 0) {
try out.print("\x1b[90m .\x1b[0m", .{});
} else {
try out.print("{s}{d:4}\x1b[0m", .{ tileColor(cell), cell });
}
}
try out.print("\n", .{});
}
try out.flush();
}
/// Maps one typed character to a Move.
/// Returns null for anything unrecognized an OPTIONAL:
/// nothing failed, there just wasn't a match.
fn parseMove(text: []const u8) ?Move {
if (text.len != 1) return null;
return switch (text[0]) {
'w' => .up,
'a' => .left,
's' => .down,
'd' => .right,
else => null,
};
}
pub fn main(init: std.process.Init) !void {
const io = init.io;
// The writer/reader need somewhere to stage bytes, so we hand each one
// a stack buffer. CRITICAL: the buffer must outlive its interface
// declare it first, and keep both alive for main's whole body.
var out_buffer: [4096]u8 = undefined;
var file_writer = std.Io.File.stdout().writer(io, &out_buffer);
const out = &file_writer.interface; // buffered stdout: flush() pushes bytes out
var in_buffer: [256]u8 = undefined;
var file_reader = std.Io.File.stdin().readerStreaming(io, &in_buffer);
const in = &file_reader.interface; // buffered stdin
var board: Board = .{};
// Seed a PRNG once from OS entropy, then reuse it every turn.
// io.random is the slow, secure source; DefaultPrng is a fast
// deterministic stream exactly what game randomness needs.
// readInt just squashes the 8 random bytes into one u64 seed.
var seed_bytes: [8]u8 = undefined;
io.random(&seed_bytes);
var prng = std.Random.DefaultPrng.init(std.mem.readInt(u64, &seed_bytes, .little));
const rand = prng.random(); // thin handle over prng; can't be misused alone
board.spawn(rand); // real 2048 opens with two tiles already on the board
board.spawn(rand);
while (true) {
try render(&board, out);
try out.print("move (w/a/s/d, q=quit): ", .{});
try out.flush();
// Two different flavors of "nothing came back":
// error = something broke surface it
// null = clean EOF (Ctrl+D / stdin closed) just quit
const maybe_line = in.takeDelimiter('\n') catch |err| {
try out.print("\nread error: {any}\n", .{err});
try out.flush();
return err;
};
const line = maybe_line orelse break;
const trimmed = std.mem.trim(u8, line, " \r\t");
if (std.mem.eql(u8, trimmed, "q")) break;
const move = parseMove(trimmed) orelse continue; // unknown redraw
const before = board.cells;
switch (move) {
.up => board.slideUp(),
.down => board.slideDown(),
.left => board.slideLeft(),
.right => board.slideRight(),
}
if (!std.meta.eql(before, board.cells)) {
board.spawn(rand);
}
}
try out.print("\nThanks for playing!\n", .{});
try out.flush();
}
test {
_ = Board;
}

Binary file not shown.