feat: add end game

main
Dozens B. McCuzzins 2024-06-15 21:15:59 -06:00
parent 1250f9f057
commit ce09973e7c
9 changed files with 220 additions and 55 deletions

View File

@ -197,5 +197,55 @@ you can break up a mill
when capturing. when capturing.
up next: up next:
ending the game. ending the game.
.
.
.IP 15
implemented a game ending!
now if a player has fewer than 3 checkers,
the other player wins the game.
up next:
endgame edge case where if a player has 3 or more checkers,
but no available legal moves,
then they lose.
.
.
.IP "WEEK TWO REVIEW"
light week.
spent almost all of it on vacation
and not working on tilde30 at all.
nonetheless,
i'm mostly finished with the core of the game.
i have one small edge case to iron out
and then the game will be all the way complete.
i think for my first stretch goal,
i want to add some kind of generative story mode
based on player moves and decisions, etc.
so that by the time you're done with the game,
you have a unique little story to take with you.
i'm not sure whether i want to do a tracery grammar
type of thing..
could be fun to try to write that.
well here's to tilde30 being half over!
hope everybody is having fun making progress
on your projects!
.
.
.IP 16
I wrote the "no-moves?" function
to determine whether a player has no legal moves remaining.
And a test for it.
But integrating it created a bug I need to track down.
.
.
.IP 17
Didn't fix the bug,
but rewrote "no-moves?"
using the "->" threading macro
which is neat.
I've never used it before.
It's basically the "compose" or "pipe" function
that I have enjoyed using before in javascript.
up next: fix that bug!
.pl \n[nl]u .pl \n[nl]u

View File

@ -1,23 +1,15 @@
(local str (require :lib.string))
(local tbl (require :lib.table))
(local {: all-mills?} (require :lib.all-mills)) (local {: all-mills?} (require :lib.all-mills))
(local {: contains} (require :lib.contains))
(local {: head} (require :lib.head))
(local {: keys} (require :lib.keys))
(local {: kvflip} (require :lib.kvflip))
(local {: mill-at?} (require :lib.mill)) (local {: mill-at?} (require :lib.mill))
(local {: pprint} (require :lib.tableprint))
(local {: slice} (require :lib.slice))
(local {: space-is-neighbor?} (require :lib.space-is-neighbor)) (local {: space-is-neighbor?} (require :lib.space-is-neighbor))
(local {: tail} (require :lib.tail)) (local {: no-moves?} (require :lib.no-moves))
{ {
: str
: tbl
: all-mills? : all-mills?
: contains
: head
: keys
: kvflip
: mill-at? : mill-at?
: pprint : no-moves?
: slice
: space-is-neighbor? : space-is-neighbor?
: tail
} }

26
lib/no-moves.fnl 100644
View File

@ -0,0 +1,26 @@
(local {: tail} (require :lib.tail))
(fn get-player-idxs [player moves]
(icollect [i p (ipairs moves)] (when (= p player) i)))
(fn idx-to-neighbors [idxs all-neighbors]
(icollect [_ i (ipairs idxs)] (tail (. all-neighbors i))))
(fn neighbor-is-occupied? [neighbors moves]
(icollect [_ move (ipairs neighbors)]
(icollect [_ neighbor (ipairs move)]
(not= (. moves neighbor) 0))))
(fn reduce-to-bool [xs]
(accumulate [acc true
_ x (ipairs xs)]
(and x)))
(fn no-moves? [neighbors all-moves player]
(-> (get-player-idxs player all-moves)
(idx-to-neighbors neighbors)
(neighbor-is-occupied? all-moves)
(reduce-to-bool)
(reduce-to-bool)))
{: no-moves? }

View File

@ -0,0 +1,51 @@
(let [{: no-moves?} (require :lib.no-moves)
{: neighbors} (require :lib.constants)
{: describe :end test-end} (require :lib.test)
with-neighbors (partial no-moves? neighbors)
]
(describe "no-moves()" (fn [t]
(let [moves [ 1 2 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 ]
player 1
]
(t {:given "one move with no moves"
:should "return true"
:expected true
:actual (with-neighbors moves player)
}))
(let [moves [ 1 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ]
player 1
]
(t {:given "one move with one move"
:should "return false"
:expected false
:actual (with-neighbors moves player)
}))
(let [moves [ 1 1 1 0 2 0 0 0 0 2 0 0 0 0 2 0 0 0 0 0 0 0 ]
player 1
]
(t {:given "several moves with no moves"
:should "return true"
:expected true
:actual (with-neighbors moves player)
}))
(let [moves [ 0 2 0 2 1 2 0 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ]
player 1
]
(t {:given "four occupied neighbors"
:should "return true"
:expected true
:actual (with-neighbors moves player)
}))
(let [moves [ 1 2 1 2 0 2 1 2 1 2 1 0 1 2 1 2 2 2 0 1 0 0 0 0 0 ]
player 2
]
(t {:given "this turn that is giving me trouble"
:should "return true"
:expected true
:actual (with-neighbors moves player)
}))
(test-end))))

4
lib/string.fnl 100644
View File

@ -0,0 +1,4 @@
(fn capitalize [s]
(.. (string.upper (string.sub s 1 1)) (string.sub s 2)))
{: capitalize}

17
lib/table.fnl 100644
View File

@ -0,0 +1,17 @@
(local {: contains} (require :lib.contains))
(local {: head} (require :lib.head))
(local {: keys} (require :lib.keys))
(local {:kvflip invert} (require :lib.kvflip))
(local {:pprint print} (require :lib.tableprint))
(local {: slice} (require :lib.slice))
(local {: tail} (require :lib.tail))
{
: contains
: head
: keys
: invert
: print
: slice
: tail
}

View File

@ -1,19 +1,18 @@
;; helper and utility functions ;; helper and utility functions
(local { (local {
: contains : str
: head : tbl
: kvflip
: pprint
: slice
: all-mills? : all-mills?
:mill-at? mill-at-maker :mill-at? mill-at-maker
:space-is-neighbor? space-is-neighbor-maker :space-is-neighbor? space-is-neighbor-maker
:no-moves? no-moves-maker
} (require :lib.index)) } (require :lib.index))
;; constants...more like just strings ;; constants...more like just strings
(local const (require :lib.constants)) (local const (require :lib.constants))
;; front-loading with some partials ;; front-loading with some partials
(local mill-at? (partial mill-at-maker const.mills)) (local mill-at? (partial mill-at-maker const.mills))
(local space-is-neighbor? (partial space-is-neighbor-maker const.neighbors)) (local space-is-neighbor? (partial space-is-neighbor-maker const.neighbors))
(local no-moves? (partial no-moves-maker const.neighbors))
;; there are three phases of play: ;; there are three phases of play:
@ -43,7 +42,7 @@
(assert (= "string" (type m)) "index-of-move needs a string argument") (assert (= "string" (type m)) "index-of-move needs a string argument")
(let [upper (string.upper m) (let [upper (string.upper m)
rev (string.reverse upper) rev (string.reverse upper)
idx (head (icollect [i v (ipairs const.spaces)] idx (tbl.head (icollect [i v (ipairs const.spaces)]
(if (or (= v upper) (= v rev)) i)))] (if (or (= v upper) (= v rev)) i)))]
idx)) idx))
@ -63,19 +62,24 @@
4 ;; CAPTURE 4 ;; CAPTURE
(do (do
(tset self.moves (index-of-move move) 0) (tset self.moves (index-of-move move) 0)
(tset self :player (self:next-player)) (let [flytime (and (self:phase-two?) (= 3 (player-count self.moves (self:next-player))))
(let [flytime (and (> self.pieces-placed 17) (= 3 (player-count self.moves self.player))) movetime (and (self:phase-two?) (> (player-count self.moves (self:next-player)) 3))
movetime (and (> self.pieces-placed 17) (> (player-count self.moves self.player) 3))] endtime (and (self:phase-two?)
(tset self :stage (if flytime stages.flying (or (< (length (icollect [_ m (ipairs self.moves)] (if (= m 1) 1))) 3)
(< (length (icollect [_ m (ipairs self.moves)] (if (= m 2) 2))) 3)))]
(tset self :stage (if endtime stages.complete
flytime stages.flying
movetime stages.moving movetime stages.moving
stages.placing)))) stages.placing))
(if (not endtime) (tset self :player (self:next-player)))
))
1 ;; PLACING 1 ;; PLACING
(do (do
(set self.pieces-placed (+ 1 self.pieces-placed)) (set self.pieces-placed (+ 1 self.pieces-placed))
(tset self :stage (if (> self.pieces-placed 17) stages.moving stages.placing)) (tset self :stage (if (self:phase-two?) stages.moving stages.placing))
(tset self.moves (index-of-move move) self.player) (tset self.moves (index-of-move move) self.player)
(let [flytime (and (> self.pieces-placed 17) (= 3 (player-count self.moves self.player))) (let [flytime (and (self:phase-two?) (= 3 (player-count self.moves self.player)))
movetime (and (> self.pieces-placed 17) (> (player-count self.moves self.player) 3)) movetime (and (self:phase-two?) (> (player-count self.moves self.player) 3))
capturetime (mill-at? self.moves (index-of-move move))] capturetime (mill-at? self.moves (index-of-move move))]
(tset self :stage (if (tset self :stage (if
capturetime stages.capture capturetime stages.capture
@ -88,13 +92,15 @@
to (index-of-move (string.sub move -2 -1))] to (index-of-move (string.sub move -2 -1))]
(tset self.moves from 0) (tset self.moves from 0)
(tset self.moves to self.player) (tset self.moves to self.player)
(let [flytime (and (> self.pieces-placed 17) (= 3 (player-count self.moves (self:next-player)))) (let [flytime (and (self:phase-two?) (= 3 (player-count self.moves (self:next-player))))
movetime (and (> self.pieces-placed 17) (> (player-count self.moves (self:next-player)) 3)) movetime (and (self:phase-two?) (> (player-count self.moves (self:next-player)) 3))
capturetime (mill-at? self.moves (index-of-move (string.sub move -2 -1)))] capturetime (mill-at? self.moves (index-of-move (string.sub move -2 -1)))
endtime (no-moves? self.moves (self:next-player))]
(tset self :stage (if (tset self :stage (if
capturetime stages.capture capturetime stages.capture
flytime stages.flying flytime stages.flying
movetime stages.moving movetime stages.moving
endtime stages.complete
stages.placing)) stages.placing))
(if (not capturetime) (tset self :player (self:next-player))))) (if (not capturetime) (tset self :player (self:next-player)))))
3 ;; FLYING 3 ;; FLYING
@ -102,8 +108,8 @@
to (index-of-move (string.sub move -2 -1))] to (index-of-move (string.sub move -2 -1))]
(tset self.moves from 0) (tset self.moves from 0)
(tset self.moves to self.player) (tset self.moves to self.player)
(let [flytime (and (> self.pieces-placed 17) (= 3 (player-count self.moves (self:next-player)))) (let [flytime (and (self:phase-two?) (= 3 (player-count self.moves (self:next-player))))
movetime (and (> self.pieces-placed 17) (> (player-count self.moves (self:next-player)) 3)) movetime (and (self:phase-two?) (> (player-count self.moves (self:next-player)) 3))
capturetime (mill-at? self.moves (index-of-move (string.sub move -2 -1)))] capturetime (mill-at? self.moves (index-of-move (string.sub move -2 -1)))]
(tset self :stage (if (tset self :stage (if
capturetime stages.capture capturetime stages.capture
@ -111,10 +117,17 @@
movetime stages.moving movetime stages.moving
stages.placing)) stages.placing))
(if (not capturetime) (tset self :player (self:next-player))))) (if (not capturetime) (tset self :player (self:next-player)))))
5 ;; COMPLETE
(print "Unreachable!")
) )
(tset self :turns (+ self.turns 1))
) )
:next-player (fn [self] (if (= player.one self.player) player.two player.one)) :next-player (fn [self] (if (= player.one self.player) player.two player.one))
:pieces-placed 0 :pieces-placed 0
:turns 0
; so basically there's phase 1 where you place your checkers
; and then phase 2 when you move and fly around trying to capture pieces
:phase-two? (fn [self] (> self.pieces-placed 17))
:init (fn [self] :init (fn [self]
; initialize moves[] to 0. ; initialize moves[] to 0.
; this is the game state. ; this is the game state.
@ -130,11 +143,6 @@
(game:init) (game:init)
; TODO: move to lib utility
(fn string-upper [s]
(.. (string.upper (string.sub s 1 1)) (string.sub s 2)))
; Print! That! Board! ; Print! That! Board!
(fn print-board [board moves] (fn print-board [board moves]
(var index 1) (var index 1)
@ -143,12 +151,13 @@
(if (> slots 0) (if (> slots 0)
(do (do
(let [offset (+ index slots) (let [offset (+ index slots)
myslice (slice moves index offset)] myslice (tbl.slice moves index offset)]
(print (string.format row-template (table.unpack myslice))) (print (string.format row-template (table.unpack myslice)))
(set index offset))) (set index offset)))
(print row)))) (print row))))
(print (.. "Stage: " (string-upper (. (kvflip stages) game.stage)))) (print (.. "Stage: " (str.capitalize (. (tbl.invert stages) game.stage))))
(print (.. "Player " game.player "'s turn:"))) (print (.. "Player " game.player "'s turn:")))
(local with-board (partial print-board const.board))
; add the inverse of each valid move ; add the inverse of each valid move
@ -161,7 +170,7 @@
; does the move exist within the domain of valid spaces ; does the move exist within the domain of valid spaces
(fn space-exists? [m] (contains const.spaces (string.upper m))) (fn space-exists? [m] (tbl.contains const.spaces (string.upper m)))
; is the space represented by a [A-Za-z0-9] move unoccupied? ; is the space represented by a [A-Za-z0-9] move unoccupied?
@ -201,8 +210,7 @@
(or (or (all-mills? game.moves game.player) (or (or (all-mills? game.moves game.player)
(not (mill-at? game.moves (index-of-move move)))) (not (mill-at? game.moves (index-of-move move))))
(print "Ma'am, it is ILLEGAL to break up a mill.") (print "Ma'am, it is ILLEGAL to break up a mill.")
) ))
)
(and (and
(= stages.moving game.stage) (= stages.moving game.stage)
(or (moving-format? move) (or (moving-format? move)
@ -212,8 +220,7 @@
(or (space-is-unoccupied? (string.sub move -2 -1)) (or (space-is-unoccupied? (string.sub move -2 -1))
(print "That space is occupied!")) (print "That space is occupied!"))
(or (space-is-neighbor? (index-of-move (string.sub move 1 2)) (index-of-move (string.sub move -2 -1))) (or (space-is-neighbor? (index-of-move (string.sub move 1 2)) (index-of-move (string.sub move -2 -1)))
(print "That ain't your neighbor, Johnny")) (print "That ain't your neighbor, Johnny")) )
)
(and (and
(= stages.flying game.stage) (= stages.flying game.stage)
(or (moving-format? move) (or (moving-format? move)
@ -221,8 +228,7 @@
(or (not (space-is-occupied-by-opponent? (string.sub move 1 2))) (or (not (space-is-occupied-by-opponent? (string.sub move 1 2)))
(print "That's not yours, don't touch it.")) (print "That's not yours, don't touch it."))
(or (space-is-unoccupied? (string.sub move -2 -1)) (or (space-is-unoccupied? (string.sub move -2 -1))
(print "That space is occupied!")) (print "That space is occupied!")))
)
) )
) )
@ -235,8 +241,7 @@
(fn main [] (fn main []
;; game loop ;; game loop
(while (not (= game.stage stages.complete)) (while (not (= game.stage stages.complete))
(print-board const.board game.moves) (with-board game.moves)
;; validation loop ;; validation loop
(var is-valid false) (var is-valid false)
(var move "") (var move "")
@ -246,11 +251,10 @@
(if (not is-valid) (if (not is-valid)
(print "Try again.") (print "Try again.")
(do (do
(print (.. "You chose " move)) (print (string.format "Turn %d: You chose %s" game.turns move))
(game:update move) (game:update move)))))
) ;; game is complete
) (print "Congratulations!")
) (print (string.format "Player %d is the winner!" game.player))
)
) )
(main) (main)

View File

@ -0,0 +1,17 @@
# this creates a board with with to test the
# "Unless There's No Other Option" exception
# to the "No Breaking Up Mills" capture rule.
# Player 1 is in a position to capture F2 or
# F4, and should not be able to capture any
# checkers from the B2-B4-B6 mill
a1
b2
a4
b4
d7
b6
d7
f2
d6
f4
a7

View File

@ -1,3 +1,7 @@
## This sets up the board to test the "Unless There's No Other Option"
# exception to the "No Breaking Up Mills" capture rule. Player 2 is
# ready to capture, but all of Player 1's checkers are in a mill.
# So Player 2 should be able to capture any piece from the G1-G4-G7 mill.
# PLACING PHASE (18 moves) # PLACING PHASE (18 moves)
A1 A1
A4 A4