9mm/main.fnl

283 lines
6.3 KiB
Plaintext
Raw Normal View History

2024-05-28 21:04:00 +00:00
; Introducing:
; Nine Mens Morris
; The Game
;
; Featuring:
; Fennel
; The Language
;
; By:
; dozens
; the human
;
; Do you know what Nine Mens Morris looks like?
; It has three concentric rings, each containing eight spaces.
; Here's what it looks like:
;
; 1-----2-----3
; | | |
; | 4---5---6 |
; | | | | |
; | | 7-8-9 | |
; | | | | | |
; 0-1-2 3-4-5 +10
; | | | | | |
; | | 6-7-8 | |
; | | | | |
; | 9---0---1 | +20
; | | |
; 2-----3-----4
;; helper and utility functions
(local {
:contains contains
:head head
:mill? mill-maker
:pprint pprint
} (require :lib.index))
; there are three phases of play:
; placing, moving, and flying.
; (plus one for capturing)
; (plus one for complete)
(local stages {
:placing 1
:moving 2
:flying 3
:capture 4
:complete 5
})
; there are two players
; their names are LUIGI and MARIO
(local player {
:one 1 ;; luigi
:two 2 ;; mario
})
; initialize moves[] to 0.
; this is the game state.
; shows which spaces are occupied by which players.
; 0 = unoccupied
; 1 = Player 1
; 2 = Player 2
(local moves (fcollect [i 1 24] 0))
(local rules {
; what moves are legal from each space
; slash what neighbors does each space have
:neighbors [
[1 2 10]
[2 1 3 5]
[3 2 15]
[4 5 11]
[5 2 4 6 8]
[6 5 14]
[7 8 12]
[8 5 7 9]
[9 8 13]
[10 1 11 22]
[11 4 10 12 19]
[12 7 11 16]
[13 9 14 18]
[14 6 13 15 21]
[15 3 14 24]
[16 12 17]
[17 16 18 20]
[18 13 17]
[19 11 20]
[20 17 19 21 23]
[21 14 20]
[22 10 23]
[23 20 22 24]
[24 15 23]
]
; sixteen combinations of spaces form a mill
:mills [
[1 2 3]
[4 5 6]
[7 8 9]
[10 11 12]
[13 14 15]
[16 17 18]
[19 20 21]
[22 23 24]
[1 10 22]
[4 11 19]
[7 12 16]
[2 5 8]
[17 20 23]
[9 13 18]
[6 14 21]
[3 15 24]
]
})
(fn mill? [state move] (partial mill-maker rules.mills))
; game state object
(local game {
:player player.one
:stage stages.placing
:update (fn [self move]
(if (mill? moves move)
(do
(print "MILLLLLLLLLLLLL!")
(tset self :stage stages.capture)
)
(tset self :player (if (= player.one self.player) player.two player.one))
)
)
})
; This is what the game board looks like
; it's also used to display the state of the game
; the Xs are converted to "%d" later for string templating
; they are Xs here so that it looks pretty =)
(local board [
" 1 2 3 4 5 6 7"
"A x-----x-----x"
" | | |"
"B | x---x---x |"
" | | | | |"
"C | | x-x-x | |"
" | | | | | |"
"D x-x-x x-x-x"
" | | | | | |"
"E | | x-x-x | |"
" | | | | |"
"F | x---x---x |"
" | | |"
"G x-----x-----x"
])
; Print! That! Board!
(fn print-board [board moves]
(var total-count -2) ; lol, m-a-g-i-c
; just kidding, it's so that -2 + 3 = 1
; which is where i want to start indexing my table
(each [_ row (ipairs board)]
(let [(template count) (string.gsub row "x" "%%d")]
(if (> count 0)
(do
(set total-count (+ total-count count)) ; where i need that magic number on first iteration
(print (string.format template (select total-count (table.unpack moves)))))
(print row)))))
; `select` above does NOT do what i thought it did.
; i thought it would return the first x values given (select x values)
; instead it returns the rest of the table having discarded the first x values
; i think that `pick-values` probably does what i thought `select` does
; these are the only moves that are valid
; i am somewhat bothered by all the wasted space
; by 2-3A and 5-6A e.g.
; Incidentally these are all in order of appearance
; so when you find a match,
; you can also update that index of `moves` to the current player number
(local valid-spaces [
"1A" "4A" "7A"
"2B" "4B" "6B"
"3C" "4C" "5C"
"1D" "2D" "3D"
"5D" "6D" "7D"
"3E" "4E" "5E"
"2F" "4F" "5F"
"1G" "4G" "7G"
])
; add the inverse of each valid move
; e.g. 1A = A1
(fn add-reverse-moves []
(let [reversed (icollect [_ v (ipairs valid-spaces)] (string.reverse v))]
(each [_ v (ipairs reversed)]
(table.insert valid-spaces v))))
(add-reverse-moves)
; does the move exist within the domain of valid spaces
(fn space-exists? [m] (contains valid-spaces (string.upper m)))
; return the numerical index of a "A1" formatted move
(fn index-of-move [m]
(let [ upper (string.upper m)
rev (string.reverse upper)
idx (head (icollect [i v (ipairs valid-spaces)]
(if (or (= v upper) (= v rev)) i)))
]
idx))
; is the space represented by a move ("A1") unoccupied?
(fn space-is-unoccupied? [m]
(let [unoccupied? 0]
(= unoccupied? (. moves (index-of-move m)))))
; is this a legal move?
; TODO: 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
(= stages.placing game.stage)
(or (space-exists? move) (print "That space does not exist!\nHint: 1a 1A A1 a1 are all valid moves."))
(or (space-is-unoccupied? move) (print "That space is occupied!"))))
(and
;; TODO: add capturing phase
(= stages.capturing game.stage)
)
(and
;; TODO: add flying phase
(= stages.flying game.stage)
)
)
; get player input
(fn get-move []
(print (.. "Player " game.player "'s turn:"))
(io.read))
(fn main []
;; game loop
(while (not (= game.stage stages.complete))
(print-board board moves)
;; validation loop
(var is-valid false)
(var move "")
(while (not is-valid)
(set move (get-move))
(set is-valid (valid-move? move))
(if (not is-valid)
(print "Try again.")
(do
(print (.. "You chose " move))
(tset moves (index-of-move move) game.player)
(game:update move)
)
)
)
)
)
(main)