merge and input done
This commit is contained in:
parent
c22c4b7883
commit
d756ce5284
4 changed files with 219 additions and 21 deletions
|
|
@ -1,6 +1,8 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
pub fn build(b: *std.Build) void {
|
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 target = b.standardTargetOptions(.{});
|
||||||
|
|
||||||
const optimize = b.standardOptimizeOption(.{});
|
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);
|
b.installArtifact(exe);
|
||||||
|
|
||||||
|
// `zig build run`: execute the freshly built program.
|
||||||
const run_step = b.step("run", "Run the app");
|
const run_step = b.step("run", "Run the app");
|
||||||
|
|
||||||
const run_cmd = b.addRunArtifact(exe);
|
const run_cmd = b.addRunArtifact(exe);
|
||||||
run_step.dependOn(&run_cmd.step);
|
run_step.dependOn(&run_cmd.step);
|
||||||
|
|
||||||
|
// Chaining run onto install guarantees the exe is rebuilt first.
|
||||||
run_cmd.step.dependOn(b.getInstallStep());
|
run_cmd.step.dependOn(b.getInstallStep());
|
||||||
|
|
||||||
if (b.args) |args| {
|
if (b.args) |args| {
|
||||||
run_cmd.addArgs(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(.{
|
const exe_tests = b.addTest(.{
|
||||||
.root_module = exe.root_module,
|
.root_module = exe.root_module,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
100
src/board.zig
100
src/board.zig
|
|
@ -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 std = @import("std");
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
|
|
@ -8,12 +11,13 @@ const Self = @This();
|
||||||
cells: [4][4]u16 = .{.{0} ** 4} ** 4,
|
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
|
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| {
|
for (row) |cell| {
|
||||||
if (cell != 0) {
|
if (cell != 0) {
|
||||||
row[write] = cell;
|
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| {
|
for (write..4) |i| {
|
||||||
row[i] = 0;
|
row[i] = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ----------------------------------------------------
|
||||||
|
/// Merges equal adjacent tiles, ONCE per move.
|
||||||
|
/// Scan left→right: 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].
|
/// Reverses one row in place: [2,0,4,0] becomes [0,4,0,2].
|
||||||
/// Only visits the first half (i < 2) — otherwise every swap
|
/// 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.
|
/// 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 {
|
pub fn spawn(self: *Self, rand: std.Random) void {
|
||||||
// --- Phase 1: find every empty cell -------------------
|
// --- Phase 1: find every empty cell -------------------
|
||||||
|
|
@ -133,3 +172,56 @@ pub fn spawn(self: *Self, rand: std.Random) void {
|
||||||
|
|
||||||
self.cells[row][col] = value;
|
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]);
|
||||||
|
}
|
||||||
|
|
|
||||||
132
src/main.zig
132
src/main.zig
|
|
@ -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 Board = @import("board.zig");
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
pub fn main() !void {
|
/// The four directions a player can swipe.
|
||||||
var board: Board = .{};
|
const Move = enum { up, down, left, right };
|
||||||
|
|
||||||
// Random number generator. The seed (42) makes every run
|
/// Maps a tile value to an ANSI foreground-color escape sequence.
|
||||||
// identical for now — handy for debugging, we'll fix it later.
|
/// "\x1b[" opens every ANSI escape, a number picks the color, 'm' closes it;
|
||||||
var prng = std.Random.DefaultPrng.init(42);
|
/// render() resets back to normal with "\x1b[0m" after each tile.
|
||||||
const rand = prng.random();
|
/// Values 128+ (128, 256, ... 2048) all land in the bold-yellow `else`.
|
||||||
|
/// Note: the `0 =>` arm is currently unreachable — render() special-cases
|
||||||
board.spawn(rand); // drop one starting tile onto the board
|
/// empty cells before ever calling this. Kept for safety if that changes.
|
||||||
|
fn tileColor(value: u16) []const u8 {
|
||||||
board.cells[0] = .{ 2, 0, 2, 0 };
|
return switch (value) {
|
||||||
board.cells[1] = .{ 0, 4, 4, 0 };
|
0 => "\x1b[90m",
|
||||||
board.cells[2] = .{ 2, 2, 2, 2 };
|
2 => "\x1b[37m",
|
||||||
board.cells[3] = .{ 0, 2, 0, 4 };
|
4 => "\x1b[33m",
|
||||||
board.slideUp(); // ← swap in any direction to test it
|
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 3–4 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 (board.cells) |row| {
|
||||||
for (row) |cell| {
|
for (row) |cell| {
|
||||||
std.debug.print("\x1b[31m{d:4}", .{cell});
|
if (cell == 0) {
|
||||||
|
try out.print("\x1b[90m .\x1b[0m", .{});
|
||||||
|
} else {
|
||||||
|
try out.print("{s}{d:4}\x1b[0m", .{ tileColor(cell), cell });
|
||||||
}
|
}
|
||||||
std.debug.print("\n", .{});
|
|
||||||
}
|
}
|
||||||
|
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.
Loading…
Add table
Reference in a new issue