;;; -*-lisp-*- ;;; ;;; Atoms game ;;; ;;; (c) 2007 Mark Wooding ;;; ;;;----- Licensing notice --------------------------------------------------- ;;; ;;; This program is free software; you can redistribute it and/or modify ;;; it under the terms of the GNU General Public License as published by ;;; the Free Software Foundation; either version 2 of the License, or ;;; (at your option) any later version. ;;; ;;; This program is distributed in the hope that it will be useful, ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;;; GNU General Public License for more details. ;;; ;;; You should have received a copy of the GNU General Public License ;;; along with this program; if not, write to the Free Software Foundation, ;;; Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. (cl:defpackage #:atoms (:use #:cl #+cmu #:ext #+sbcl #:sb-ext #+clisp #:ext) #+clisp (:shadow #:map-dependents #:add-dependent #:remove-dependent) (:export #:start-atom-game)) (cl:in-package #:atoms) (eval-when (:compile-toplevel :load-toplevel :execute) (asdf:operate 'asdf:load-op :gtk)) (clg:clg-init) ;;; Before we start, I should probably point out that the first version of ;;; this program was written in Smalltalk, which may explain its slight ;;; object-ravioli nature. ;;;-------------------------------------------------------------------------- ;;; Dependent management. ;; Protocol. (defgeneric notify (dependent model aspect &key &allow-other-keys) (:method-combination progn) (:method progn (dependent model aspect &rest arguments) (declare (ignore arguments)) nil)) (defgeneric map-dependents (model function)) (defgeneric add-dependent (model dependent)) (defgeneric remove-dependent (model dependent)) (defgeneric changed (model &optional aspect &key &allow-other-keys) (:method (model &optional aspect &rest arguments) (map-dependents model (lambda (dependent) (apply #'notify dependent model aspect arguments))))) ;; Generic implementation. (defclass model () ((dependents :type list :initform nil))) (defun clean-up-danglies (model) (with-slots (dependents) model (setf dependents (delete-if-not (lambda (weak) (nth-value 1 (weak-pointer-value weak))) dependents)))) (defmethod map-dependents ((model model) function) (with-slots (dependents) model (let ((danglies nil)) (dolist (dependent dependents) (multiple-value-bind (object foundp) (weak-pointer-value dependent) (if foundp (funcall function object) (setf danglies t)))) (when danglies (clean-up-danglies model)) nil))) (defmethod add-dependent ((model model) dependent) (let ((foundp (block nil (map-dependents model (lambda (dep) (when (eql dependent dep) (return t))))))) (unless foundp (push (make-weak-pointer dependent) (slot-value model 'dependents))))) (defmethod remove-dependent ((model model) dependent) (with-slots (dependents) model (setf dependents (delete dependent dependents :key #'weak-pointer-value)) (clean-up-danglies model))) ;;;-------------------------------------------------------------------------- ;;; Undo and redo. (defclass undoable () ((undo-list :type list :reader undo-list :initform nil) (redo-list :type list :reader redo-list :initform nil))) (defgeneric snapshot (object)) (defgeneric restore (object snapshot)) (defgeneric store-undo-snapshot (object snapshot)) (defgeneric undo (object)) (defgeneric redo (object)) (defgeneric reset-undo-state (object)) (defmethod store-undo-snapshot ((object undoable) snapshot) (push snapshot (slot-value object 'undo-list)) (setf (slot-value object 'redo-list) nil)) (defmacro with-undo-snapshot ((object) &body body) (let ((snap (gensym "SNAPSHOT")) (obj (gensym "OBJECT"))) `(let* ((,obj ,object) (,snap (snapshot ,obj))) (multiple-value-prog1 (progn ,@body) (store-undo-snapshot ,obj ,snap))))) (defun undo-redo (object from to) (let ((from-list (slot-value object from))) (assert from-list) (let ((undo-snap (car from-list)) (here-snap (snapshot object))) (restore object undo-snap) (push here-snap (slot-value object to)) (pop (slot-value object from))))) (defmethod undo ((object undoable)) (undo-redo object 'undo-list 'redo-list)) (defmethod redo ((object undoable)) (undo-redo object 'redo-list 'undo-list)) (defmethod reset-undo-state ((object undoable)) (setf (slot-value object 'undo-list) nil (slot-value object 'redo-list) nil)) (defclass undoable-model (undoable model) ()) (defmethod undo :after ((object undoable-model)) (changed object :undo)) (defmethod redo :after ((object undoable-model)) (changed object :redo)) ;;;-------------------------------------------------------------------------- ;;; Main game logic. ;; Protocol. (defclass atom-cell () ((owner :reader cell-owner :initform nil :type (or fixnum null)) (count :reader cell-count :initform 0 :type fixnum) (pending :initform 0 :type fixnum) (neighbours :reader cell-neighbours :type list :initform nil) (x :reader cell-x :initarg :x :type fixnum) (y :reader cell-y :initarg :y :type fixnum))) (defgeneric cell-played (cell player)) (defgeneric cell-critical-p (cell)) (defgeneric cell-explode (cell)) (defgeneric cell-apply-pending-updates (cell)) (defun cell-position (cell) (vector (cell-x cell) (cell-y cell))) (defun make-atoms-grid (width height) (let ((grid (make-array (list height width) :element-type 'atom-cell))) (dotimes (j height) (dotimes (i width) (setf (aref grid j i) (make-instance 'atom-cell :x i :y j)))) (dotimes (j height) (dotimes (i width) (setf (slot-value (aref grid j i) 'neighbours) (nconc (and (> j 0) (list (aref grid (1- j) i))) (and (> i 0) (list (aref grid j (1- i)))) (and (< i (1- width)) (list (aref grid j (1+ i)))) (and (< j (1- height)) (list (aref grid (1+ j) i))))))) grid)) (defclass player () ((name :accessor player-name :initarg :name :type string) (score :accessor player-score :initform 0 :type fixnum) (state :accessor player-state :initform :starting :type (member :starting :playing :ready :losing :winning)) (colour :accessor player-colour :initarg :colour :type gdk:color))) (defclass human-player (player) ()) (defgeneric player-cell-selected (game player i j) (:method (game player i j) nil)) (defgeneric player-turn-begin (game player) (:method (game player) nil)) (defclass atom-game (undoable-model) ((grid :accessor game-grid :initarg :grid :type (array atom-cell (* *))) (players :accessor game-players :initarg :players :type vector) (player-index :accessor game-player-index :initform 0 :type fixnum) (timeout-id :initform nil))) (defgeneric game-cell-selected (game i j)) (defgeneric play-cell (game player i j)) ;; Implementation. (defmethod cell-played ((cell atom-cell) player) (with-slots (owner count) cell (cond ((zerop count) (setf owner player count 1) t) ((eql owner player) (incf count) t) (t nil)))) (defmethod cell-critical-p ((cell atom-cell)) (with-slots (count neighbours) cell (>= count (length neighbours)))) (defmethod cell-explode ((cell atom-cell)) (with-slots (count neighbours owner pending) cell (multiple-value-bind (spill left) (floor count (length neighbours)) (and (plusp spill) (progn (dolist (neighbour neighbours) (incf (slot-value neighbour 'pending) spill) (setf (slot-value neighbour 'owner) owner)) (setf count left) (when (zerop left) (setf owner nil)) (cons cell (copy-list neighbours))))))) (defmethod cell-apply-pending-updates ((cell atom-cell)) (with-slots (count pending) cell (incf count pending) (setf pending 0))) (deftype cell-snapshot () '(unsigned-byte 16)) (defmethod snapshot ((cell atom-cell)) (with-slots (count owner) cell (cond ((null owner) 0) (t (assert (and (<= 0 count 255) (<= 0 owner 255))) (logior (ash owner 8) (ash count 0)))))) (defmethod restore ((cell atom-cell) (snapshot integer)) (declare (type cell-snapshot snapshot)) (with-slots (count owner) cell (setf (values count owner) (if (zerop snapshot) (values 0 nil) (values (ldb (byte 8 0) snapshot) (ldb (byte 8 8) snapshot)))))) (defmethod player-cell-selected (game (player human-player) i j) (and (eql (player-state player) :ready) (play-cell game player i j))) (defmethod snapshot ((player player)) (list (player-score player) (player-state player))) (defmethod restore ((player player) (list list)) (destructuring-bind (score state) list (setf (player-score player) score (player-state player) state))) (defmethod game-update-scores (game) (let ((players (game-players game)) (grid (game-grid game))) (dotimes (i (length players)) (setf (player-score (aref players i)) 0)) (dotimes (i (array-total-size grid)) (let* ((cell (row-major-aref grid i)) (owner (cell-owner cell)) (player (and owner (aref players owner))) (count (cell-count cell))) (when (and player (plusp count)) (incf (player-score player) count)))) (let ((remaining 0) (found nil)) (dotimes (i (length players)) (let* ((player (aref players i)) (score (player-score player)) (state (player-state player))) (cond ((and (zerop score) (eql state :playing)) (setf (player-state player) :losing)) ((member state '(:playing :starting :ready)) (incf remaining) (setf found player))))) (changed game :scores :players players) (when (= remaining 1) (setf (player-state found) :winning) (changed game :finished :victor found))))) (defmethod game-next-player (game) (let ((players (game-players game)) (player-index (game-player-index game))) (dotimes (i (length players)) (let* ((j (mod (+ player-index i 1) (length players))) (player (aref players j))) (when (member (player-state player) '(:starting :playing)) (setf (game-player-index game) j (player-state player) :ready) (player-turn-begin game player) (changed game :start-turn :player player) (return)))))) (defvar *cells-remaining* nil) (defun perform-pending-explosions (game cells) (let ((affected (delete-duplicates (mapcan #'cell-explode cells)))) (mapc #'cell-apply-pending-updates affected) (perform-explosions game affected))) (defvar *explosion-time* 100) (defun perform-explosions (game cells) (game-update-scores game) (changed game :cell-update :cells cells) (let ((critical (delete-if-not #'cell-critical-p cells))) (setf *cells-remaining* critical) (cond ((null critical) (game-next-player game) t) (t (with-slots (timeout-id) game (setf timeout-id (glib:timeout-add *explosion-time* (lambda () (setf timeout-id nil) (perform-pending-explosions game critical) nil)))))) t)) (defun game-cancel-timeout (game) (with-slots (timeout-id) game (when timeout-id (glib:source-remove timeout-id) (setf timeout-id nil)))) (defmethod game-player ((game atom-game)) (aref (game-players game) (game-player-index game))) (defmethod game-cell-selected ((game atom-game) i j) (player-cell-selected game (game-player game) i j)) (defmethod initialize-instance :after ((game atom-game) &key) (setf (player-state (game-player game)) :ready)) (defmethod play-cell ((game atom-game) player i j) (with-slots (grid players player-index) game (assert (and (<= 0 i) (< i (array-dimension grid 1)) (<= 0 j) (< j (array-dimension grid 0)))) (let ((cell (aref grid j i)) (player (aref players player-index))) (block escape (with-undo-snapshot (game) (unless (cell-played cell player-index) (return-from escape)) (setf (player-state player) :playing) (perform-explosions game (list cell))) (changed game :processing-move))))) (defmethod restart-game ((game atom-game) &key grid players) (game-cancel-timeout game) (setf (game-grid game) (or grid (let ((old (game-grid game))) (make-atoms-grid (array-dimension old 1) (array-dimension old 0))))) (if players (setf (game-players game) players) (setf players (game-players game))) (reset-undo-state game) (dotimes (i (length players)) (let ((player (aref players i))) (setf (player-score player) 0 (player-state player) (if (zerop i) :ready :starting)))) (setf (game-player-index game) 0) (changed game :refresh)) ;;;-------------------------------------------------------------------------- ;;; Snapshots and undo. (defclass atom-game-snapshot () ((grid :type (array cell-snapshot (* *)) :initarg :grid) (players :type list :initarg :players) (player-index :type fixnum :initarg :player-index))) (defmethod snapshot ((game atom-game)) (let* ((grid (game-grid game)) (grid-snapshot (make-array (array-dimensions grid) :element-type 'cell-snapshot :initial-element 0))) (dotimes (i (array-total-size grid)) (setf (row-major-aref grid-snapshot i) (snapshot (row-major-aref grid i)))) (make-instance 'atom-game-snapshot :players (map 'list #'snapshot (game-players game)) :player-index (game-player-index game) :grid grid-snapshot))) (defmethod restore ((game atom-game) (snapshot atom-game-snapshot)) (let ((snap-grid (slot-value snapshot 'grid)) (snap-players (slot-value snapshot 'players)) (grid (game-grid game)) (players (game-players game))) (dotimes (i (array-total-size grid)) (restore (row-major-aref grid i) (row-major-aref snap-grid i))) (loop for player across players for snap-player in snap-players do (restore player snap-player)) (setf (game-player-index game) (slot-value snapshot 'player-index)) (game-cancel-timeout game) (changed game :refresh))) ;;;-------------------------------------------------------------------------- ;;; The interactive board. (defclass atoms-board (gtk:drawing-area) ((game :accessor board-game :initarg :game :type atom-game) (cache :initform nil :accessor board-cache)) (:metaclass glib:gobject-class)) (defmethod board-grid ((board atoms-board)) (game-grid (board-game board))) (defgeneric paint (widget event)) (defun paint-atoms (cr count colour) (let* ((centrep (and (oddp count) (/= count 3))) (surround (if centrep (1- count) count)) (angle (and (plusp surround) (/ (* 2 pi) surround))) (theta (case count ((0 1 2 3) (/ pi 2)) (t (/ (- pi angle) 2)))) (radius 0.15) (sep (cond ((and centrep (<= surround 6)) (* 2 radius)) ((<= surround 2) radius) (t (/ radius (sin (/ angle 2))))))) (when centrep (cairo:new-sub-path cr) (cairo:arc cr 0 0 radius 0 (* 2 pi))) (dotimes (i surround) (cairo:new-sub-path cr) (cairo:arc cr (* sep (cos theta)) (- (* sep (sin theta))) radius 0 (* 2 pi)) (incf theta angle)) (gdk:cairo-set-source-color cr (gdk:ensure-color colour)) (cairo:fill cr t) (setf (cairo:line-width cr) (max 0.02 (cairo:device-to-user-distance cr 1))) (cairo:set-source-color cr 0 0 0) (cairo:stroke cr nil))) (defparameter cache-limit 8) (defun make-cached-atom-surfaces (board colour) (multiple-value-bind (width height) (gtk:widget-get-size-allocation board) (let* ((vector (make-array cache-limit)) (grid (board-grid board)) (surface-width (floor width (array-dimension grid 1))) (surface-height (floor height (array-dimension grid 0)))) (dotimes (i (length vector)) (let* ((surface (make-instance 'cairo:image-surface :width surface-width :height surface-height :format :argb32)) (cr (make-instance 'cairo:context :target surface))) (cairo:scale cr surface-width surface-height) (cairo:translate cr 0.5 0.5) (paint-atoms cr (1+ i) colour) (setf (aref vector i) surface))) vector))) (defun cached-atom-surface (board count colour) (let ((cache (board-cache board))) (unless cache (setf cache (make-hash-table) (board-cache board) cache)) (let ((vector (gethash colour cache))) (unless vector (setf vector (make-cached-atom-surfaces board colour) (gethash colour cache) vector)) (and (< 0 count) (<= count (length vector)) (aref vector (1- count)))))) (defmethod paint ((widget atoms-board) event) (multiple-value-bind (width height) (gtk:widget-get-size-allocation widget) (let* ((style (gtk:widget-style widget)) (grid (board-grid widget)) (vsq (array-dimension grid 0)) (hsq (array-dimension grid 1)) (game (board-game widget)) (players (game-players game)) lo-hsq hi-hsq lo-vsq hi-vsq (display (gtk:widget-get-display widget)) (region (make-instance 'gdk:region)) (redraw-map (make-array (list vsq hsq) :element-type 'bit :initial-element 0))) (loop (let* ((loh (floor (* (gdk:event-x event) hsq) width)) (hih (ceiling (* (+ (gdk:event-x event) (gdk:event-width event)) hsq) width)) (lov (floor (* (gdk:event-y event) vsq) height)) (hiv (ceiling (* (+ (gdk:event-y event) (gdk:event-height event)) vsq) height))) (gdk:region-union region (vector (gdk:event-x event) (gdk:event-y event) (gdk:event-width event) (gdk:event-height event))) (when (or (null lo-hsq) (< loh lo-hsq)) (setf lo-hsq loh)) (when (or (null hi-hsq) (< hih hi-vsq)) (setf hi-hsq hih)) (when (or (null lo-vsq) (< lov lo-hsq)) (setf lo-vsq lov)) (when (or (null hi-vsq) (< hiv hi-vsq)) (setf hi-vsq hiv)) (do ((j lov (1+ j))) ((>= j hiv)) (do ((i loh (1+ i))) ((>= i hih)) (setf (bit redraw-map j i) 1))) (when (zerop (gdk:event-count event)) (return)) (setf event (gdk:display-get-event display)))) (gdk:with-cairo-context (cr (gtk:widget-window widget)) (cairo:reset-clip cr) (gdk:cairo-region cr region) (cairo:clip cr) (cairo:with-context (cr) (gdk:cairo-set-source-color cr (gtk:style-fg style :normal)) (cairo:translate cr 1/2 1/2) (setf (cairo:line-width cr) 1 (cairo:antialias cr) :none) (let ((h (1- height)) (w (1- width))) (do ((j lo-vsq (1+ j))) ((> j hi-vsq)) (let ((y (round (* j h) vsq))) (cairo:move-to cr 0 y) (cairo:line-to cr w y))) (do ((i lo-hsq (1+ i))) ((> i hi-hsq)) (let ((x (round (* i w) hsq))) (cairo:move-to cr x 0) (cairo:line-to cr x h)))) (cairo:stroke cr)) (do ((j lo-vsq (1+ j))) ((>= j hi-vsq)) (do ((i lo-hsq (1+ i))) ((>= i hi-hsq)) (when (plusp (bit redraw-map j i)) (let* ((cell (aref grid j i)) (count (cell-count cell)) (colour (and (plusp count) (cell-owner cell) (player-colour (aref players (cell-owner cell))))) (surface (and colour (cached-atom-surface widget count colour)))) (cond ((or (zerop count) (null (cell-owner cell))) nil) ((null surface) (cairo:with-context (cr) (cairo:scale cr (/ width hsq) (/ height vsq)) (cairo:translate cr (+ i 0.5) (+ j 0.5)) (paint-atoms cr count colour))) (t (cairo:set-source-surface cr surface (round (* i width) hsq) (round (* j height) vsq)) (cairo:paint cr))))))))))) (defun board-set-size-request (board) (when (slot-boundp board 'game) (let ((grid (board-grid board))) (gtk:widget-set-size-request board (* 50 (array-dimension grid 1)) (* 50 (array-dimension grid 0)))))) (defmethod (setf board-game) :before (game (board atoms-board)) (when (slot-boundp board 'game) (remove-dependent (board-game board) board))) (defmethod (setf board-game) :after (game (board atoms-board)) (board-set-size-request board) (add-dependent game board)) (defmethod resized ((board atoms-board) allocation) (setf (board-cache board) nil) nil) (defmethod notify progn ((board atoms-board) (game atom-game) (aspect (eql :cell-update)) &key cells) (unless (slot-boundp board 'gtk:window) (return-from notify)) (multiple-value-bind (width height) (gtk:widget-get-size-allocation board) (let* ((region (make-instance 'gdk:region)) (grid (board-grid board)) (hsq (array-dimension grid 1)) (vsq (array-dimension grid 0))) (dolist (cell cells) (gdk:region-union region (vector (floor (* (cell-x cell) width) hsq) (floor (* (cell-y cell) height) vsq) (ceiling width hsq) (ceiling height vsq)))) (gdk:window-invalidate-region (gtk:widget-window board) region nil)))) (defmethod notify progn ((board atoms-board) (game atom-game) (aspect (eql :refresh)) &key) (board-set-size-request board) (setf (board-cache board) nil) (gtk:widget-queue-draw board)) (defmethod button-press ((widget atoms-board) event) (case (gdk:event-class-type (class-of event)) (:button-press (case (gdk:event-button event) (1 (multiple-value-bind (width height) (gtk:widget-get-size-allocation widget) (let* ((grid (board-grid widget)) (x (floor (* (gdk:event-x event) (array-dimension grid 1)) width)) (y (floor (* (gdk:event-y event) (array-dimension grid 0)) height))) (game-cell-selected (board-game widget) x y) t))))))) (defmethod initialize-instance :after ((board atoms-board) &key) (gtk:signal-connect board :expose-event #'paint :object t) (setf (gtk:widget-events board) (list :button-press)) (gtk:signal-connect board :button-press-event #'button-press :object t) (gtk:signal-connect board :size-allocate #'resized :object t) (when (slot-boundp board 'game) (add-dependent (board-game board) board)) (board-set-size-request board)) ;;;-------------------------------------------------------------------------- ;;; Tree view utilities. (defun add-tree-view-column (view title &rest args) (let ((column (apply #'make-instance 'gtk:tree-view-column :title title args))) (gtk:tree-view-append-column view column) column)) (defun add-cell-renderer (view column attrs &key (type 'gtk:cell-renderer-text) pack-args renderer-args) (let ((renderer (apply #'make-instance type renderer-args)) (store (gtk:tree-view-model view))) (apply #'gtk:cell-layout-pack column renderer pack-args) (loop for (attribute col-name) on attrs by #'cddr do (gtk:cell-layout-add-attribute column renderer attribute (gtk:tree-model-column-index store col-name))) renderer)) ;;;-------------------------------------------------------------------------- ;;; The player list. (defvar *player-list*) (defvar *player-list-view*) (defclass player-list (gtk:tree-view) ((store :initform (make-instance 'gtk:list-store :column-names '(colour name score state) :column-types '(gdk:color string integer string)) :type gtk:list-store) (game :initarg :game :type atom-game)) (:metaclass glib:gobject-class)) (defun update-player-list (list game) (let ((store (slot-value list 'store)) (players (game-players game))) (gtk:list-store-clear store) (loop for player across players for i from 0 do (gtk:list-store-append store (vector (player-colour player) (player-name player) (player-score player) (case (player-state player) (:losing "out") (:winning "winner!") (:ready "<<<") (t ""))))))) (defmethod initialize-instance :after ((list player-list) &key) (let ((store (slot-value list 'store))) (setf (gtk:tree-view-model list) store) (flet ((add-column (&rest args) (apply #'add-tree-view-column list args)) (add-renderer (&rest args) (apply #'add-cell-renderer list args))) (add-renderer (add-column "" :expand nil :sizing :fixed :fixed-width 20) '(:cell-background-gdk colour) :renderer-args '(:cell-background-set t)) (add-renderer (add-column "Name" :resizable t :expand t) '(:text name)) (add-renderer (add-column "Score" :resizable t) '(:text score)) (add-renderer (add-column "State" :resizable t) '(:text state))) (setf (gtk:tree-selection-mode (gtk:tree-view-selection list)) :none) (when (slot-boundp list 'game) (with-slots (game) list (add-dependent game list) (update-player-list list game))))) (defmethod notify progn ((list player-list) (game atom-game) aspect &key) (case aspect ((:cell-update :start-turn :refresh) (update-player-list list game)))) ;;;-------------------------------------------------------------------------- ;;; New game dialogue. (defparameter *player-colours* (vector "red" "blue" "green" "orange" "magenta" "white" "black")) (defclass new-game-dialogue (gtk:dialog) ((game :initarg :game :type atom-game) (width-adjustment :type gtk:adjustment :initform (make-instance 'gtk:adjustment :lower 1 :upper 99 :step-increment 1)) (height-adjustment :type gtk:adjustment :initform (make-instance 'gtk:adjustment :lower 1 :upper 99 :step-increment 1)) (count-adjustment :type gtk:adjustment :initform (make-instance 'gtk:adjustment :lower 2 :upper 20 :step-increment 1)) (players :type gtk:list-store :initform (make-instance 'gtk:list-store :column-types '(gdk:color string) :column-names '(colour name)))) (:default-initargs :title "New game" :default-height 360 :has-separator nil) (:metaclass glib:gobject-class)) (defun choose-player-colour (window path) (let* ((players (slot-value window 'players)) (colour-dialogue (make-instance 'gtk:color-selection-dialog)) (coloursel (gtk:color-selection-dialog-colorsel colour-dialogue)) (colour (gtk:tree-model-value players path 'colour))) (unwind-protect (progn (setf (gtk:color-selection-current-color coloursel) colour (gtk:color-selection-previous-color coloursel) colour) (case (gtk:dialog-run colour-dialogue) (:ok (setf (gtk:tree-model-value players path 'colour) (gtk:color-selection-current-color coloursel))))) (gtk:widget-destroy colour-dialogue)))) (defun insert-or-remove-players (window) (let* ((players (slot-value window 'players)) (current-count (gtk:tree-model-iter-n-children players)) (new-count (floor (gtk:adjustment-value (slot-value window 'count-adjustment))))) (if (> current-count new-count) (let ((iter (make-instance 'gtk:tree-iter))) (gtk:tree-model-get-iter players (vector new-count) iter) (dotimes (i (- current-count new-count)) (gtk:list-store-remove players iter))) (loop with iter = (make-instance 'gtk:tree-iter) for i from current-count below new-count for colour = (if (< i (length *player-colours*)) (gdk:color-parse (aref *player-colours* i)) (make-instance 'gdk:color :red (random 1.0) :green (random 1.0) :blue (random 1.0))) for name = (format nil "Player ~A" (1+ i)) do (gtk:list-store-append players (vector colour name) iter))))) (defun start-new-game (window) (with-slots (game width-adjustment height-adjustment players) window (let ((grid (make-atoms-grid (floor (gtk:adjustment-value width-adjustment)) (floor (gtk:adjustment-value height-adjustment)))) (new-players (let ((iter (make-instance 'gtk:tree-iter))) (gtk:tree-model-get-iter players #(0) iter) (loop for row = (gtk:tree-model-row-data players iter) collect (make-instance 'human-player :colour (aref row 0) :name (aref row 1)) while (gtk:tree-model-iter-next players iter))))) (restart-game game :grid grid :players (coerce new-players 'vector)) (gtk:widget-destroy window)))) (defmethod initialize-instance :after ((window new-game-dialogue) &key) (with-slots (width-adjustment height-adjustment count-adjustment players) window (let* ((game (slot-value window 'game)) (grid (game-grid game))) (setf (gtk:container-border-width window) 4) (gtk:dialog-add-button window "gtk-cancel" #'gtk:widget-destroy :object t) (gtk:dialog-add-button window "gtk-ok" (lambda () (start-new-game window)) :default t) (setf (gtk:adjustment-value width-adjustment) (array-dimension grid 1) (gtk:adjustment-value height-adjustment) (array-dimension grid 0) (gtk:adjustment-value count-adjustment) (length (game-players game))) (let* ((frame (make-instance 'gtk:frame :label "Board size")) (table (make-instance 'gtk:table :parent frame :border-width 4 :n-columns 2 :n-rows 2 :row-spacing 4 :column-spacing 4))) (loop for row from 0 for (adj-slot label) on '(width-adjustment "Width" height-adjustment "Height") by #'cddr do (make-instance 'gtk:label :label label :xalign 1 :parent (list table :top-attach row :bottom-attach (1+ row) :left-attach 0 :right-attach 1 :x-options '(:fill))) do (make-instance 'gtk:spin-button :numeric t :width-chars 2 :adjustment (slot-value window adj-slot) :xalign 1 :activates-default t :parent (list table :top-attach row :bottom-attach (1+ row) :left-attach 1 :right-attach 2 :x-options '(:expand :fill)))) (gtk:container-add window frame :fill nil :expand nil) (gtk:widget-show-all frame)) (let* ((frame (make-instance 'gtk:frame :label "Players")) (vbox (make-instance 'gtk:v-box :parent frame :spacing 4 :border-width 4)) (view (make-instance 'gtk:tree-view :model players))) (make-instance 'gtk:h-box :spacing 4 :parent (list vbox :expand nil :fill nil) :child (list (make-instance 'gtk:label :label "Number of players" :xalign 1) :expand nil :fill nil) :child (list (make-instance 'gtk:spin-button :adjustment count-adjustment :numeric t :width-chars 2 :activates-default t :xalign 1) :expand t :fill t)) (make-instance 'gtk:scrolled-window :hscrollbar-policy :automatic :vscrollbar-policy :automatic :shadow-type :in :child view :parent vbox) (add-cell-renderer view (add-tree-view-column view "" :sizing :fixed :fixed-width 20) (list :cell-background-gdk 'colour) :renderer-args '(:cell-background-set t)) (let ((renderer (add-cell-renderer view (add-tree-view-column view "Name") (list :text 'name) :renderer-args '(:editable t)))) (gtk:signal-connect renderer :edited (lambda (path new-text) (setf (gtk:tree-model-value players path 'name) new-text)))) (gtk:signal-connect view :row-activated (lambda (path column) (when (eql (position column (gtk:tree-view-columns view)) 0) (choose-player-colour window path)))) (loop for player across (game-players game) do (gtk:list-store-append players (vector (player-colour player) (player-name player)))) (gtk:signal-connect count-adjustment :value-changed #'insert-or-remove-players :args (list window)) (gtk:container-add window frame :fill t :expand t) (gtk:widget-show-all frame))))) ;;;-------------------------------------------------------------------------- ;;; About this program. (defparameter atoms-logo-pixbuf (gdk:pixbuf-load #p"/home/mdw/src/atoms/atoms.png")) (defparameter licence-text (format nil "This program is free software; you can redistribute it and/or modify ~ it under the terms of the GNU General Public License as published by ~ the Free Software Foundation; either version 2 of the License, or ~ (at your option) any later version.~2%~ ~ This program is distributed in the hope that it will be useful, ~ but WITHOUT ANY WARRANTY; without even the implied warranty of ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ~ GNU General Public License for more details.~2%~ ~ You should have received a copy of the GNU General Public License ~ along with this program; if not, write to the Free Software Foundation, ~ Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.")) (let ((about nil)) (defun action-about (window) (declare (ignore window)) (unless about (setf about (make-instance 'gtk:about-dialog :name "Atoms" :version "1.0.0" :copyright "Copyright (c) 2007 Mark Wooding" :website "http://www.distorted.org.uk/" :website-label "Homepage" :authors (list "Mark Wooding ") :comments "May contain trace quantities of Lisp." :license licence-text :wrap-license t :logo atoms-logo-pixbuf :signal (list :destroy (lambda () (setf about nil))) :signal (list :cancel (lambda () (gtk:widget-destroy about)))))) (gtk:window-present about))) ;;;-------------------------------------------------------------------------- ;;; Application window. (defclass atom-game-window (gtk:window) ((game :type atom-game) (board :type atoms-board) (player-list :type player-list) (actions :type gtk:action-group) (ui :type gtk:ui-manager) (new-game :type (or new-game-dialogue null) :initform nil)) (:default-initargs :title "Atoms game" :allow-shrink t :show-children t) (:metaclass glib:gobject-class)) (defun action-quit (window) (gtk:widget-destroy window)) (defun action-new-game (window) (with-slots (new-game game) window (if new-game (gtk:window-present new-game) (progn (setf new-game (make-instance 'new-game-dialogue :game game :transient-for window)) (gtk:widget-show-all new-game) (gtk:signal-connect new-game :destroy (lambda () (setf new-game nil))))))) (defun update-undo-redo-sensitivity (window) (with-slots (actions game) window (setf (gtk:action-sensitive-p (gtk:action-group-get-action actions "undo")) (undo-list game) (gtk:action-sensitive-p (gtk:action-group-get-action actions "redo")) (redo-list game)))) (defmethod notify progn ((window atom-game-window) (game atom-game) aspect &key) (case aspect ((:undo :redo :refresh :processing-move) (update-undo-redo-sensitivity window)))) (defun action-undo (window) (undo (slot-value window 'game))) (defun action-redo (window) (redo (slot-value window 'game))) (defmethod destroyed ((window atom-game-window)) (with-slots (new-game) window (when new-game (gtk:widget-destroy new-game)))) (defun build-player-vector (player-spec) (flet ((make-player (spec i) (etypecase spec (player spec) ((or string list) (destructuring-bind (name &key colour) (if (listp spec) spec (list spec)) (cond (colour) ((< i (length *player-colours*)) (setf colour (aref *player-colours* i))) (t (setf colour (make-instance 'gdk:color :red (random 1.0) :green (random 1.0) :blue (random 1.0))))) (make-instance 'human-player :name name :colour (gdk:ensure-color colour))))))) (let ((i 0)) (map 'vector (lambda (spec) (make-player spec (prog1 i (incf i)))) (etypecase player-spec (sequence player-spec) ((or integer null) (loop for i from 1 upto (or player-spec 4) collect (format nil "Player ~A" i)))))))) (defmethod shared-initialize :after ((window atom-game-window) slot-names &key (width 7) (height width) players) (declare (ignore slot-names)) (let* ((vbox (make-instance 'gtk:v-box :parent window)) (paned (make-instance 'gtk:h-paned :parent (list vbox :pack-type :end))) (aspect (make-instance 'gtk:aspect-frame :parent (list paned :resize t :shrink t) :obey-child t :frame :none :shadow-type :none)) (scrolled (make-instance 'gtk:scrolled-window :parent (list paned :resize nil :shrink t) :shadow-type :in :hscrollbar-policy :automatic :vscrollbar-policy :automatic)) (action-list (mapcar (lambda (item) (destructuring-bind (name callback &rest args) item (apply #'make-instance 'gtk:action :name name :callback (and callback (list callback :args (list window))) args))) `(("file" nil :label "_File") ("edit" nil :label "_Edit") ("help" nil :label "_Help") ("quit" ,#'action-quit :stock-id "gtk-close" :tooltip "Close this window." :accelerator "W") ("undo" ,#'action-undo :stock-id "gtk-undo" :tooltip "Take back the most recent move." :sensitive nil :accelerator "Z") ("redo" ,#'action-redo :stock-id "gtk-redo" :sensitive nil :tooltip "Revert an undone move." :accelerator "Z") ("about" ,#'action-about :tooltip "Show information about this game." :stock-id "gtk-about") ("new-game" ,#'action-new-game :label "_New game..." :stock-id "gtk-new" :tooltip "Start a new game." :accelerator "N"))))) (with-slots (game board player-list ui actions) window (setf actions (make-instance 'gtk:action-group :name "actions" :actions action-list) ui (make-instance 'gtk:ui-manager :add-tearoffs t :action-group actions :ui '((:menubar "menu-bar" (:menu "file" (:menuitem "new-game") (:menuitem "quit")) (:menu "edit" (:menuitem "undo") (:menuitem "redo")) (:menu "help" (:menuitem "about"))) (:toolbar "toolbar" (:toolitem "new-game") :separator (:toolitem "undo") (:toolitem "redo"))))) (gtk:window-add-accel-group window (gtk:ui-manager-accel-group ui)) (setf (gtk:toolbar-show-arrow-p (gtk:ui-manager-get-widget ui "/toolbar")) nil) (dolist (name '("/menu-bar" "/toolbar")) (make-instance 'gtk:handle-box :child (gtk:ui-manager-get-widget ui name) :parent (list vbox :expand nil))) (gtk:signal-connect window :destroy #'destroyed :object t) (setf game (make-instance 'atom-game :grid (make-atoms-grid width height) :players (build-player-vector players)) board (make-instance 'atoms-board :game game :parent aspect) player-list (make-instance 'player-list :game game :width-request 160 :parent scrolled)) (add-dependent game window)))) ;;;-------------------------------------------------------------------------- ;;; Useful things. (defvar *window* nil) (defun start-atom-game (&rest initargs) (when *window* (gtk:widget-destroy *window*) (setf *window* nil)) (setf *window* (apply #'make-instance 'atom-game-window initargs)) (gtk:widget-show-all *window*)) (start-atom-game :width 7 :players (list "Mark" "Vicky")) ;;;----- That's all, folks --------------------------------------------------