210 lines
8.3 KiB
Racket
210 lines
8.3 KiB
Racket
#lang racket
|
|
|
|
(provide (all-defined-out)) ; for testing module
|
|
|
|
(require racket/draw racket/random)
|
|
|
|
; a Grid is a list of lines. All lines in a grid must be specified as moving left to right, top to bottom ("to" coordinate higher than "from" coordinate")
|
|
|
|
; represents a point in 2d space.
|
|
(struct point (x y) #:transparent)
|
|
|
|
; represents a line drawn from one point to another and which player drew it.
|
|
(struct line (from to player) #:transparent)
|
|
|
|
; represents the state of a game in play
|
|
; grid is the grid
|
|
; player is the current player
|
|
; scores is a list of numbers for the score of each player.
|
|
(struct GameState (grid player scores))
|
|
|
|
; width and height of the playing grid.
|
|
(define GRID-WIDTH 6)
|
|
(define GRID-HEIGHT 6)
|
|
|
|
; size of one grid position in pixels
|
|
(define GRID-SCALE 16)
|
|
|
|
; image representing the empty grid
|
|
(define EMPTY-GRID
|
|
(let* ([bitmap (make-bitmap (* GRID-WIDTH GRID-SCALE) (* GRID-HEIGHT GRID-SCALE))]
|
|
[dc (new bitmap-dc% [bitmap bitmap])])
|
|
(send dc set-brush "black" 'solid)
|
|
(for ([x (in-range GRID-WIDTH)])
|
|
(for ([y (in-range GRID-HEIGHT)])
|
|
(send dc draw-ellipse (* x GRID-SCALE) (* y GRID-SCALE) (/ GRID-SCALE 2) (/ GRID-SCALE 2))))
|
|
bitmap))
|
|
|
|
; Grid that contains all possible lines filled. Used for filtering valid moves. This is an ugly way, but i can't think of a better one right now.
|
|
(define ALL-LINES (flatten
|
|
(for/list ([x (in-range GRID-WIDTH)])
|
|
(for/list ([y (in-range GRID-HEIGHT)])
|
|
(list
|
|
(if (< x (- GRID-WIDTH 1)) (line (point x y) (point (+ x 1) y) 1) '())
|
|
(if (< y (- GRID-HEIGHT 1)) (line (point x y) (point x (+ y 1)) 1) '()))))))
|
|
|
|
; Line Line -> Bool
|
|
; tests if two lines are in the same position
|
|
(define (same-position? l1 l2)
|
|
(or
|
|
(and
|
|
(equal? (line-from l1) (line-from l2))
|
|
(equal? (line-to l1) (line-to l2)))
|
|
(and
|
|
(equal? (line-to l1) (line-from l2))
|
|
(equal? (line-from l1) (line-to l2)))))
|
|
|
|
; Point Point -> Point
|
|
; adds two points together.
|
|
(define (point+ p1 p2)
|
|
(point (+ (point-x p1) (point-x p2)) (+ (point-y p1) (point-y p2))))
|
|
|
|
; Grid -> List of Lines
|
|
; returns all valid moves on a grid.
|
|
; TODO optimise. This "filtering what is invalid out of all possible lines" method sucks.
|
|
; TODO write more tests
|
|
(define (valid-moves g)
|
|
(filter (lambda (move) (valid-move? move g)) ALL-LINES))
|
|
|
|
; Point Grid -> Number
|
|
; given a point that is the top-left corner of a square on the grid, returns the amount of edges surrounding that square.
|
|
(define (count-square p g)
|
|
; explanation:
|
|
; for every item in the grid (g), test it against every edge of the box for equality. if any are equal, it matches.
|
|
; then only those that match are kept and this list is checked for length to determine the matching lines.
|
|
(length
|
|
(filter (lambda (item)
|
|
(member #t
|
|
(for/list ([x (list (point 0 0) (point 0 1) (point 1 0) (point 0 0))]
|
|
[y (list (point 0 1) (point 1 1) (point 1 1) (point 1 0))])
|
|
(same-position? item (line (point+ x p) (point+ y p) 0))))) g))) ; for every position in the square, check if there's a line there
|
|
|
|
; Line -> Bool
|
|
; returns #t if the line is forwards (with forwards being moving right and down, higher end coord than start)
|
|
(define (forwards? line)
|
|
(and
|
|
(>= (point-x (line-to line)) (point-x (line-from line)))
|
|
(>= (point-y (line-to line)) (point-y (line-from line)))))
|
|
|
|
; Line -> Bool
|
|
; returns #t if a line is valid in terms of length (only moving one point in one direction)
|
|
(define (valid-length? line)
|
|
(= 1
|
|
(+ (abs (- (point-x (line-from line)) (point-x (line-to line))))
|
|
(abs (- (point-y (line-from line)) (point-y (line-to line)))))))
|
|
|
|
; Point -> Bool
|
|
; returns #t if a point is out of bounds (off the edge of the grid)
|
|
(define (out-of-bounds? p)
|
|
(or
|
|
(> 0 (point-x p))
|
|
(> 0 (point-y p))
|
|
(< (- GRID-WIDTH 1) (point-x p))
|
|
(< (- GRID-WIDTH 1) (point-y p))))
|
|
|
|
; Line Grid -> Bool
|
|
; returns #t if adding the given move to the grid is valid.
|
|
(define (valid-move? line grid)
|
|
(and
|
|
(empty? (filter (lambda (l) (same-position? l line)) grid)) ; line doesn't already exist on board
|
|
(valid-length? line)
|
|
(forwards? line)
|
|
(and
|
|
(not (out-of-bounds? (line-from line)))
|
|
(not (out-of-bounds? (line-to line))))))
|
|
|
|
; a Player is a function with the signature:
|
|
; Grid Number -> Line
|
|
; where the grid is the current board state, the Number is if the player is player 1 or 2, and it returns the move it will play that turn.
|
|
|
|
; a Player that plays random moves
|
|
(define (random-player g n)
|
|
(let ([m (car (random-sample (valid-moves g) 1))])
|
|
(line (line-from m) (line-to m) n)))
|
|
|
|
; Grid Line -> Grid
|
|
; adds line to grid, if it is a valid move. Otherwise skips the move.
|
|
(define (append-move g l)
|
|
(if (valid-move? l g) (append g (list l)) g))
|
|
|
|
; Line -> List of Points
|
|
; returns the positions of boxes the given line affects, assuming a valid line.
|
|
; TODO this is messy as fuck, especially with that (filter). Racket must have some way to do this more nicely.
|
|
(define (boxes-for line)
|
|
(filter (lambda (x) (not (eq? x 0)))
|
|
(list
|
|
(if (and
|
|
(not (= (- GRID-WIDTH 1) (point-x (line-from line))))
|
|
(not (= (- GRID-HEIGHT 1) (point-y (line-from line))))) (line-from line) 0)
|
|
(if (and
|
|
(< (point-x (line-from line)) (point-x (line-to line))) ; horizontal line
|
|
(> (point-y (line-from line)) 0))
|
|
(point (point-x (line-from line)) (- (point-y (line-from line)) 1)) 0)
|
|
(if (and
|
|
(< (point-y (line-from line)) (point-y (line-to line))) ; vertical line
|
|
(> (point-x (line-from line)) 0))
|
|
(point (- (point-x (line-from line)) 1) (point-y (line-from line))) 0))))
|
|
|
|
; Grid Line -> bool or Number
|
|
; returns the number of boxes this completes
|
|
(define (completes-boxes grid l)
|
|
(let
|
|
([boxes (boxes-for l)]
|
|
[g (append-move grid l)])
|
|
(length (filter (lambda (b) (= (count-square b g) 4)) boxes))))
|
|
|
|
; GameState, List of 2 Players -> GameState
|
|
; runs the game.
|
|
; If the game is over, returns the final game state
|
|
; else, lets a player play a move. If they complete a box, they stay on and earn a point. Else, the other player has a turn.
|
|
(define (play-game s players)
|
|
(let ([g (GameState-grid s)]
|
|
[p (GameState-player s)])
|
|
(cond
|
|
[(= (length g) (length ALL-LINES)) s] ; end of the game
|
|
[else
|
|
(let* ([move ((list-ref players p) g p)]
|
|
[new-grid (append-move g move)]
|
|
[boxes (completes-boxes g move)]
|
|
[box (not (= 0 boxes))]
|
|
[scores (GameState-scores s)])
|
|
(play-game (GameState
|
|
new-grid
|
|
(if box p (abs (- p 1)))
|
|
(if box (list-set scores p (+ boxes (list-ref scores p))) scores)) players))])))
|
|
|
|
; Grid -> Image
|
|
; renders grid to an image for showing humans the game.
|
|
(define (render-grid-bitmap grid)
|
|
(let* ([bitmap (make-bitmap (+ 10 (* GRID-WIDTH GRID-SCALE)) (+ 10 (* GRID-HEIGHT GRID-SCALE)))] ; add border for printing
|
|
[dc (new bitmap-dc% [bitmap bitmap])])
|
|
(render-grid grid dc)
|
|
bitmap))
|
|
|
|
; Grid dc !
|
|
; renders grid on the given drawing context.
|
|
; TODO setting line color doesn't seem to be always working?
|
|
; TODO color in squares
|
|
(define (render-grid grid dc)
|
|
(define passed '())
|
|
(for ([l grid]) ; reversed for fill-drawing malarkey.
|
|
(if (empty? passed) (set! passed (list l)) (set! passed (append passed (list l))))
|
|
(cond
|
|
[(= (line-player l) 0) (send dc set-pen "red" 4 'solid)] ; player 1 draws in red
|
|
[(= (line-player l) 1) (send dc set-pen "blue" 4 'solid)]) ; player 2 draws in blue
|
|
(send dc draw-line
|
|
(+ 4 (* GRID-SCALE (point-x (line-from l))))
|
|
(+ 4 (* GRID-SCALE (point-y (line-from l))))
|
|
(+ 4 (* GRID-SCALE (point-x (line-to l))))
|
|
(+ 4 (* GRID-SCALE (point-y (line-to l))))) ; draw the line
|
|
(let ([boxes (boxes-for l)])
|
|
(for ([b boxes])
|
|
(if (= (count-square b passed) 4)
|
|
(send dc draw-text
|
|
(if (= (line-player l) 0) "R" "B")
|
|
(+ 6 (* GRID-SCALE (point-x b)))
|
|
(+ 3 (* GRID-SCALE (point-y b))))
|
|
0))))
|
|
(send dc draw-bitmap EMPTY-GRID 0 0)) ; overlay dot grid
|
|
|