summary refs log tree commit diff
path: root/main.fnl
diff options
context:
space:
mode:
authordozens2024-05-28 15:04:00 -0600
committerdozens2024-05-28 15:04:00 -0600
commitf265d24c0cacb92c7f7db19f364a155d87938184 (patch)
treea6e3ce61dee9020b79c8f2d8e3c91b51bc29e959 /main.fnl
inits
Diffstat (limited to 'main.fnl')
-rw-r--r--main.fnl282
1 files changed, 282 insertions, 0 deletions
diff --git a/main.fnl b/main.fnl
new file mode 100644
index 0000000..169f4f9
--- /dev/null
+++ b/main.fnl
@@ -0,0 +1,282 @@
+; 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)