~/bend-docscommunity

game.bend source

game.bend on the hub · documented module

# 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 Baseimport ./grid.bend as Gimport ./cell.bend as Cimport ./board.bend as Bimport ./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}:      cfdef Game.cells(g: Game) -> G.Grid<&2, C.Cell>:  match g:    case Game{cf, cg, ph, lv, bx, by, ts, sc, ck}:      cgdef Game.phase(g: Game) -> B.Phase:  match g:    case Game{cf, cg, ph, lv, bx, by, ts, sc, ck}:      phdef Game.live(g: Game) -> Bool:  match g:    case Game{cf, cg, ph, lv, bx, by, ts, sc, ck}:      lvdef Game.bx(g: Game) -> U32:  match g:    case Game{cf, cg, ph, lv, bx, by, ts, sc, ck}:      bxdef Game.by(g: Game) -> U32:  match g:    case Game{cf, cg, ph, lv, bx, by, ts, sc, ck}:      bydef Game.tiles(g: Game) -> List<&2, Image>:  match g:    case Game{cf, cg, ph, lv, bx, by, ts, sc, ck}:      tsdef Game.scale(g: Game) -> Nat:  match g:    case Game{cf, cg, ph, lv, bx, by, ts, sc, ck}:      scdef Paint.cells(pt: Paint) -> G.Grid<&2, C.Cell>:  match pt:    case Paint{cg, ts, d, lo, bx, by}:      cgdef Paint.tiles(pt: Paint) -> List<&2, Image>:  match pt:    case Paint{cg, ts, d, lo, bx, by}:      tsdef Paint.depth(pt: Paint) -> Nat:  match pt:    case Paint{cg, ts, d, lo, bx, by}:      ddef Paint.lost(pt: Paint) -> Bool:  match pt:    case Paint{cg, ts, d, lo, bx, by}:      lodef Paint.bx(pt: Paint) -> U32:  match pt:    case Paint{cg, ts, d, lo, bx, by}:      bxdef Paint.by(pt: Paint) -> U32:  match pt:    case Paint{cg, ts, d, lo, bx, by}:      bydef Game.clock(g: Game) -> Clock:  match g:    case Game{cf, cg, ph, lv, bx, by, ts, sc, ck}:      ckdef 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{}:      gdef 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{}:      gdef 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)))