# Game: the state, the input, and the frame. # ========================================== # # The state is pure Data. Every click returns a new state, so a move # is a value you can print, compare or replay. main.bend runs it in a # window; demo.bend runs the same functions over a scripted list of # events and prints the result. # # The board is not laid out until the first click. That click sets the # safe cell, so it can never be a mine and always opens a region. import Base import ./grid.bend as G import ./cell.bend as C import ./board.bend as B import ./art.bend as A # The clock the window drives, one step per frame. It is kept as # whole seconds plus a frame remainder, because Base's Nat arithmetic # counts up one step at a time: small numbers stay cheap, and a raw # millisecond timestamp would not be. type Clock is Data: Clock{secs: Nat, sub: Nat} type Game is Data: Game{ conf: B.Conf, cells: G.Grid<&2, C.Cell>, phase: B.Phase, live: Bool, bx: U32, by: U32, tiles: List<&2, Image>, scale: Nat, clock: Clock} # The payload of a frame pass: everything one tile needs to know. type Paint is Data: Paint{ cells: G.Grid<&2, C.Cell>, tiles: List<&2, Image>, depth: Nat, lost: Bool, bx: U32, by: U32} def Game.conf(g: Game) -> B.Conf: match g: case Game{cf, cg, ph, lv, bx, by, ts, sc, ck}: cf def Game.cells(g: Game) -> G.Grid<&2, C.Cell>: match g: case Game{cf, cg, ph, lv, bx, by, ts, sc, ck}: cg def Game.phase(g: Game) -> B.Phase: match g: case Game{cf, cg, ph, lv, bx, by, ts, sc, ck}: ph def Game.live(g: Game) -> Bool: match g: case Game{cf, cg, ph, lv, bx, by, ts, sc, ck}: lv def Game.bx(g: Game) -> U32: match g: case Game{cf, cg, ph, lv, bx, by, ts, sc, ck}: bx def Game.by(g: Game) -> U32: match g: case Game{cf, cg, ph, lv, bx, by, ts, sc, ck}: by def Game.tiles(g: Game) -> List<&2, Image>: match g: case Game{cf, cg, ph, lv, bx, by, ts, sc, ck}: ts def Game.scale(g: Game) -> Nat: match g: case Game{cf, cg, ph, lv, bx, by, ts, sc, ck}: sc def Paint.cells(pt: Paint) -> G.Grid<&2, C.Cell>: match pt: case Paint{cg, ts, d, lo, bx, by}: cg def Paint.tiles(pt: Paint) -> List<&2, Image>: match pt: case Paint{cg, ts, d, lo, bx, by}: ts def Paint.depth(pt: Paint) -> Nat: match pt: case Paint{cg, ts, d, lo, bx, by}: d def Paint.lost(pt: Paint) -> Bool: match pt: case Paint{cg, ts, d, lo, bx, by}: lo def Paint.bx(pt: Paint) -> U32: match pt: case Paint{cg, ts, d, lo, bx, by}: bx def Paint.by(pt: Paint) -> U32: match pt: case Paint{cg, ts, d, lo, bx, by}: by def Game.clock(g: Game) -> Clock: match g: case Game{cf, cg, ph, lv, bx, by, ts, sc, ck}: ck def Clock.secs(c: Clock) -> Nat: match c: case Clock{s, u}: s # The original stops counting at 999, and so does this one. That also # keeps Nat.show cheap. def Clock.roll(s: Nat, u: Nat, full: Bool) -> Clock: match full: case True{}: Clock{Nat.min(1n+s, 999n), 0n} case False{}: Clock{s, u} def Clock.bump(c: Clock) -> Clock: match c: case Clock{s, u}: +v = {1n+u : Nat} Clock.roll(s, v, Nat.is_ge(v, 60n)) def Game.is_lost(g: Game) -> Bool: B.Phase.is_lost(Game.phase(g)) # An empty board, before the first click picks the safe cell. def Game.blank_cell(+cf: B.Conf, +x: U32, +y: U32) -> C.Cell: C.Cell{False{}, 0, C.Hidden{}} def Game.blank(+cf: B.Conf) -> G.Grid<&2, C.Cell>: G.Grid.init(~C.Cell, ~B.Conf, ~Game.blank_cell, B.Conf.depth(cf), cf, 0, 0) # depth 4n is a 16x16 board; scale 5n is a 32x32 tile, so a 512x512 window. def Game.new(+depth: Nat, mines: U32, seed: U32, +scale: Nat) -> Game: +cf = B.Conf.new(depth, mines, seed, 0, 0) Game{cf, Game.blank(cf), B.Playing{}, False{}, 0, 0, A.Art.tiles(scale), scale, Clock{0n, 0n}} # Restart. The seed walks on, so the next board is a different one. def Game.reset(+g: Game) -> Game: +cf = B.Conf.new( B.Conf.depth(Game.conf(g)), B.Conf.mines(Game.conf(g)), B.Board.hash(B.Conf.seed(Game.conf(g)), 2654435761), 0, 0) Game{cf, Game.blank(cf), B.Playing{}, False{}, 0, 0, Game.tiles(g), Game.scale(g), Clock{0n, 0n}} # The first click lays the board out and aims the safe block at itself. def Game.wake.pick(+g: Game, x: U32, y: U32, live: Bool) -> Game: match live: case True{}: g case False{}: +cf = B.Conf.aim(Game.conf(g), x, y) Game{cf, B.Board.make!(cf), Game.phase(g), True{}, 0, 0, Game.tiles(g), Game.scale(g), Game.clock(g)} def Game.wake(+g: Game, x: U32, y: U32) -> Game: Game.wake.pick(g, x, y, Game.live(g)) # A left click opens a hidden cell and chords a shown one. def Game.move.left(+g: Game, x: U32, y: U32, shown: Bool) -> G.Grid<&2, C.Cell>: match shown: case True{}: B.Board.chord_at(Game.conf(g), Game.cells(g), x, y) case False{}: B.Board.open_at(Game.conf(g), Game.cells(g), x, y) def Game.move.other(+g: Game, x: U32, y: U32, flagging: Bool) -> G.Grid<&2, C.Cell>: match flagging: case True{}: B.Board.flag_at(Game.conf(g), Game.cells(g), x, y) case False{}: B.Board.chord_at(Game.conf(g), Game.cells(g), x, y) def Game.move.pick(+g: Game, btn: U32, +x: U32, +y: U32, left: Bool) -> G.Grid<&2, C.Cell>: match left: case True{}: c = G.Grid.get(C.Cell, Game.cells(g), G.Grid.path(B.Conf.depth(Game.conf(g)), x, y)) Game.move.left(g, x, y, C.Cell.is_shown(c)) case False{}: Game.move.other(g, x, y, U32.is_eq(btn, 1)) # Button 0 is the left one, 1 the right one, 2 the middle one. def Game.move(+g: Game, +btn: U32, +x: U32, +y: U32) -> G.Grid<&2, C.Cell>: Game.move.pick(g, btn, x, y, U32.is_eq(btn, 0)) # A lost game turns its mines face up and remembers the one that blew. def Game.settle.pick(+g: Game, cg: G.Grid<&2, C.Cell>, ph: B.Phase, x: U32, y: U32) -> Game: match ph: case B.Lost{}: Game{Game.conf(g), B.Board.expose(cg), B.Lost{}, True{}, x, y, Game.tiles(g), Game.scale(g), Game.clock(g)} case B.Won{}: Game{Game.conf(g), cg, B.Won{}, True{}, 0, 0, Game.tiles(g), Game.scale(g), Game.clock(g)} case B.Playing{}: Game{Game.conf(g), cg, B.Playing{}, True{}, 0, 0, Game.tiles(g), Game.scale(g), Game.clock(g)} def Game.settle(+g: Game, +cg: G.Grid<&2, C.Cell>, x: U32, y: U32) -> Game: Game.settle.pick(g, cg, B.Board.status(Game.conf(g), cg), x, y) def Game.press.go(+g: Game, btn: U32, +x: U32, +y: U32) -> Game: +w = Game.wake(g, x, y) Game.settle(w, Game.move(w, btn, x, y), x, y) def Game.press.pick(+g: Game, btn: U32, x: U32, y: U32, ph: B.Phase) -> Game: match ph: case B.Playing{}: Game.press.go(g, btn, x, y) case B.Won{}: g case B.Lost{}: g # A click in window pixels. A finished game ignores it. def Game.press(+g: Game, btn: U32, px: U32, py: U32) -> Game: +sc = Game.scale(g) Game.press.pick(g, btn, U32.shrn(px, sc), U32.shrn(py, sc), Game.phase(g)) def Game.mouse(+g: Game, btn: U32, x: U32, y: U32, down: Bool) -> Game: match down: case True{}: Game.press(g, btn, x, y) case False{}: g def Game.key.pick(+g: Game, restart: Bool) -> Game: match restart: case True{}: Game.reset(g) case False{}: g # `r` starts a new board. def Game.key(+g: Game, code: U32, down: Bool) -> Game: Game.key.pick(g, Bool.and(down, U32.is_eq(code, 114))) # Folds a frame's events into the state and reports whether the window # is still open. Both come out of one walk, because the list a frame # hands back is affine and cannot be walked twice. # # `alive` is a parameter rather than a test on a computed value: a # match may only scrutinise a parameter, so the answer to this round # is carried into the next call. def Game.feed(es: List<&1, Event>, alive: Bool, g: Game) -> Game & Bool: match es alive: case Nil{} a: (g, a) case Con{e, rest} False{}: (g, False{}) case Con{Close{}, rest} True{}: (g, False{}) case Con{Key{code, down}, rest} True{}: Game.feed(rest, True{}, Game.key(g, code, down)) case Con{Mouse{x, y, button, down}, rest} True{}: Game.feed(rest, True{}, Game.mouse(g, button, x, y, down)) case Con{Move{x, y}, rest} True{}: Game.feed(rest, True{}, g) def Game.tile_at(+pt: Paint, +x: U32, +y: U32) -> Image: c = G.Grid.get(C.Cell, Paint.cells(pt), G.Grid.path(Paint.depth(pt), x, y)) boom = Bool.and(U32.is_eq(x, Paint.bx(pt)), U32.is_eq(y, Paint.by(pt))) A.Art.tile(Paint.tiles(pt), A.Art.face(c, Paint.lost(pt), boom)) # Draws the board. The four quadrants run in parallel, and each leaf # only copies a reference to a tile that was drawn once at startup. def Game.paint(d: Nat, +pt: Paint, +x: U32, +y: U32) -> Image: match d: case 0n: Game.tile_at(pt, x, y) case Succ{+k}: +h = U32.shln(1, k) a b c e = Game.paint(k, pt, x, y) Game.paint(k, pt, (x + h : U32), y) Game.paint(k, pt, x, (y + h : U32)) Game.paint(k, pt, (x + h : U32), (y + h : U32)) Qua{a, b, c, e} def Game.image(+g: Game) -> Image: +d = B.Conf.depth(Game.conf(g)) pt = {Paint{Game.cells(g), Game.tiles(g), d, Game.is_lost(g), Game.bx(g), Game.by(g)} : Paint} Game.paint(d, pt, 0, 0) # Mines left to find, never below zero. def Game.left(+g: Game) -> U32: +m = B.Conf.mines(Game.conf(g)) +f = B.Board.flags(Game.cells(g)) A.Art.pick(U32.is_gt(f, m), 0, U32.sub(m, f)) # One frame of the clock. It runs only while a laid-out board is still # in play, so it starts on the first click and stops on a win or a loss. def Game.tock.pick(+g: Game, run: Bool) -> Game: match run: case True{}: Game{Game.conf(g), Game.cells(g), Game.phase(g), Game.live(g), Game.bx(g), Game.by(g), Game.tiles(g), Game.scale(g), Clock.bump(Game.clock(g))} case False{}: g def Game.tock(+g: Game) -> Game: Game.tock.pick(g, Bool.and(Game.live(g), B.Phase.is_playing(Game.phase(g)))) def Game.phase.show(p: B.Phase) -> String: match p: case B.Playing{}: "" case B.Won{}: " - swept!" case B.Lost{}: " - boom (press r)" def Game.title(+g: Game) -> String: "Minesweeper mines left: " ++ U32.show(Game.left(g)) ++ " time: " ++ Nat.show(Clock.secs(Game.clock(g))) ++ Game.phase.show(Game.phase(g)) # The window is as wide as the board: 2^depth cells of 2^scale pixels. def Game.size(+g: Game) -> U32: U32.shln(1, Nat.add(B.Conf.depth(Game.conf(g)), Game.scale(g)))