// m3te core model — pure, headless match-3 board (Phase 1). // // Everything here is deterministic and rendering-free: a fixed seed always // produces the same board. Later phases build on these primitives — // P1.2 (match detection), P1.3 (swap legality), P2 (clear/cascade/refill) — // so the layout favours plain index access (`at` / `idx`) over anything // rendering-specific. #import "modules/std.sx"; // ── Gem ────────────────────────────────────────────────────────────────── // Six distinct gem types. The enum's ordinal (0..5) IS the gem index, so it // casts cleanly to/from the integers the RNG and the textual dump work in. GEM_COUNT :: 6; Gem :: enum { red; orange; yellow; green; blue; purple; } // One stable character per gem type, indexed by ordinal — the alphabet the // board dump (and its golden) is written in. GEM_CHARS :: "ROYGBP"; gem_char :: (g: Gem) -> u8 { GEM_CHARS[cast(s64) g] } // ── Deterministic RNG ───────────────────────────────────────────────────── // A 32-bit linear congruential generator (Numerical Recipes constants), // carried in an s64 and masked back to 32 bits after every step so the // stream is identical regardless of host integer width. The state*MUL+ADD // product stays well under s64 range, so no intermediate overflow. Any seed // (including 0) yields a valid stream — an LCG has no forbidden state. RNG_MASK32 :: 0xFFFFFFFF; RNG_MUL :: 1664525; RNG_ADD :: 1013904223; Rng :: struct { state: s64; // Advance and return the next 32-bit value. next_u32 :: (self: *Rng) -> s64 { self.state = (self.state * RNG_MUL + RNG_ADD) & RNG_MASK32; self.state } // Uniform-ish value in [0, n). Uses the high bits, whose period is far // longer than the low bits of an LCG. next_range :: (self: *Rng, n: s64) -> s64 { (self.next_u32() >> 16) % n } } rng_seeded :: (seed: s64) -> Rng { Rng.{ state = seed & RNG_MASK32 } } // ── Board ───────────────────────────────────────────────────────────────── BOARD_COLS :: 8; BOARD_ROWS :: 8; BOARD_CELLS :: BOARD_COLS * BOARD_ROWS; Board :: struct { // Row-major: cell (col, row) lives at row*BOARD_COLS + col. cells: [BOARD_CELLS]Gem; idx :: (col: s64, row: s64) -> s64 { row * BOARD_COLS + col } at :: (self: *Board, col: s64, row: s64) -> Gem { self.cells[Board.idx(col, row)] } set :: (self: *Board, col: s64, row: s64, g: Gem) { self.cells[Board.idx(col, row)] = g; } // Fill every cell from `seed` so that NO horizontal or vertical run of // three same-type gems exists. Cells are placed in row-major order; when // placing one, any gem type that would complete a 3-in-a-row with the two // already-placed cells to its left or above is excluded, and the gem is // drawn from the remaining allowed types. At most two types are ever // excluded, so a choice always remains. init :: (self: *Board, seed: s64) { rng := rng_seeded(seed); for 0..BOARD_ROWS: (row) { for 0..BOARD_COLS: (col) { self.set(col, row, pick_gem(self, @rng, col, row)); } } } } // Choose a gem for (col, row) that can't extend an existing run leftward or // upward. Pure given the board's already-placed prefix and the RNG state. pick_gem :: (board: *Board, rng: *Rng, col: s64, row: s64) -> Gem { forbidden : [GEM_COUNT]bool = ---; for 0..GEM_COUNT: (t) { forbidden[t] = false; } // Two same gems immediately to the left → a third of that type matches. if col >= 2 { left := board.at(col - 1, row); if left == board.at(col - 2, row) { forbidden[cast(s64) left] = true; } } // Two same gems immediately above → a third of that type matches. if row >= 2 { up := board.at(col, row - 1); if up == board.at(col, row - 2) { forbidden[cast(s64) up] = true; } } allowed := 0; for 0..GEM_COUNT: (t) { if !forbidden[t] { allowed += 1; } } // Pick the k-th still-allowed type; single RNG draw, always terminates. k := rng.next_range(allowed); for 0..GEM_COUNT: (t) { if !forbidden[t] { if k == 0 { return cast(Gem) t; } k -= 1; } } .red // unreachable: `allowed` >= GEM_COUNT-2 >= 4, so k is always consumed } // Deterministic textual dump: one row per line, top (row 0) to bottom, a // single gem character per cell. Suitable for snapshotting. board_dump :: (self: *Board) -> string { line_w := BOARD_COLS + 1; // 8 gem chars + newline buf := cstring(BOARD_ROWS * line_w); for 0..BOARD_ROWS: (row) { base := row * line_w; for 0..BOARD_COLS: (col) { buf[base + col] = gem_char(self.at(col, row)); } buf[base + BOARD_COLS] = 10; // '\n' } buf }