GameState InProgress Failed Won inst Eq GameState eq (l, r): (case (l, r)) (InProgress, InProgress): True (Failed, Failed): True (Won, Won): True _: False Tile hidden Bool is-bomb Bool flagged Bool TileMap tiles Slice (Slice Tile) width I32 height I32 fn mk-tiles(w, h, al) -> TileMap rows = al Alloc.allocate(h) for row in rows Slice.addresses() row <&= al Alloc.default(w, { hidden: True, is-bomb: False, flagged: False }) return { tiles: rows, width: w size-i32(), height: h size-i32() } fn map-tile(tilemap TileMap, x, y, fun) tp = tilemap.tiles[y i32-size()] Slice.get-ptr(x i32-size()) tp <&= fun(tp&) inst Indexable TileMap elem-get (self, idx) x = idx fst() i32-size() y = idx snd() i32-size() return self.tiles Slice.try-get(y) and-maybe(fn s: s Slice.try-get(x)) or-else({ is-bomb: False, hidden: False, flagged: False }) fn generate-map (w, h, bombs, al) -> TileMap tiles = mk-tiles(w, h, al) for bomb in 1 to (bombs) x = @undefined y = @undefined while True x <= Random.next-u32() Math.mod(w size-u32()) u32-i32() y <= Random.next-u32() Math.mod(h size-u32()) u32-i32() if not tiles[(x, y)].is-bomb break tiles map-tile(x, y, fn _: { hidden: True, is-bomb: True, flagged: False }) return tiles # basado impl, but it does not want to compile monomorphize for some reason. # fn count-neighbor-bombs (tilemap TileMap, x, y): # (y - 1) to (y + 1) # cartesian((x - 1) to (x + 1)) # filter(fn xy: tilemap[xy].is-bomb) # count() fn count-neighbor-bombs (tilemap TileMap, x, y) neighbors = 0 for yy in (y - 1) to (y + 1) for xx in (x - 1) to (x + 1) if tilemap[(xx, yy)].is-bomb neighbors <= neighbors + 1 return neighbors ControlScheme MouseControls KeyControls inst Eq ControlScheme eq (l, r): (case (l, r)) (KeyControls, KeyControls): True (MouseControls, MouseControls): True _: False Controls cursor-x I32 cursor-y I32 current-control-scheme ControlScheme fn render-tiles (screen Ptr (Tui.Screen Allocator), tilemap TileMap, (x-off, y-off), game-state, controls Controls) # draw border first screen Tui.set-screen-bg(Term.Color8(Term.Magenta8)) for x in (-1) to (tilemap.width) screen Tui.put-char(' ', x + x-off, y-off - 1) screen Tui.put-char(' ', x + x-off, y-off + tilemap.height) for y in 0 to (tilemap.height - 1) screen Tui.put-char(' ', x-off - 1, y + y-off) screen Tui.put-char(' ', x-off + tilemap.width, y + y-off) for (row, yi) in tilemap.tiles zip(from(0)) for (col, xi) in row zip(from(0)) is-cursor = controls.current-control-scheme == KeyControls and xi == controls.cursor-x and yi == controls.cursor-y fn set-bg(bg) screen Tui.set-screen-bg(if is-cursor: Term.Color8(Term.Yellow8) else bg) x = xi + x-off y = yi + y-off bg = if (xi + yi) Math.mod(2) == 0: Term.Color8(Term.White8) else Term.Color16(Term.BrightWhite16) if col.hidden set-bg(bg) if col.flagged screen Tui.set-screen-fg(Term.Color8(Term.Red8)) screen Tui.put-char('F', x, y) else screen Tui.put-char(' ', x, y) else screen Tui.set-screen-fg(Term.ColorDefault) set-bg(Term.ColorDefault) num-bombs = tilemap count-neighbor-bombs(xi, yi) if num-bombs == 0 screen Tui.put-char(' ', x, y) else num-bombs-char = num-bombs chars() head() or-fail('impossible:)') screen Tui.put-char(num-bombs-char, x, y) if game-state == Failed and col.is-bomb if col.hidden screen Tui.set-screen-fg(Term.Color8(Term.Red8)) set-bg(Term.ColorDefault) screen Tui.put-char('o', x, y) else screen Tui.set-screen-fg(Term.ColorDefault) set-bg(Term.Color8(Term.Red8)) screen Tui.put-char('o', x, y) fn is-game-won(tilemap TileMap) -> Bool: tilemap.tiles flatten() all(fn(tile): (tile.is-bomb and tile.hidden) or (not tile.is-bomb and not tile.hidden)) fn uncover-tile(tilemap TileMap, x, y) -> Bool if x < 0 or x >= tilemap.width or y < 0 or y >= tilemap.height return False tile = tilemap[(x, y)] if not tile.hidden return False tilemap map-tile(x, y, fn tile: tile { hidden: False }) if tile.is-bomb return True else nb = tilemap count-neighbor-bombs(x, y) if nb == 0 for yy in (y - 1) to (y + 1) for xx in (x - 1) to (x + 1) tilemap uncover-tile(xx, yy) return False fn toggle-flag(tilemap TileMap, x, y) if x < 0 or x >= tilemap.width or y < 0 or y >= tilemap.height return False tilemap map-tile(x, y, fn tile: tile { flagged: not tile.flagged }) fn get-board-off(tui Ptr Tui.Tui, tilemap TileMap): (tui&.width u32-i32()/2 - tilemap.width/2, tui&.height u32-i32()/2 - tilemap.height/2) # Scoring Stats total-wins U32 fn load-score(al) -> Stats data-dir = XDG.data-home() stats-file = '\(data-dir)/kc-minesweeper/stats' case with(stats-file StrBuilder.mk-const-str(al), fn filename: filename StrBuilder.free-const-str(al), fn filename: filename File.try-read-contents(al)) None return { total-wins: 0 } Just(stats) it =& stats lines() into-iter() total-wins = it next() and-maybe(parse-int) or-else(0) i64-u32() return { total-wins } fn save-score(stats Stats, al) data-dir = XDG.data-home() minesweeper-data-dir = '\(data-dir)/kc-minesweeper' minesweeper-data-dir Sh.mkdir-p(al) stats-file = '\(minesweeper-data-dir)/stats' with(stats-file StrBuilder.mk-const-str(al), fn filename: filename StrBuilder.free-const-str(al), fn filename: filename File.write-contents('\(stats.total-wins)\n')) args = Args.get() width = 9 height = 9 bombs = 10 case args [l, r, bs] width <= parse-int(l) or-fail('could not parse width') i64-size() height <= parse-int(r) or-fail('could not parse height') i64-size() bombs <= parse-int(bs) or-fail('could not parse bombs') i64-i32() [] pass _ panic('args??') al = Alloc.idc() stats = load-score(al) game-state = InProgress tilemap = generate-map(width, height, bombs, al) controls = Controls { cursor-x: width size-i32() / 2, cursor-y: height size-i32() / 2, current-control-scheme: MouseControls } tui =& Tui.mk() al = Alloc.idc() screen =& tui Tui.mk-screen(al) running = True while running for ev in Iter.from-function(fn: Tui.read-event(tui)) case ev Tui.Key(Tui.Char(c)) if c == 'q' running <= False elif c == 'r' tilemap <= generate-map(width, height, bombs, al) game-state <= InProgress elif game-state == InProgress and controls.current-control-scheme == MouseControls controls <.current-control-scheme= KeyControls elif game-state == InProgress if c == 'h' controls <.cursor-x= (controls.cursor-x - 1) Math.clamp(0, tilemap.width - 1) if c == 'j' controls <.cursor-y= (controls.cursor-y + 1) Math.clamp(0, tilemap.height - 1) if c == 'k' controls <.cursor-y= (controls.cursor-y - 1) Math.clamp(0, tilemap.height - 1) if c == 'l' controls <.cursor-x= (controls.cursor-x + 1) Math.clamp(0, tilemap.width - 1) if c == ' ' x = controls.cursor-x y = controls.cursor-y # bad, I should make a total action if not tilemap[(x, y)].flagged and tilemap uncover-tile(x, y) game-state <= Failed if tilemap is-game-won() game-state <= Won stats <.total-wins= stats.total-wins + 1 stats save-score(al) if c == 'n' tilemap toggle-flag(controls.cursor-x, controls.cursor-y) Tui.Mouse(ev) controls <.current-control-scheme= MouseControls x = ev.x - tui get-board-off(tilemap) fst() y = ev.y - tui get-board-off(tilemap) snd() if ev.button == Tui.MouseLeft and ev.pressed if game-state == InProgress if not tilemap[(x, y)].flagged and tilemap uncover-tile(x, y) game-state <= Failed # BAD, see above if game-state == InProgress and tilemap is-game-won() game-state <= Won stats <.total-wins= stats.total-wins + 1 stats save-score(al) if ev.button == Tui.MouseRight and ev.pressed if game-state == InProgress tilemap toggle-flag(x, y) # BAD! look above (the basic action functions should encompass all state changes, so controls can be easily changed/extended) if (ev.button == Tui.MouseLeft or ev.button == Tui.MouseRight) and x Math.between(0, tilemap.width - 1) and y Math.between(0, tilemap.height - 1) controls <.cursor-x= x controls <.cursor-y= y screen Tui.resize-screen-if-needed() if tui Tui.should-redraw() screen Tui.clear-screen() screen Tui.set-screen-colors(Term.ColorDefault, Term.ColorDefault) screen Tui.fill-default() screen Tui.draw-str('total wins: \(stats.total-wins)', 0, 0) screen render-tiles(tilemap, tui get-board-off(tilemap), game-state, controls) screen Tui.set-screen-colors(Term.ColorDefault, Term.ColorDefault) if game-state == Failed screen Tui.draw-str-center('ya failed. restart? (r)', tui&.width u32-i32() / 2, tilemap.height + tui get-board-off(tilemap) snd() + 2) elif game-state == Won screen Tui.draw-str-center('ya won. quit? (q)', tui&.width u32-i32() / 2, tilemap.height + tui get-board-off(tilemap) snd() + 2) screen Tui.render-screen() Tui.sync(tui) screen Tui.free-screen() tui Tui.deinit()