C’est la troisième et dernière partie du jeu de la vie en Racket.

Trouver le prochain état d’une cellule

Vous avez l’habitude maintenant, je commence par un test très simple.

(check-equal? (next-cell-state '(1 1 1 0 0 0 0 0 0)) 1)

Et une implémentation minimale.

#lang racket

...

(define (next-cell-state neighborhood)
  1)

(provide create-generation
         next-cell-state)

Puis je teste d’autres cas.

(check-equal? (next-cell-state '(1 1 1 0 0 0 0 0 0)) 1)
(check-equal? (next-cell-state '(1 0 0 1 0 0 1 0 0)) 1)

(check-equal? (next-cell-state '(1 0 0 1 1 0 1 0 0)) 1)
(check-equal? (next-cell-state '(1 0 0 1 0 0 1 0 1)) 0)

La fonction for/sum réduit une liste à la somme de ses éléments.

(define (next-cell-state neighborhood)
  (define sum (for/sum ([i neighborhood]) i))
  (if (= 3 sum)
    1
    (list-ref neighborhood 4)))

Je teste les derniers cas.

(check-equal? (next-cell-state '(1 1 1 0 0 0 0 0 0)) 1)
(check-equal? (next-cell-state '(1 0 0 1 0 0 1 0 0)) 1)

(check-equal? (next-cell-state '(1 0 0 1 1 0 1 0 0)) 1)
(check-equal? (next-cell-state '(1 0 0 1 0 0 1 0 1)) 0)

(check-equal? (next-cell-state '(1 1 1 1 1 1 1 1 1)) 0)
(check-equal? (next-cell-state '(0 0 0 0 0 0 0 0 0)) 0)

Comme il y a maintenant trois cas, j’utilise cond au lieu de if.

(define (next-cell-state neighborhood)
  (define sum (for/sum ([i neighborhood]) i))
  (cond [(= 3 sum) 1]
        [(= 4 sum) (list-ref neighborhood 4)]
        [else 0]))

On pourrait aussi utiliser match plutôt que cond:

(define (next-cell-state neighborhood)
  (match (for/sum ([i neighborhood]) i)
         [3 1]
         [4 (list-ref neighborhood 4)]
         [_ 0]))

Je n’ai aucune idée de laquelle est la plus performante, même si je peux imaginer à priori que dans ce cas là c’est cond.

test-case

Je pense qu’il est temps de regrouper les tests en test-case. Rackunit, le framework de test de Racket est assez évolutif.

#lang racket

(require rackunit
         "generation.rkt")

(test-case "create-generation"
  (check-pred list? (create-generation 3 4)
            "It returns a list")

  (check-equal? (length (create-generation 3 4)) 4
              "It builds a list with the right height")

  (check-equal? (length (first (create-generation 3 4))) 3
              "It builds a list with the right width")

  (let ([cell (first (first (create-generation 3 4)))])
  (check-true (or (= cell 0) (= cell 1))
              "It populates generation with 0s or 1s"))

  ((λ ()
   (random-seed 1)
   (check-equal? (create-generation 2 3) '((1 1) (0 1) (1 1))
                 "It populates generation uniformly"))))

(test-case "next-cell-state"
  (check-equal? (next-cell-state '(1 1 1 0 0 0 0 0 0)) 1)
  (check-equal? (next-cell-state '(1 0 0 1 0 0 1 0 0)) 1)

  (check-equal? (next-cell-state '(1 0 0 1 1 0 1 0 0)) 1)
  (check-equal? (next-cell-state '(1 0 0 1 0 0 1 0 1)) 0)

  (check-equal? (next-cell-state '(1 1 1 1 1 1 1 1 1)) 0)
  (check-equal? (next-cell-state '(0 0 0 0 0 0 0 0 0)) 0))

Extraire un voisinage de cellule

Comme toujours je commence par un test simple. On peut noter les arguments nommés de Racket (#:).

(test-case "extract-neighborhood"
  (let ([game '((1 0 1 0)
                (0 1 0 1)
                (1 0 0 1))])
    (check-equal? (extract-neighborhood game #:x 1 #:y 1) '(1 0 1 0 1 0 1 0 0))))

Et une implémentation encore plus simple.

(define (extract-neighborhood generation #:x [x 0] #:y [y 0])
  '(1 0 1 0 1 0 1 0 0))

La suite est classique, j’ajoute un nouveau test.

(test-case "extract-neighborhood"
  (let ([game '((1 0 1 0)
                (0 1 0 1)
                (1 0 0 1))])
    (check-equal? (extract-neighborhood game #:x 1 #:y 1) '(1 0 1 0 1 0 1 0 0))
    (check-equal? (extract-neighborhood game #:x 2 #:y 1) '(0 1 0 1 0 1 0 0 1))))

Je regarde ce test échouer.

$ racket game-of-life-test.rkt 
--------------------
extract-neighborhood
FAILURE
actual:     (1 0 1 0 1 0 1 0 0)
expected:   (0 1 0 1 0 1 0 0 1)

Et j’implémente le minimum de code pour faire passer ce nouveau test. Je vous épargne ça dans l’article, si vous êtes curieux vous pouvez trouver le code sur Github.

Une nouvelle génération

J’écris un test pour la production d’une nouvelle génération.

(test-case "next-generation"
  (let ([game '((1 0 1 0)
                (0 1 0 1)
                (1 0 0 1))])

    (check-equal? (next-generation game) '((0 1 1 0) (1 1 0 1) (0 0 1 0)))))

Et voici le code qui fait passer ce test.

(define (next-generation current)
  (for/list ([y (length current)])
    (for/list ([x (length (first current))])
      (define neighborhood (extract-neighborhood current #:x x #:y y))
      (next-cell-state neighborhood))))

On peut maintenant lancer le jeu de la vie.

#lang racket

(require "generation.rkt"
         "window.rkt")

(define size 100)
(define generation (create-generation size size))
(define canvas (create-window size size generation))

(define (loop n g)
  (send canvas change-generation g)
  (sleep 0.2)
  (when (> n 0)
    (loop (sub1 n) (next-generation g))))

(loop 30 generation)

Mise à l’échelle

Pour rendre les choses un peu plus intéressantes visuellement, on va faire un zoom x4.

#lang racket/gui

(define (create-window w h g)
  (define scale 4)

  (define frame (new frame%
                     [label "Game of Life"]
                     [width (* w scale)]
                     [height (* h scale)]))

  (define canvas (new (class canvas%

    ...

         (define/override (on-paint)
           (send dc set-brush (new brush% [color "black"]))
           (send dc draw-rectangle 0 0 (* w scale) (* h scale))
           (send dc set-brush (new brush% [color "white"]))
           (for ([y (length current-generation)])
             (for ([x (length (first current-generation))])
               (when (= 1 (list-ref (list-ref current-generation y) x))
                 (send dc draw-rectangle (* x scale) (* y scale) scale scale))))))))
  ...

Une surface de jeu sans bordures

Il reste à retirer les bordures du jeu. Le processus est exactement le même que pour les versions Javascript et Ruby et je n’ai pas envie de réécrire les mêmes phrases. Au besoin, je vous rappelle que le code complet du jeu de la vie en Racket se trouve sur Github.