From 1250f9f057c2e21a0edab87f0a6003a25decd1b7 Mon Sep 17 00:00:00 2001 From: dozens Date: Sat, 8 Jun 2024 20:58:40 -0600 Subject: feat: add flying phase --- doc/musings.txt | 2 + doc/tilde30.t | 201 ++++++++++++++++++++++++++++++++++++++++ justfile | 4 + lib/all-mills.fnl | 25 +++++ lib/all-mills.test.fnl | 41 ++++++++ lib/index.fnl | 2 + lib/mill.fnl | 3 +- main.fnl | 114 +++++++++++++++-------- test/capture-oops-all-mills.dat | 51 ++++++++++ test/flying.dat | 43 +++++++++ test/moving-capture.dat | 3 + test/test.awk | 2 + 12 files changed, 450 insertions(+), 41 deletions(-) create mode 100644 doc/musings.txt create mode 100644 doc/tilde30.t create mode 100644 lib/all-mills.fnl create mode 100644 lib/all-mills.test.fnl create mode 100644 test/capture-oops-all-mills.dat create mode 100644 test/flying.dat diff --git a/doc/musings.txt b/doc/musings.txt new file mode 100644 index 0000000..afc8394 --- /dev/null +++ b/doc/musings.txt @@ -0,0 +1,2 @@ +# musings and learnings +2024-06-14T19:42:55-06:00 i sure do miss types sometimes. i'd like a little warning to pop up and warn me that i'm returning a string, e.g., when i ought to be returning a bool. i wonder if i could use some kind of a fake struct.. it would just a table, obviously. but maybe i could write a function that registers structs with a struct registry? and then another function to wrap functions that take a struct as a parameter to enforce the shape of the struct? diff --git a/doc/tilde30.t b/doc/tilde30.t new file mode 100644 index 0000000..5eeb436 --- /dev/null +++ b/doc/tilde30.t @@ -0,0 +1,201 @@ +--- +title: TILDE30 +subtitle: nine mens morris fennel game +author: dozens +created: 2024-06-01 +updated: 2024-06-14 +--- +.pl 999i +.ce +{{title}} + +.ce +{{subtitle}} + +.IP 01 +what is up tilde30 fans it's ya boi dozens back with another up date on my +project! today i found a bug that was preventing mills from being recognized +as mills. my algorithm for detecting mills is kind of (probably needlessly?) +complex. but luckily, i had already written a small module for writing unit +tests. so after a small refactor to isolate the individual steps as functions +that i can export, i imported them into a test file and was able to more +carefully examine each step. turns out the culprit was a small reducer in +which i was doing an 'and' when i ought to have been doing an 'or'. literally +just changed one word and that fixed it. but i'm still pleased with the +process by which i arrived at that realization. i'm now confident that the +entirety of the mill detection algorithm does what i want it to do. yay unit +tests! +. +. +.IP 02 +today's goal was to implement capturing. but instead i discovered a bug in +rendering the board, and fixed that. i didn't bother taking the time to +really understand why the bug was happening. but after rewriting (and +simplifying) the render function, it's working correctly now. so yippee for +that. up next: capturing! +. +. +.IP 03 +went to go do some remote smol computering. posted up at the library and fixed +a small typo that was preventing the game state from advancing from phase 1 +to phase 2. and that's about all i could stand doing whilst coding on my +phone. later, on my laptop, successfully implemented capturing. next +up: prevent a player from capturing a checker that is part of a mill. i think +this will lead to a refactoring of the 'mill?' algorithm to generalize it a +little more. +. +. +.IP 04 +i did not work on tidle30 today. not enough spoons. +. +. +.IP 05 +today i did the refactor of the 'mill?' algorithm. +i ended up making it much more simple than how i had originally written it. +and it works pretty great! +the goal of this refactor was to be able check for a mill +after a move in order to change the game phase from placing to capture, +and also to check for a mill before a move +in order to check whether a capture is legal. +(a capture cannot break up a mill.) +as predicted, +my test-driven development workflow +made the refactor pretty painless. +i just got tripped up for a while +because i didn't realize i was passing the wrong type of move to the function. +you see, i have two different representations of a move: +one is a number 1 - 24 referring to the index of an array of player moves +(that is, the game state is a single array with values 0 = vacant, 1 = occupied by player 1, 2 = player 2); +and the other is an alphanumeric value (e.g. A1, b4, 7G) +referring to a place on the game board. +i frequently have to convert e.g. B2 into 4. +and what i don't really have right now +is a good type system that can tell me if i passed the wrong type to a function. +oh well! +i guess i'll have to do some manual type checking +in each function if i really want that kind of type safety. +this completes milestone 3: capture a checker. +next up: +implement some kind of a play counter +so the game can transition from placing to sliding. +.nf +http://cgit.tilde.town/~dozens/9mm/commit/?id=7776b2011a2585723078b275c838fd7332488d76 +.fi +. +. +.IP 06 +added transitioning from Placing to Moving, +and also implemented Moving logic. +this completes milestones 4 and 5. +starting to feel like a real game! +after getting my ass beat yesterday +by unknown function parameter types, +i added some type assertions to my new function. +and sure enough, +later on i passed the wrong value to the function. +but this time the assertion failed +and gave me a useful error message. +wahoooooo! +no time wasted tonight haha. +up next: +fix a bug that prevents captures from happening +during the moving phase? +watching: +maniac (2018) on netflix, +starring emma stone and jonah hill. +.nf +http://cgit.tilde.town/~dozens/9mm/commit/?id=f985dc4e5c9fdec06436c21440c3dc7245369847 +.fi +. +. +.IP 07 +as i said earlier, +there is currently a bug +that is preventing the board +from updating when a capture happens. +instead of working on the bug, +i instead focused on how sad it makes me +to have to enter 18 - 20 moves every time +just to test the capturing ui in the moving phase of the game. +this sadness inspired me to write an expect(1) script +that will interact with the game ui +and make all the moves for me. +much faster! +then i abstracted and isolated the moves +into a data file +so that for future ui testing, +i can just write down a list of moves +and not write a whole expect script. +and then i wrote a small awk script +that will convert data files into expect files. +so now i have some basic ui scripting, +which is maybe the first step toward actual ui testing? +and i have already used it to confirm the behavior +in both the placing phase +and the moving phase. +so it's not anything unique to phase 2 +despite my original suspicions. +thanks, tests! +up next: +fix the dang bug +.nf +http://cgit.tilde.town/~dozens/9mm/commit/?id=91b1662302c14cf84ca8b90c1f3ec20a585f67a5 +.fi +. +. +.IP 08 +with fresh eyes, +i was able to see the bug that was preventing capturing. +it was a single line in the update function. +and i deleted it! +then i set about trying to allow transitioning +moving to flying. +but i introduced another bug +that i can't find right now.. +it prevents capturing in the moving phase. +i'll have to look at it more later. +right now, +i have to finish packing! +i'm going on vacation! +we'll see whether or not i'm able to continue +working on 9mm / tilde30 +while away from home. +i'm not bringing my laptop. +so i'll be limited to coding on my phone +with my little folding bluetooth keyboard. +. +. +.IP "WEEK ONE REVIEW" +when i look back on week one, +i feel like i made more progress +than i had expected to. +even while spending time on writing unit tests +and "ui tests." +if i keep up this pace +then i expect i'll be done with the game in another week. +but i'm on vacation next week, +so i'm not confident that i will keep up the pace. +whether or not i get around to it as part of tilde30, +i do want to build a gui frontend for the game. +because i think that would be fun. +. +. +.IP "09-13" +i did not do any computering +during this five day low-tech beach vacation. +. +. +.IP 14 +fixed the bug that prevented capturing during the moving phase. +implemented flying! +and also handled an edge case +where you cannot break up a mill +when capturing +unless there are no other non-milled checkers. +in which case +you can break up a mill +when capturing. +up next: +ending the game. + +.pl \n[nl]u diff --git a/justfile b/justfile index e5fa402..4827cef 100644 --- a/justfile +++ b/justfile @@ -9,3 +9,7 @@ test: # build expect scripts expects: for f in test/*.dat; do awk -f test/test.awk $f > ${f/dat/expect}; done + +# make the project +project: + awk '$0 ~ /^---$/ && times++ < 2 { a=!a;next; } a' doc/tilde30.t | recfmt -f doc/tilde30.t | awk '$0 ~ /^---$/ { times++;next } times > 1' | nroff -ms -Tascii diff --git a/lib/all-mills.fnl b/lib/all-mills.fnl new file mode 100644 index 0000000..562bb97 --- /dev/null +++ b/lib/all-mills.fnl @@ -0,0 +1,25 @@ +(local {: mill-at? } (require :lib.mill)) +(local {: mills } (require :lib.constants)) + +(fn toggle-player [p] (if (= p 1) 2 1)) + +(fn only-player-moves [moves player] + (icollect [_ move (ipairs moves)] (if (= move player) player 0))) + +(fn all-moves-are-mills? [moves player] + (accumulate [result true + i m (ipairs moves) ] + (and result (if (= m 0) true (mill-at? mills moves i))))) + +(fn all-mills? [all-moves current-player] + (let [next-player (toggle-player current-player) + player-moves (only-player-moves all-moves next-player) + all-mills (all-moves-are-mills? player-moves current-player)] + all-mills)) + +{: all-mills? + ;; do not use; just for testing: + : toggle-player + : only-player-moves + : all-moves-are-mills? + } diff --git a/lib/all-mills.test.fnl b/lib/all-mills.test.fnl new file mode 100644 index 0000000..7f33ab1 --- /dev/null +++ b/lib/all-mills.test.fnl @@ -0,0 +1,41 @@ +(let [{: describe + :end test-end} (require :lib.test) + {: all-mills? + : toggle-player + : only-player-moves + : all-moves-are-mills? + } (require :lib.all-mills)] + + (describe "all-mills" (fn [] + (describe "#toggle-player()" (fn [t] + (t {:given "a player" + :should "return the next" + :expected 2 + :actual (toggle-player 1) + }))) + (describe "#only-player-moves()" (fn [t] + (let [moves [ 0 2 0 2 2 2 0 0 0 0 0 0 0 2 0 0 0 2 0 2 0 1 1 1 ] + expected [ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 ] + ] + (t {:given "a bunch of moves and a player" + :should "filter out all the moves not belonging to the player" + : expected + :actual (only-player-moves moves 1) + })))) + (describe "#all-moves-are-mills?()" (fn [t] + (let [moves [ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 ] + ] + (t {:given "a bunch of moves and a player" + :should "return true if all the player moves are mills" + :expected true + :actual (all-moves-are-mills? moves 1) + })) + (let [moves [ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 ] + ] + (t {:given "a bunch of moves and no mill and a player" + :should "return false" + :expected false + :actual (all-moves-are-mills? moves 1) + })))) + (test-end)))) + diff --git a/lib/index.fnl b/lib/index.fnl index 1fb686c..601ccb0 100644 --- a/lib/index.fnl +++ b/lib/index.fnl @@ -1,3 +1,4 @@ +(local {: all-mills?} (require :lib.all-mills)) (local {: contains} (require :lib.contains)) (local {: head} (require :lib.head)) (local {: keys} (require :lib.keys)) @@ -9,6 +10,7 @@ (local {: tail} (require :lib.tail)) { + : all-mills? : contains : head : keys diff --git a/lib/mill.fnl b/lib/mill.fnl index 14df2e7..f9c8673 100644 --- a/lib/mill.fnl +++ b/lib/mill.fnl @@ -28,8 +28,7 @@ (let [candidates (get-candidates all-mills move) my-moves (candidate-moves candidates current-moves) my-mills (move-mills my-moves) - result (any my-mills) - ] + result (any my-mills)] result)) {: mill-at? diff --git a/main.fnl b/main.fnl index bf6933e..fae5445 100644 --- a/main.fnl +++ b/main.fnl @@ -5,6 +5,7 @@ : kvflip : pprint : slice + : all-mills? :mill-at? mill-at-maker :space-is-neighbor? space-is-neighbor-maker } (require :lib.index)) @@ -28,21 +29,29 @@ }) +;; story mode: ;; there are two players -;; their names are LUIGI and MARIO +;; their names are WIGI and MALO (local player { - :one 1 ;; luigi has light cows - :two 2 ;; mario has DARK cows >:) + :one 1 ;; wigi has light cows + :two 2 ;; malo has DARK cows >:) }) ; return the numerical index (1-24) of a [A-Za-z0-9] formatted move (fn index-of-move [m] - (let [upper (string.upper m) - rev (string.reverse upper) - idx (head (icollect [i v (ipairs const.spaces)] - (if (or (= v upper) (= v rev)) i)))] - idx)) + (assert (= "string" (type m)) "index-of-move needs a string argument") + (let [upper (string.upper m) + rev (string.reverse upper) + idx (head (icollect [i v (ipairs const.spaces)] + (if (or (= v upper) (= v rev)) i)))] + idx)) + + +(fn player-count [moves player] + (accumulate [count 0 + _ x (ipairs moves)] + (if (= x player) (+ count 1) count))) ;; game state object @@ -51,34 +60,57 @@ :stage stages.placing :update (fn [self move] (case self.stage - 4 ;; capture + 4 ;; CAPTURE (do - ;; TODO: capturing during moving is not working? (tset self.moves (index-of-move move) 0) (tset self :player (self:next-player)) - (tset self :stage (if (> self.pieces-placed 17) stages.moving stages.placing)) - (tset self.moves (index-of-move move) self.player) - ) - 1 ;; placing + (let [flytime (and (> self.pieces-placed 17) (= 3 (player-count self.moves self.player))) + movetime (and (> self.pieces-placed 17) (> (player-count self.moves self.player) 3))] + (tset self :stage (if flytime stages.flying + movetime stages.moving + stages.placing)))) + 1 ;; PLACING (do (set self.pieces-placed (+ 1 self.pieces-placed)) (tset self :stage (if (> self.pieces-placed 17) stages.moving stages.placing)) (tset self.moves (index-of-move move) self.player) - (if (mill-at? self.moves (index-of-move move)) - (tset self :stage stages.capture) - (tset self :player (self:next-player)) - ) - ) - 2 ;; moving + (let [flytime (and (> self.pieces-placed 17) (= 3 (player-count self.moves self.player))) + movetime (and (> self.pieces-placed 17) (> (player-count self.moves self.player) 3)) + capturetime (mill-at? self.moves (index-of-move move))] + (tset self :stage (if + capturetime stages.capture + flytime stages.flying + movetime stages.moving + stages.placing)) + (if (not capturetime) (tset self :player (self:next-player))))) + 2 ;; MOVING (let [from (index-of-move (string.sub move 1 2)) to (index-of-move (string.sub move -2 -1))] + (tset self.moves from 0) + (tset self.moves to self.player) + (let [flytime (and (> self.pieces-placed 17) (= 3 (player-count self.moves (self:next-player)))) + movetime (and (> self.pieces-placed 17) (> (player-count self.moves (self:next-player)) 3)) + capturetime (mill-at? self.moves (index-of-move (string.sub move -2 -1)))] + (tset self :stage (if + capturetime stages.capture + flytime stages.flying + movetime stages.moving + stages.placing)) + (if (not capturetime) (tset self :player (self:next-player))))) + 3 ;; FLYING + (let [from (index-of-move (string.sub move 1 2)) + to (index-of-move (string.sub move -2 -1))] (tset self.moves from 0) (tset self.moves to self.player) - (if (mill-at? self.moves to) - (tset self :stage stages.capture) - (tset self :player (self:next-player)) - ) - ) + (let [flytime (and (> self.pieces-placed 17) (= 3 (player-count self.moves (self:next-player)))) + movetime (and (> self.pieces-placed 17) (> (player-count self.moves (self:next-player)) 3)) + capturetime (mill-at? self.moves (index-of-move (string.sub move -2 -1)))] + (tset self :stage (if + capturetime stages.capture + flytime stages.flying + movetime stages.moving + stages.placing)) + (if (not capturetime) (tset self :player (self:next-player))))) ) ) :next-player (fn [self] (if (= player.one self.player) player.two player.one)) @@ -98,6 +130,7 @@ (game:init) +; TODO: move to lib utility (fn string-upper [s] (.. (string.upper (string.sub s 1 1)) (string.sub s 2))) @@ -136,27 +169,23 @@ (let [unoccupied? 0] ; i.e. is move equal to 0 (= unoccupied? (. game.moves (index-of-move m))))) - +; is the space m occupied by the player's opponent? (fn space-is-occupied-by-opponent? [m] + "is the space m occupied by the player's opponent?" (let [opponent (if (= game.player 1) 2 1) result (= opponent (. game.moves (index-of-move m))) ] result)) - +; checks that the first 2 charcters and the last 2 characters +; of a string are legal spaces +; moving-format is the same as flying-format (fn moving-format? [m] (let [from (string.sub m 1 2) to (string.sub m -2 -1)] - (and (space-exists? from) (space-exists? to)))) + (and (>= (length m) 4) (space-exists? from) (space-exists? to)))) + ; is this a legal move? -; maybe some functional error handling here? -; https://mostly-adequate.gitbook.io/mostly-adequate-guide/ch08#pure-error-handling -; https://mostly-adequate.gitbook.io/mostly-adequate-guide/appendix_b#either -; or maybe all i need is a case-try statement.. -; https://fennel-lang.org/reference#case-try-for-matching-multiple-steps -; update: i didn't really like that -; i think maybe i do want the monad after all.. -; i'll come back to it later (fn valid-move? [move] (or (and @@ -169,8 +198,10 @@ (= stages.capture game.stage) (or (space-is-occupied-by-opponent? move) (print "Choose an opponent's piece to remove.")) - (or (not (mill-at? game.moves (index-of-move move))) - (print "Ma'am, it is ILLEGAL to break up a mill.")) + (or (or (all-mills? game.moves game.player) + (not (mill-at? game.moves (index-of-move move)))) + (print "Ma'am, it is ILLEGAL to break up a mill.") + ) ) (and (= stages.moving game.stage) @@ -184,8 +215,13 @@ (print "That ain't your neighbor, Johnny")) ) (and - ;; TODO: add flying phase (= stages.flying game.stage) + (or (moving-format? move) + (print "Try a move like A1A2 or A7 D7")) + (or (not (space-is-occupied-by-opponent? (string.sub move 1 2))) + (print "That's not yours, don't touch it.")) + (or (space-is-unoccupied? (string.sub move -2 -1)) + (print "That space is occupied!")) ) ) ) diff --git a/test/capture-oops-all-mills.dat b/test/capture-oops-all-mills.dat new file mode 100644 index 0000000..96fec37 --- /dev/null +++ b/test/capture-oops-all-mills.dat @@ -0,0 +1,51 @@ +# PLACING PHASE (18 moves) +A1 +A4 +A7 +b2 +b4 +b6 +c3 +c4 +c5 +d1 +d2 +d3 +d5 +d6 +d7 +e3 +e4 +e5 +# MOVING PHASE (6 captures) +e4f4 +e3e4 +f4g4 +d3e3 +b4 +g4g1 +c4b4 +d2 +d7g7 +e3d3 +c5c4 +b2d2 +a1 +c4c5 +d2b2 +d5 +g7g4 +b2d2 +a7 +c3c4 +d2b2 +c4 +# FLYING PHASE! +c5g7 +d3 +e4f4 +g7f6 +b2d2 +f6g7 +d1 +d2b2 diff --git a/test/flying.dat b/test/flying.dat new file mode 100644 index 0000000..a76cbf8 --- /dev/null +++ b/test/flying.dat @@ -0,0 +1,43 @@ +# PLACING PHASE (18 moves) +A1 +A4 +A7 +b2 +b4 +b6 +c3 +c4 +c5 +d1 +d2 +d3 +d5 +d6 +d7 +e3 +e4 +e5 +# MOVING PHASE (6 captures) +e4f4 +e3e4 +f4g4 +d3e3 +b4 +g4g1 +c4b4 +d2 +d7g7 +e3d3 +c5c4 +b2d2 +a1 +c4c5 +d2b2 +d5 +g7g4 +b2d2 +a7 +c3c4 +d2b2 +c4 +# FLYING PHASE! diff --git a/test/moving-capture.dat b/test/moving-capture.dat index d19ede6..a032eaf 100644 --- a/test/moving-capture.dat +++ b/test/moving-capture.dat @@ -1,3 +1,4 @@ +# placing time A1 A4 A7 @@ -16,7 +17,9 @@ d7 e3 e4 e5 +# moving time e4f4 e3e4 f4g4 +# player 2 to capture: d3e3 diff --git a/test/test.awk b/test/test.awk index ef6df20..2e3e301 100644 --- a/test/test.awk +++ b/test/test.awk @@ -3,6 +3,8 @@ BEGIN { print "spawn fennel main.fnl" } +/^#/ { next } + { print "expect -re \"Player .'s turn:\"" print "send -- \"" $0 "\\r\"" } -- cgit 1.4.1-2-gfad0