I'm implementing Conway's Game of Life in Lisp to familiarize myself with the language, with the eventual goal of hosting it on a website with a pretty GUI. I'm a hobbyist and inexperienced (this is my first project where there isn't a tutorial that can show me how to do everything I want), so any critique would really help me out.
Hook?: I want the simulation to be on an infinite grid. If the grid is infinite, I obviously can't try to iterate over the whole thing to check which cells need to live and die. Only live cells and cells immediately adjacent to live cells can potentially change their state, so instead of a set grid containing cells relative to the grid's origin, I've got a list of cells, and just update the position of all the cells in the list each turn relative to some point, which becomes the new origin.
null2
exists because (null '(list-of-structs)) ==> NIL
Also, is there some way to not have to type my-package::foo
each time I want to use a function outside of a package? It's really cluttering up my code.
gol.asd
;;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
(defsystem "gol"
:components ((:file "cell")
(:file "convert-txt" ;not posted, empty file
:depends-on ("cell"))
(:file "ascii-display"
:depends-on ("cell"))))
cell.lisp
(defpackage :cell
(:use :common-lisp)
(:nicknames :c)
(:export
#:null2
#:for
#:min-x
#:min-y
#:max-x
#:max-y
#:cell-at
#:turn
#:o-change
#:cell
#:cell-p
#:live
#:x
#:y
#:make-cell))
(in-package :cell)
(defstruct (cell (:conc-name nil)
(:print-function (lambda (c s d)
(declare (ignore d))
(format t "(~A ~A ~A) " (live c) (x c) (y c)))))
(live nil) (x 0) (y 0))
(defvar *archive* nil
"List containing all cells that have been in program.") ; fun to it over and make refs
(defmacro for (var start stop &body body)
(let ((gstop (gensym)))
`(do ((,var ,start (1+ ,var))
(,gstop ,stop))
((> ,var ,gstop))
,@body)))
(defmacro for-adj (c &body body)
(let ((gcol (gensym))
(grow (gensym)))
`(for ,gcol -1 1
(for ,grow -1 1
(when (not (and (zerop ,gcol) (zerop ,grow)))
(let* ((adj-x (+ (x ,c) ,gcol))
(adj-y (+ (y ,c) ,grow)))
,@body))))))
(defun null2 (lst)
"Decides whether a list is nil based on length. nd"
(zerop (length lst)))
(defun min-x (lst) (apply #'min (mapcar #'x lst)))
(defun min-y (lst) (apply #'min (mapcar #'y lst)))
(defun max-x (lst) (apply #'max (mapcar #'x lst)))
(defun max-y (lst) (apply #'max (mapcar #'y lst)))
(defun cell-at (x y lst)
"Fetches cell occupying (x y), or puts one there. nd"
(let* ((already? (mapcar (lambda (c) (when (and (= x (x c))
(= y (y c)))
c)) lst))
(already? (remove-if-not (lambda (c) (cell-p c)) already?)))
(if (null2 already?)
(values (make-cell :x x :y y) nil)
(values (car already?) t))))
(defun num-adj (c lst)
"Returns the number of live cells adjacent to c. nd"
(let ((count 0))
(for-adj c
(let ((adj (cell-at adj-x adj-y lst)))
(when (live adj)
(incf count))))
count))
(defun validate (lst)
"Pads all the cells in lst. nd"
(let ((acc lst))
(dolist (c acc acc)
(for-adj c
(multiple-value-bind (adj p)
(cell-at adj-x adj-y acc)
(unless p
(push adj acc)))))))
(defun turn (lst)
"Returns a list containing next turn's extant cells. nd"
(let ((acc nil))
(dolist (c (validate lst) (validate acc))
(let ((nadj (num-adj c lst)))
(when (and (live c) (or (= 2 nadj)
(= 3 nadj)))
(push c acc))
(when (and (not (live c)) (= 3 nadj))
(push (make-cell :live t :x (x c) :y (y c)) acc))))))
(defun o-change (lst &optional (o (car lst)))
"Returns a list with the coordinates of each point updated to be relative to the point o. nd"
(let ((acc nil)
(x-change (- (x o)))
(y-change (- (y o))))
(dolist (c lst acc)
(push (make-cell :live (live c) :x (+ x-change (x c)) :y (+ y-change (y c))) acc))))
ascii-display.lisp
(defpackage :ascii-display
(:use :common-lisp)
(:nicknames :ad))
(in-package :ascii-display)
(defun rect (lst)
"Returns the dimensions of a rectangular region containing all cells in lst. nd"
(values (abs (- (c::max-x lst) (c::min-x lst)))
(abs (- (c::max-y lst) (c::min-y lst)))))
(defun display (lst)
"Prints lst to the screen. nd"
(multiple-value-bind (x y)
(rect lst)
(let* ((top-left (c::cell-at (c::min-x lst) (c::max-y lst) lst))
(lst (c::o-change lst top-left)))
(c::for row 0 y
(c::for col 0 x
(let ((c (c::cell-at col (- row) lst)))
(if (c::live c)
(princ "C ")
(princ "E "))))
(terpri)))))
(defun cycle-input (prompt)
(format t prompt)
(let ((input (read)))
(if (or (eql input 'n) (eql input 'q))
input
(cycle-input prompt))))
(defun cycle (lst &optional (turns 0))
(format t
"Conway's Game of Life : TURN ~A
------------------------------~%" turns)
(display lst)
(format t
"------------------------------
'n' to continue simulation.
'q' to quit.~%")
(let ((input (cycle-input "> ")))
(when (eql input 'n)
(cycle (setf lst (c::turn lst)) (1+ turns)))))
:cell
to the:use
list of:ascii-display
, then you don't need the package prefixes. Also, since the symbols are exported, you can access them with a single:
, you only need::
for non-exported symbols. \$\endgroup\$