summary refs log tree commit diff
path: root/main.fnl
blob: af23d90ad2f1d042f5d2d29941242deec35ece8e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
;; helper and utility functions
(local {
  : contains
  : head
  : kvflip
  : pprint
  : slice
  :mill-at? mill-at-maker
  :space-is-neighbor? space-is-neighbor-maker
  } (require :lib.index))
;; constants...more like just strings
(local const (require :lib.constants))
;; front-loading with some partials
(local mill-at? (partial mill-at-maker const.mills))
(local space-is-neighbor? (partial space-is-neighbor-maker const.neighbors))


;; there are three phases of play:
;; placing, moving, and flying.
;; (plus one for capturing)
;; (plus one for game-over)
(local stages {
  :placing  1 ;; placing the cows
  :moving   2 ;; moving the cows
  :flying   3 ;; flying the cows
  :capture  4 ;; capture a cow (we do not shoot cows)
  :complete 5 ;; no more cows! jk the cows are fine. the game's just over okay
})


;; there are two players
;; their names are LUIGI and MARIO
(local player {
  :one 1 ;; luigi has light cows
  :two 2 ;; mario 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))


;; game state object
(local game {
  :player player.one
  :stage stages.placing
  :update (fn [self move]
    (case self.stage
      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
        (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 [from (index-of-move (string.sub move 1 2))
              to  (index-of-move (string.sub move -2 -1))]
            (print "From" from)
            (print "To" to)
            (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))
                )
            )
    )
  )
  :next-player (fn [self] (if (= player.one self.player) player.two player.one))
  :pieces-placed 0
  :init (fn [self]
    ; 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
    ; NOTE: I think it might be a good idea to make moves
    ;       a list of moves. so that there can be undo and history
    (set self.moves (fcollect [i 1 24] 0))
  )
})
(game:init)


(fn string-upper [s]
  (.. (string.upper (string.sub s 1 1)) (string.sub s 2)))


; Print! That! Board!
(fn print-board [board moves]
  (var index 1)
  (each [_ row (ipairs board)]
    (let [(row-template slots) (string.gsub row "x" "%%d")]
      (if (> slots 0)
        (do
          (let [offset (+ index slots)
                myslice (slice moves index offset)]
            (print (string.format row-template (table.unpack myslice)))
            (set index offset)))
        (print row))))
  (print (.. "Stage: " (string-upper (. (kvflip stages) game.stage))))
  (print (.. "Player " game.player "'s turn:")))


; add the inverse of each valid move
; e.g. 1A = A1
(fn add-reverse-moves []
  (let [reversed (icollect [_ v (ipairs const.spaces)] (string.reverse v))]
    (each [_ v (ipairs reversed)]
      (table.insert const.spaces v)))) ;; oh nooooo i'm mutating a const????
(add-reverse-moves)


; does the move exist within the domain of valid spaces
(fn space-exists? [m] (contains const.spaces (string.upper m)))


; is the space represented by a [A-Za-z0-9] move unoccupied?
(fn space-is-unoccupied? [m]
  (let [unoccupied? 0] ; i.e. is move equal to 0
    (= unoccupied? (. game.moves (index-of-move m)))))


(fn space-is-occupied-by-opponent? [m]
  (let [opponent (if (= game.player 1) 2 1)
        result (= opponent (. game.moves (index-of-move m))) ]
    result))


(fn moving-format? [m]
  (let [from (string.sub m 1 2)
        to (string.sub m -2 -1)]
    (and (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
      (= stages.placing game.stage)
      (or (space-exists? move)
          (print "That space does not exist!\nHint: 1a 1A A1 a1 are all the same move."))
      (or (space-is-unoccupied? move)
          (print "That space is occupied!")))
    (and
      (= 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."))
      )
    (and
      (= stages.moving 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!")) 
      (or (space-is-neighbor? (index-of-move (string.sub move 1 2)) (index-of-move (string.sub move -2 -1)))
          (print "That ain't your neighbor, Johnny")) 
      )
    (and
      ;; TODO: add flying phase
      (= stages.flying game.stage)
      )
    )
  )


; get player input
(fn get-move []
  (io.read))


(fn main []
  ;; game loop
  (while (not (= game.stage stages.complete))
    (print-board const.board game.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))
          (game:update move)
          )
        )
    )
  )
)
(main)