summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/musings.txt2
-rw-r--r--doc/tilde30.t201
-rw-r--r--justfile4
-rw-r--r--lib/all-mills.fnl25
-rw-r--r--lib/all-mills.test.fnl41
-rw-r--r--lib/index.fnl2
-rw-r--r--lib/mill.fnl3
-rw-r--r--main.fnl114
-rw-r--r--test/capture-oops-all-mills.dat51
-rw-r--r--test/flying.dat43
-rw-r--r--test/moving-capture.dat3
-rw-r--r--test/test.awk2
12 files changed, 450 insertions, 41 deletions
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\""
}