#lang racket (provide (all-defined-out)) ; for testing module (require racket/draw racket/random file/gif) ; a Grid contains: ; -- 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") ; -- the width and height of the grid. (struct grid (lines width height) #:transparent) ; 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 (ignored when the game is over) ; scores is a list of numbers for the score of each player. ; players is a list of two players who are playing in the game (struct GameState (grid player scores players) #:transparent) ; represents a player. func 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. ; name is the player's name. ; func can also be 0 (struct player (name func) #:transparent) ; size of one grid position in pixels (define GRID-SCALE 16) ; Number Number -> Image ; image representing the empty grid (define (grid-dots w h) (let* ([bitmap (make-bitmap (* w GRID-SCALE) (* h GRID-SCALE))] [dc (new bitmap-dc% [bitmap bitmap])]) (send dc set-brush "black" 'solid) (for ([x (in-range w)]) (for ([y (in-range h)]) (send dc draw-ellipse (* x GRID-SCALE) (* y GRID-SCALE) (/ GRID-SCALE 2) (/ GRID-SCALE 2)))) bitmap)) ; Width Height -> Grid ; Generates a 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 (full-grid w h) (grid (flatten (for/list ([x (in-range w)]) (for/list ([y (in-range h)]) (list (if (< x (- w 1)) (line (point x y) (point (+ x 1) y) 1) '()) (if (< y (- h 1)) (line (point x y) (point x (+ y 1)) 1) '()))))) w h)) ; 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. (define (valid-moves g) (filter (lambda (move) (valid-move? move g)) (grid-lines (full-grid (grid-width g) (grid-height g))))) ; 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-box 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))))) (grid-lines 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 Grid -> Bool ; returns #t if a point is out of bounds (off the edge of the grid) (define (out-of-bounds? p g) (or (> 0 (point-x p)) (> 0 (point-y p)) (< (- (grid-width g) 1) (point-x p)) (< (- (grid-height g) 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-lines grid))) ; line doesn't already exist on board (valid-length? line) (forwards? line) (and (not (out-of-bounds? (line-from line) grid)) (not (out-of-bounds? (line-to line) grid))))) ; 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) (grid (append (grid-lines g) (list l)) (grid-width g) (grid-height g)) g)) ; Line -> Bool (define (vertical? l) (= (point-x (line-from l)) (point-x (line-to l)))) ; Line -> Bool (define (horizontal? l) (= (point-y (line-from l)) (point-y (line-to l)))) ; Point -> Bool ; returns if a box is within the boundaries of grid (define (box-in-grid? g b) (not (or (< (point-x b) 0) (< (point-y b) 0) (>= (point-x b) (- (grid-width g) 1)) (>= (point-y b) (- (grid-height g) 1))))) ; Line Grid -> List of Points ; returns the boxes either side of the given line. ; Given a horizontal line from (x1 y) to (x2 y), the two boxes on either side of ; it lie at (x1 (- y 1)) and (x1 y); analogous for a vertical line from (x y1) ; to (x y2). (define (boxes-for line g) (filter (curry box-in-grid? g) (let ([dx (if (horizontal? line) 0 1)] [dy (if (vertical? line) 0 1)] [px (point-x (line-from line))] [py (point-y (line-from line))]) (list (point px py) (point (- px dx) (- py dy)))))) ; Grid Line -> bool or Number ; returns the number of boxes this completes (define (completes-boxes gr l) (let ([boxes (boxes-for l gr)] [g (append-move gr l)]) (length (filter (lambda (b) (= (count-box b g) 4)) boxes)))) ; Grid -> Number ; returns the total amount of moves possible for grid g. ; formula determined using a pen and paper to be: ; (width * (height-1) + (height * (width - 1)) (define (total-moves g) (let ([w (grid-width g)] [h (grid-height g)]) (+ (* w (- h 1)) (* h (- w 1))))) ; GameState -> 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) (let ([g (GameState-grid s)] [p (GameState-player s)]) (cond [(= (length (grid-lines g)) (total-moves g)) s] ; end of the game when every possible line is played [else (let* ([move ((player-func (list-ref (GameState-players s) 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) (GameState-players s))))]))) ; player player Number Number -> GameState ; convenience function for play-game. ; numbers are grid dimensions (define (start-game p1 p2 grid1 grid2) (play-game (GameState (grid '() grid1 grid2) 0 '(0 0) (list p1 p2)))) ; GameState path ! ; saves a game into the .dbn file format that I just made up. ; don't worry, it's sexpy. (define (save-game! s filename) (let ([g (GameState-grid s)] [f (open-output-file filename)]) (pretty-write `((player0 ,(player-name (first (GameState-players s)))) (player1 ,(player-name (second (GameState-players s)))) (grid ,(grid-width g) ,(grid-height g)) (score ,(first (GameState-scores s)) ,(second (GameState-scores s))) (lines ,(for/list ([l (grid-lines g)]) (list (point-x (line-from l)) (point-y (line-from l)) (point-x (line-to l)) (point-y (line-to l)) (line-player l))))) f) (close-output-port f))) ; path -> GameState ; loads a game from a .dbn file into a GameState structure (define (load-game! p) (let* ([f (open-input-file p)] [data (read f)]) (close-input-port f) (GameState (grid (for/list ([i (last (assoc 'lines data))]) (line (point (first i) (second i)) (point (third i) (fourth i)) (fifth i))) (second (assoc 'grid data)) (last (assoc 'grid data))) 0 ; assumed, the game is over (list (second (assoc 'score data)) (last (assoc 'score data))) `( ,(player (last (assoc 'player0 data)) 0) ,(player (last (assoc 'player1 data)) 0))))) ; Grid -> Image ; renders grid to an image for showing humans the game. (define (render-grid-bitmap g) (let* ([bitmap (make-bitmap (* (grid-width g) GRID-SCALE) (* (grid-width g) GRID-SCALE))] ; add border for printing [dc (new bitmap-dc% [bitmap bitmap])]) (render-grid g dc) bitmap)) ; Grid dc ! ; renders grid on the given drawing context. (define (render-grid g dc) (define passed '()) (for ([l (grid-lines g)]) (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 g)]) (for ([b boxes]) (if (= (count-box b (grid passed (grid-width g) (grid-height g))) 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 (grid-dots (grid-width g) (grid-height g)) 0 0)) ; overlay dot grid ; GameState filepath ! ; saves a game's history as images into /tmp/ ; TODO learn file/gif and make the gif properly (define (game->images game) (let* ([g (GameState-grid game)] [frames (grid-frames (grid '() (grid-width g) (grid-height g)) g '())]) (for ([frame (in-range (length frames))]) (send (list-ref frames frame) save-file (format "/tmp/dotsandboxes~a.png" frame) 'png) ))) ; Used internally for rendering all the frames from a grid's history (define (grid-frames g grid-in frames) (cond [(empty? (grid-lines grid-in)) frames] [(grid-frames (grid (append (grid-lines g) (list (car (grid-lines grid-in)))) (grid-width g) (grid-height g)) (grid (cdr (grid-lines grid-in)) (grid-width grid-in) (grid-height grid-in)) (append frames (list (render-grid-bitmap g))))] ))