summary refs log tree commit diff
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\""
 }