Hello.
I was planning to program Conway's Game of Life, life.rkt
, in the functional way.
#!/usr/bin/env racket
#lang racket
(require (only-in srfi/41 stream-for-each stream-from)
(only-in srfi/43 vector-fold vector-map vector-unfold))
(provide initial-board dead-or-alive? evolve stream-boards)
;; Vertical
(define *N* 22)
;; Horizontal
(define *M* 78)
;; The Board for Life (implemented as One-Dimensional array as Peter Norvig suggested in his book, PAIP)
;;; The initial State
(define *init* `(,(+ (/ *M* 2) (* *M* (/ *N* 2)))
,(+ (/ *M* 2) (* *M* (sub1 (/ *N* 2))))
,(+ (/ *M* 2) (* *M* (add1 (/ *N* 2))))
,(+ (sub1 (/ *M* 2)) (* *M* (/ *N* 2)))
,(+ (add1 (/ *M* 2)) (* *M* (sub1 (/ *N* 2))))))
(define (initial-board m n init)
(vector-unfold (lambda (i x)
(values (and (!null? x)
(= i (car x)))
(if (or (null? x)
(!=? i (car x)))
x
(cdr x)))) (* m n) (sort init <)))
;; One-Dimensional Array -> A Formatted String in 2D
(define (format-board m board)
(let ((table (hasheq #t "♥" #f "‧")))
(vector-fold (lambda (index x str)
(format (if (zero? (modulo (add1 index) m))
"~a~a\n"
"~a~a")
x (hash-ref table str)))
"" board)))
;; The Rule
;; https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life#Rules
(define (dead-or-alive? m board)
(lambda (index x)
(let* ((len (vector-length board))
(table (hasheq #t 1 #f 0))
(nth (lambda (i) (vector-ref board (modulo i len))))
(count (apply +
(map (lambda (x)
(hash-ref table (nth x)))
(let ((i-m (- index m)) (i+m (+ index m)))
`(,(sub1 i-m) ,i-m ,(add1 i-m)
,(sub1 index) ,(add1 index)
,(sub1 i+m) ,i+m ,(add1 i+m)))))))
(or (and x (< 1 count 4))
(= count 3)))))
;; Generate the next generation
(define (evolve m board)
(vector-map (dead-or-alive? m board) board))
;; Board -> Stream
;; https://mitp-content-server.mit.edu/books/content/sectbyfn/books_pres_0/6515/sicp.zip/full-text/book/book-Z-H-24.html#%_sec_3.5.2
(define (stream-boards m n init)
(define s (stream-cons (initial-board m n init)
(stream-map (lambda (board)
(evolve m board))
s)))
s)
;; Print the stream of Life on the terminal
(define (print-boards m boards)
(stream-for-each (lambda (i board)
(system (if (eq? (system-type) 'windows)
"cls"
"clear"))
(display (format "Generation ~a\n~a" i
(format-board m board)))
(sleep 1/7))
(stream-from 1) boards))
;; Utilities
(define !null? (compose1 not null?))
(define !=? (compose1 not =))
;; main
(define (main)
(print-boards *M* (stream-boards *M* *N* *init*)))
(main)
This program is an infinite loop, because it uses an infinite stream, so we have to quit by pressing Ctrl-C; however the problem is more physical.
I mean it hurts our eyes.
Usually, on UNIX, this kind of program uses curses
library to control the terminal display. I should use charterm
library, but I have heard the library is especially for UNIX. In other words, it does not work on Windows; thus I instead use the OS command, clear
on UNIX and cls
on Windows, for the portability, I am a Linux user, though.
However, as you know, printing something on the display and refreshing it takes a rather time than that of computing itself. That is the reason why our eyes are twitching. Dry eyes. Need eye drops?
Now, we come up with the idea that it must be better to make an animation with some graphical tools. I am not familiar with Racket GUI; consequently, as cutting corners, I decided to use Racket's plot
, following this blog post.
#lang racket/gui
(require plot plot-container
(only-in srfi/43 vector-fold)
"life.rkt")
;; Vertical
(define *N* 100)
;; Horizontal
(define *M* 100)
;;; Initial State
(define *init* `(,(+ (/ *M* 2) (* *M* (/ *N* 2)))
,(+ (/ *M* 2) (* *M* (sub1 (/ *N* 2))))
,(+ (/ *M* 2) (* *M* (add1 (/ *N* 2))))
,(+ (sub1 (/ *M* 2)) (* *M* (/ *N* 2)))
,(+ (add1 (/ *M* 2)) (* *M* (sub1 (/ *N* 2))))))
(define *s* (stream-boards *M* *N* *init*))
(plot-x-label #f)
(plot-y-label #f)
(define toplevel (new frame% (label "Conway's Game of Life")
(height (* 3 *M*))
(width (* 3 *N*))))
(define container (new plot-container% (parent toplevel)))
(define snip (plot-snip '() #:x-min 0 #:x-max *M* #:y-min 0 #:y-max *N*))
(send container set-snip snip)
(send toplevel show #t)
(define (board->points m board)
(vector-fold (lambda (index lst elm)
(if elm
(cons `(,(modulo index m) ,(quotient index m)) lst)
lst))
'() board))
(define (make-renderers board)
`(,(points (board->points *M* board)
#:x-min 0
#:x-max *M*
#:y-min 0
#:y-max *N*
#:color 'red
#:size 1)))
(stream-for-each (lambda (x)
(send snip set-overlay-renderers (make-renderers x))
(collect-garbage 'incremental)
(sleep/yield (/ 1 7))) *s*)
This is a sort of a test suit, which includes a bug, displaying the animation upside down. The CLI version starts a point from the upper corner left, but plot
starts a point from the downside left, in the mathematical graph manner.
But this is O.K., because it is easy to write a reflection code, according to linear map.
The serious problem here is that we cannot quit the CLI program running at the background, which is an infinite loop, even though we click the window close button and close the window. Oops!
I have to write a code for the window close button, in order to tell the CLI one killed, but how?
I checked out the Racket documentation, but found no keyword, nor close button
stuff.
I have the idea of such a framework as:
(call/cc
(lambda (break)
(stream-for-each (lambda (x)
(when window-close-button-clicked-p ; how?
break)
(send snip set-overlay-renderers (make-renderers x))
(collect-garbage 'incremental)
(sleep/yield (/ 1 7))) *s*)))
But I do not know how to write window-close-button-clicked-p
.
Any ideas?
And please do not say "Do not use streams".
I love it.
Thanks.