feat: add flying phase

main
Dozens B. McCuzzins 2024-06-08 20:58:40 -06:00
parent 91b1662302
commit 1250f9f057
12 changed files with 450 additions and 41 deletions

2
doc/musings.txt 100644
View File

@ -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?

201
doc/tilde30.t 100644
View File

@ -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

View File

@ -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

25
lib/all-mills.fnl 100644
View File

@ -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?
}

View File

@ -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))))

View File

@ -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

View File

@ -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?

114
main.fnl
View File

@ -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!"))
)
)
)

View File

@ -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

43
test/flying.dat 100644
View File

@ -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!

View File

@ -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

View File

@ -3,6 +3,8 @@ BEGIN {
print "spawn fennel main.fnl"
}
/^#/ { next }
{ print "expect -re \"Player .'s turn:\""
print "send -- \"" $0 "\\r\""
}