1#lang racket
2
3  (require "board.rkt")
4
5  ;; a move is either:
6  ;;  - (make-enter-piece pawn)
7  ;;  - (make-move-piece-main pawn start distance)
8  ;;  - (make-move-piece-home pawn start distance)
9  (define-struct move () #:inspector (make-inspector))
10  (define-struct (enter-piece move) (pawn) #:inspector (make-inspector))
11  (define-struct (move-piece-main move) (pawn start distance) #:inspector (make-inspector))
12  (define-struct (move-piece-home move) (pawn start distance) #:inspector (make-inspector))
13
14  (provide/contract
15   (struct enter-piece ((pawn pawn?)))
16   (struct move-piece-main ([pawn pawn?] [start number?] [distance number?]))
17   (struct move-piece-home ([pawn pawn?] [start number?] [distance number?])))
18
19  (provide take-turn
20           bad-move
21           make-moves
22           move?
23
24           board-enter-piece
25           board-move-piece-main
26           board-move-piece-home
27
28           blockade-moved?
29           find-end-spot
30           board-doubles-penalty
31
32           make-one-move
33
34           get-move-id
35           get-move-color
36
37           has-entering-roll?
38           entering-blockade?
39           exn:bad-move?
40           exn:bad-move-with-info?
41           exn:bad-move-with-info-color
42           exn:bad-move-with-info-board
43           exn:bad-move-with-info-dice
44           exn:bad-move-with-info-moves
45
46           board-all-in?
47           <=/m
48           possible-to-move)
49
50  (define bop-bonus 20)
51  (define home-bonus 10)
52
53  ;; moves-dice : moves -> (listof number)
54  ;; does not return the die moves that correspond to entering pawns
55  (define (moves-dice moves)
56    (let loop ([moves moves]
57               [dice null])
58      (cond
59        [(null? moves) dice]
60        [else
61         (let ([move (car moves)])
62           (cond
63             [(move-piece-main? move)
64              (loop (cdr moves) (cons (move-piece-main-distance move) dice))]
65             [(move-piece-home? move)
66              (loop (cdr moves) (cons (move-piece-home-distance move) dice))]
67             [else (loop (cdr moves) dice)]))])))
68
69  ;; board-doubles-penalty : board color -> board
70  (define (board-doubles-penalty board color)
71    (let home-row-loop ([i board-home-row-size])
72      (cond
73        [(zero? i)
74         (let main-board-loop ([i (get-enter-pos color)]
75                               [first-time? #t])
76           (cond
77             [(and (not first-time?) (= i (get-enter-pos color)))
78              board]
79             [else
80              (let* ([next-i (modulo (- i 1) board-main-size)]
81                     [ent (board-main-i board next-i)])
82                (if (and (pair? ent)
83                         (eq? (pawn-color (car ent)) color))
84                    (move-piece board (car ent) 'start)
85                    (main-board-loop next-i #f)))]))]
86        [else
87         (let ([ent (board-home-row-i board color (- i 1))])
88           (if (null? ent)
89               (home-row-loop (- i 1))
90               (move-piece board (car ent) 'start)))])))
91
92  ;; take-turn : color board (listof number) (listof move) -> board
93  ;; raises an exception if the turn is illegal
94  (define (take-turn color original-board original-dice original-moves)
95    (parameterize ([current-color/board/dice/moves
96                    (list color original-board original-dice original-moves)])
97      (unless (andmap (lambda (x) (eq? color (get-move-color x))) original-moves)
98        (bad-move "attempted to move two different colors"))
99      (let loop ([moves original-moves]
100                 [board original-board]
101                 [dice original-dice])
102        (cond
103          [(null? moves)
104
105           (when (and (has-entering-roll? dice)
106                      (memf (lambda (pawn) (eq? (pawn-color pawn) color))
107                            (board-start board)) ;; has pieces in start
108                      (not (entering-blockade? board color)))
109             (bad-move "can still enter a pawn"))
110
111           (let ([used-dice (moves-dice original-moves)])
112             (for-each (lambda (die)
113                         (let ([potential-board (possible-to-move color board die)])
114                           (when potential-board
115                             (unless (blockade-moved? original-board potential-board color)
116                               (bad-move "die roll ~a can still be used" die)))))
117                       dice)
118
119             (when (blockade-moved? original-board board color)
120               (bad-move "cannot move blockade together")))
121
122           board]
123          [else
124           (let ([move (car moves)])
125             (let-values ([(new-board bonus new-dice)
126                           (make-move/dice board move dice)])
127               (let ([new-new-dice (if bonus
128                                       (cons bonus new-dice)
129                                       new-dice)])
130                 (loop (cdr moves)
131                       new-board
132                       new-new-dice))))]))))
133
134  ;; get-move-color : move -> symbol
135  ;; extracts the moved color from the move
136  (define (get-move-color move) (pawn-color (get-move-pawn move)))
137
138  ;; get-move-id : move -> number
139  (define (get-move-id move) (pawn-id (get-move-pawn move)))
140
141  (define (get-move-pawn move)
142    (cond
143      [(enter-piece? move) (enter-piece-pawn move)]
144      [(move-piece-main? move) (move-piece-main-pawn move)]
145      [(move-piece-home? move) (move-piece-home-pawn move)]))
146
147  ;; blocakde-moved? : board board color -> boolean
148  (define (blockade-moved? original-board new-board color)
149    (let ([original-blockades (find-blockades/color original-board color)]
150          [new-blockades (find-blockades/color new-board color)])
151      (ormap (lambda (new-blockade) (memf (same-blockade-different-place? new-blockade) original-blockades))
152             new-blockades)))
153
154  (define ((same-blockade-different-place? b1) b2)
155    (and (equal? (blockade-p1 b1) (blockade-p1 b2))
156         (equal? (blockade-p2 b1) (blockade-p2 b2))
157         (not (equal? (blockade-loc b1) (blockade-loc b2)))))
158
159  ;; make-move/dice : board move (listof number) number -> (values board bonus (listof number))
160  ;; makes the given move, removing the used dice from the dice list.
161  ;; raises an error if the move isn't legal.
162  ;; check for: using a five to move when there are pieces to come in
163  ;;            moving without the matching roll
164  (define (make-move/dice board move dice)
165    (cond
166      [(enter-piece? move)
167       (let ([new-dice (cond
168                         [(memq 5 dice) (remq 5 dice)]
169                         [(and (memq 1 dice) (memq 4 dice))
170                          (remq 1 (remq 4 dice))]
171                         [(and (memq 2 dice) (memq 3 dice))
172                          (remq 2 (remq 3 dice))]
173                         [else (bad-move "entered without having a 5")])])
174         (let-values ([(board bonus) (board-enter-piece board (enter-piece-pawn move))])
175           (values board bonus new-dice)))]
176      [(move-piece-main? move)
177       (do-move/dice/moving board dice
178                            (move-piece-main-distance move)
179                            (move-piece-main-pawn move)
180                            (move-piece-main-start move)
181                            board-move-piece-main)]
182      [(move-piece-home? move)
183       (do-move/dice/moving board dice
184                            (move-piece-home-distance move)
185                            (move-piece-home-pawn move)
186                            (move-piece-home-start move)
187                            board-move-piece-home)]))
188
189  ;; helper function to collapse last two cases of make-move/dice
190  (define (do-move/dice/moving board dice die pawn start board-move-piece)
191    (let ([new-dice (remq die dice)])
192      (unless (memq die dice)
193        (bad-move "tried to move ~a squares but dice read ~a" die dice))
194      (let-values ([(new-board bonus)
195                    (board-move-piece board pawn start die)])
196        (values new-board bonus new-dice))))
197
198  ;; entering-blocade? : board symbol -> boolean
199  (define (entering-blockade? board color)
200    (let ([ent (board-main-i board (get-enter-pos color))])
201      (and (pair? ent) (pair? (cdr ent)))))
202
203  (define (no-blockades board start end)
204    (let ([ind (find-blockade/between board start end)])
205      (cond
206        [(not ind) (void)]
207        [(number? ind)
208         (bad-move "there is a blockade at ~a in the main ring" ind)]
209        [(home-row-loc? ind)
210         (bad-move "there is a blockade at ~a in the ~a home row"
211                   (home-row-loc-num ind)
212                   (home-row-loc-color ind))]
213        [else (bad-move "blockade in the way")])))
214
215  ;; has-entering-roll? : (listof number) -> boolean
216  (define (has-entering-roll? dice)
217    (or (memq 5 dice)
218        (and (memq 1 dice) (memq 4 dice))
219        (and (memq 2 dice) (memq 3 dice))))
220
221  ;; possible-to-move : symbol board number -> (union #f board)
222  ;; indicates if there are any moves that could happen with the
223  ;; given die, for the given color in the given board.
224  ;; doesn't consider entering moves
225  (define (possible-to-move color board die)
226    (let/ec k
227      (for-each-pawn/loc
228       board
229       (lambda (pawn loc)
230         (when (and (eq? color (pawn-color pawn))
231                    (not (symbol? loc)))
232           (with-handlers ([exn:bad-move? (lambda (x) #f)])
233             (cond
234               [(number? loc)
235                (let-values ([(board bonus) (board-move-piece-main board pawn loc die)])
236                  (k board))]
237               [(home-row-loc? loc)
238                (let-values ([(board bonus) (board-move-piece-home board pawn (home-row-loc-num loc) die)])
239                  (k board))])))))
240      #f))
241
242  ;; make-moves : board (listof move) -> board (listof number)
243  ;; only checks that each move, in isloation, would be possible
244  (define (make-moves board moves)
245    (let loop ([board board]
246               [bonus '()]
247               [moves moves])
248      (cond
249        [(null? moves) (values board bonus)]
250        [else
251         (let-values ([(new-board new-bonus) (make-one-move board (car moves))])
252           (loop new-board
253                 (if new-bonus (cons new-bonus bonus) bonus)
254                 (cdr moves)))])))
255
256  ;; make-one-move : board move -> board
257  (define (make-one-move board move)
258    (cond
259      [(enter-piece? move) (board-enter-piece board (enter-piece-pawn move))]
260      [(move-piece-main? move) (board-move-piece-main board
261                                                      (move-piece-main-pawn move)
262                                                      (move-piece-main-start move)
263                                                      (move-piece-main-distance move))]
264      [(move-piece-home? move) (board-move-piece-home board
265                                                      (move-piece-home-pawn move)
266                                                      (move-piece-home-start move)
267                                                      (move-piece-home-distance move))]))
268
269
270
271  (define (board-all-in? board color)
272    (not (memf (lambda (pawn) (eq? (pawn-color pawn) color))
273               (board-start board))))
274
275  ;; enter-piece : board pawn -> (values board (union #f number))
276  (define (board-enter-piece orig-board pawn)
277    (unless (member pawn (board-start orig-board))
278      (bad-move "~a's pawn ~a is already on the board" (pawn-color pawn) (pawn-id pawn)))
279    ;; move the color out of the starting area
280    (let* ([pos (get-enter-pos (pawn-color pawn))]
281           [old-ent (board-main-i orig-board pos)])
282      (when (= 2 (length old-ent))
283        (bad-move "cannot move out into a blockade"))
284      (cond
285        ;; no bop
286        [(or (null? old-ent)
287             (eq? (pawn-color (car old-ent)) (pawn-color pawn)))
288         (values (move-piece orig-board pawn pos)
289                 #f)]
290        ;; bop
291        [else
292         (values (move-piece2
293                  orig-board
294                  pawn
295                  pos
296                  (car old-ent)
297                  'start)
298                 bop-bonus)])))
299
300  ;; board-move-piece-home : board pawn number number -> (values board (union #f number))
301  ;; result of #f indicates no bop; result of a color indicates who got bopped
302  (define (board-move-piece-home board pawn start distance)
303    (let* ([color (pawn-color pawn)]
304           [old (board-home-row-i board color start)])
305      (unless (member pawn old)
306        (bad-move "color ~a is not in the home row on ~a" (pawn-color pawn) start))
307      (unless (and (<= 0 start) (< start board-home-row-size))
308        (error 'boad-move-piece-home "bad start argument ~e" start))
309      (unless (<= 0 start (+ start distance) (+ board-home-row-size 1))
310        (bad-move "moved too far, off the end of the board"))
311
312      (let ([finish (+ start distance)])
313        (cond
314          [(= finish board-home-row-size)
315           (when (< start (- finish 1))
316             ;; if only moving one square, then we don't need to check blockades
317             ;; this lets us satisfy the inputs to no-blockades
318             (no-blockades board
319                           (make-home-row-loc (+ start 1) color)
320                           (make-home-row-loc (- finish 1) color)))
321           (values (move-piece board pawn 'home)
322                   home-bonus)]
323          [(< finish board-home-row-size)
324           (no-blockades board
325                         (make-home-row-loc (+ start 1) color)
326                         (make-home-row-loc finish color))
327
328           (let ([old-ent (board-home-row-i board color finish)])
329             (cond
330               [(or (null? old-ent)
331                    (null? (cdr old-ent)))
332                (values (move-piece board
333                                    pawn
334                                    (make-home-row-loc finish color))
335                        #f)]
336               [else
337                (bad-move "moved onto a blockade in the home row")]))]
338          [else
339           (bad-move "moved off of the end of the board")]))))
340
341  ;; board-move-piece-main : board pawn number number -> (values board (union #f number))
342  ;; result of #f indicates no bop; result of a color indicates who got bopped
343  (define (board-move-piece-main board pawn start distance)
344    (unless (member pawn (board-main-i board start))
345      (bad-move "color ~a (piece #~a) is not on square ~a"
346                (pawn-color pawn)
347                (pawn-id pawn)
348                start))
349    (let* ([color (pawn-color pawn)]
350           [landed (find-end-spot color start distance)]
351           [exit (get-exit-pos color)])
352      (cond
353        [(eq? landed 'too-far) (bad-move "moved off of the board")]
354        [(eq? landed 'home)
355         (no-blockades board
356                       (modulo (+ start 1) board-main-size)
357                       (make-home-row-loc (- board-home-row-size 1) color))
358         (values (move-piece board
359                             pawn
360                             'home)
361                 10)]
362        [(eq? (car landed) 'home-row)
363         ;; turned onto the exit ramp
364
365         (let* ([final-spot (cdr landed)])
366           (no-blockades board
367                         (next-pos color start)
368                         (make-home-row-loc final-spot color))
369
370           (let ([old (board-home-row-i board color final-spot)])
371             (when (and (pair? old)
372                        (pair? (cdr old)))
373               (bad-move "cannot move onto a blockade"))
374             (values (move-piece board pawn (make-home-row-loc final-spot color))
375                     #f)))]
376        [else
377         ;; stayed on the main board
378         (let ([end (cdr landed)])
379           (let ([start+1 (modulo (+ start 1) board-main-size)])
380             (unless (= start+1 end)
381               (no-blockades board start+1 end)))
382           (let ([old-contents (board-main-i board end)])
383
384             (cond
385               ;; no one there
386               [(null? old-contents)
387                (values (move-piece board pawn end)
388                        #f)]
389
390               [(and (pair? old-contents)
391                     (pair? (cdr old-contents)))
392                (bad-move "cannot move directly onto a blockade")]
393
394               ;; already one of the same color on that spot
395               [(eq? (pawn-color (car old-contents)) color)
396                (values (move-piece board
397                                    pawn
398                                    end)
399                        #f)]
400
401               ;; attempt to bop on a safety -- illegal
402               [(safety? end)
403                (bad-move "cannot move onto a safety if someone else is already there")]
404
405               ;; successful bop
406               [else
407                (values
408                 (move-piece2 board
409                              pawn
410                              end
411                              (car old-contents)
412                              'start)
413                 bop-bonus)])))])))
414
415  ;; next-pos : color number -> (union number home-row-loc)
416  ;; given a position on the main ring, it finds the next position
417  ;; for the given color on the board.
418  (define (next-pos color pos)
419    (cond
420      [(= pos (get-exit-pos color))
421       (make-home-row-loc 0 color)]
422      [else
423       (modulo (+ pos 1) board-main-size)]))
424
425  ;; find-end-spot : color number number -> (union 'too-far 'home (cons 'home-row number) (cons 'main number)))
426  (define (find-end-spot color start distance)
427    (let ([exit (get-exit-pos color)]
428          [end (modulo (+ start distance) board-main-size)])
429      (cond
430        [(and (<=/m start exit end)
431              (not (= exit end)))
432         (let* ([distance-to-exit (modulo (- exit start) board-main-size)]
433                [final-spot (- distance distance-to-exit 1)])
434           (cond
435             [(final-spot . = . board-home-row-size)
436              'home]
437             [(final-spot . < . board-home-row-size)
438              (cons 'home-row final-spot)]
439             [else
440              'too-far]))]
441        [else
442         (cons 'main end)])))
443
444
445  (define (<=/m one two three)
446    (or (<= one two three)
447        (<= two three one)
448        (<= three one two)))
449
450  (define-struct (exn:bad-move exn) ())
451  (define-struct (exn:bad-move-with-info exn:bad-move) (color board dice moves))
452
453  (define current-color/board/dice/moves (make-parameter #f))
454
455  (define (bad-move _str . args)
456    (define str (if (null? args) _str (apply format _str args)))
457    (raise
458     (cond
459       [(current-color/board/dice/moves)
460        (define-values (color board dice moves) (apply values (current-color/board/dice/moves)))
461        (make-exn:bad-move-with-info str (current-continuation-marks) color board dice moves)]
462       [else
463        (make-exn:bad-move str (current-continuation-marks))])))
464