123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200 |
- (in-package #:ui)
- #|
- While the game object represents the state of a game, a UI object is meant to
- mediate all its relations with the outer world.
- For one thing, it makes sense to restrict possible user actions depending on
- the game state. For instance, it is useful to prevent a player from moving
- checkers when his opponent is considering a cube decision. This makes it
- possible to filter out unwanted events without re-implementing a state machine
- on the client side. Thus, a UI object contains the information about which
- actions are admissible at a given state of the game.
- TODO add about pauses
- |#
- (defclass base-ui ()
- ((checkers-locked :initform (list :white t :black t))
- (offer-double-locked :initform (list :white t :black t))
- (accept-double-locked :initform (list :white t :black t))
- (finish-move-locked ::initform (list :white t :black t))
- (undo-locked :initform (list :white t :black t))
- (paused :accessor pausedp :initform nil)))
- #|
- For each of the slots, we define a getter (slot-p player ui) and a setter
- (setf (slot-pp player ui) val), which return and modify corresponding
- properties of the plists.
- |#
- (macrolet ((def (function slot)
- `(progn
- (defgeneric ,function (player ui))
- (defmethod ,function (player (ui base-ui))
- (or (pausedp ui) (getf (slot-value ui ',slot) player)))
- (defgeneric (setf ,function) (new-value player ui))
- (defmethod (setf ,function) (new-value player (ui base-ui))
- (setf (getf (slot-value ui ',slot) player) new-value)))))
- (def checkers-locked-p checkers-locked)
- (def offer-double-locked-p offer-double-locked)
- (def accept-double-locked-p accept-double-locked)
- (def finish-move-locked-p finish-move-locked)
- (def undo-locked-p undo-locked))
- (defun unlock-only (actions player ui)
- (macrolet ((foo (accessor code)
- `(setf (,accessor player ui) (if (member ,code actions) nil t)
- (,accessor (game:opponent player) ui) t)))
- (foo checkers-locked-p :checkers)
- (foo offer-double-locked-p :offer-double)
- (foo accept-double-locked-p :accept-double)
- (foo finish-move-locked-p :finish-move)
- (foo undo-locked-p :undo)))
- #|
- A UI object is responsible for updating users' views of the game.
- In a typical web setting, there are several views of a single game. The
- players are supposed to have it open in their browsers. A player can open it
- more than once. There can be spectators. All these views are managed by the
- same UI object. We say that the views are associated with *connections* taken
- in an abstract sense. The set of connections is dynamic (for instance, a user
- can close a browser tab with the game, or a new spectator can join). Now our
- aim is to provide a few primitives in terms of connections. We don't want to
- concretize the implementation of a connection, so we make the fundamental
- functions generic in order to establish methods in a more specific setting.
- Two possible specific settings are hunchensocket and websocket-driver
- connections.
- |#
- (defgeneric refresh (game ui))
- (defmethod refresh ((game game:game) (ui base-ui)))
- (defclass user ()
- ((id :reader id :initarg :id)
- (name :accessor name :initarg :name :initform nil)
- (preferences :reader preferences :initform (make-hash-table :test 'equal))))
- (defclass registered-user (user)
- ())
- (defclass guest-user (user)
- ())
- (defmethod name ((user guest-user))
- (or (call-next-method) (format nil "Guest ~D" (abs (id user)))))
- (defclass multiple-connections-ui (base-ui)
- ((player-users :initform (list :white nil :black nil))))
- (defmethod player-user (player (ui multiple-connections-ui))
- (getf (slot-value ui 'player-users) player))
- (defmethod (setf player-user) (new-value player (ui multiple-connections-ui))
- (setf (getf (slot-value ui 'player-users) player) new-value))
- (defgeneric connections (ui))
- ;; user or nil
- (defgeneric connection-user (connection))
- (defgeneric add-connection (connectiton ui))
- (defgeneric remove-connection (connectiton ui))
- (defgeneric send-to-connection (message connection))
- ;; user or nil
- (defgeneric player-user (player ui))
- (defgeneric user= (user1 user2))
- (defmethod user= (user1 user2)
- (eql user1 user2))
- (defun board-info (board)
- `(:bar (:white ,(board:checkers-on-bar :white board)
- :black ,(board:checkers-on-bar :black board))
- :off (:white ,(board:checkers-off :white board)
- :black ,(board:checkers-off :black board))
- :points ,(loop with k
- with player
- for i from 1 to 24
- do (setf (values k player) (board:checkers-on-point i board))
- collect `(:checkers ,k ,@(if player `(:player ,player))))))
- ;; todo typep => matchp
- (defun game-info (game &optional with-moves?)
- (let* ((session (game::session game))
- (info `(:board ,(board-info (game::partial-board game))
- :dice ,(game::dice game)
- :rest-dice ,(game::rest-dice game)
- :score ,(let ((score (game::game-score game)))
- (abs score))
- :dice-no ,(game::dice-no game)
- :game-no ,(if session
- (length (game::games session))
- 1))))
- (flet ((push-non-null (tag-value)
- (when (second tag-value)
- (setf info (append tag-value info)))))
- (mapcar #'push-non-null `((:cube ,(game::cube game))
- (:turn ,(game::turn game))
- (:cube-owner ,(game::cube-owner game))
- (:is-doubling ,(game::is-doubling game))
- (:winner ,(game::winner game))
- (:match-score ,(and session (game::score session)))
- (:match-limit ,(and (typep session 'game::match) (game::limit session)))
- (:crawford? ,(game::crawford-game-p game))
- (:jacoby? ,(and session (game::jacobyp session)))))
- (when with-moves?
- (push-non-null `(:moves ,(loop for seq in (game::remaining-moves (game::partial-moves game)
- (game::possible-moves game))
- collect (butlast (first seq))))))
- (list :game info))))
- (defun ui-info (player ui)
- (let ((info (loop for (tag lock) on '(:checkers-enabled ui:checkers-locked-p
- :offer-double-enabled ui:offer-double-locked-p
- :accept-double-enabled ui:accept-double-locked-p
- :move-completion-enabled ui:finish-move-locked-p
- :undo-enabled ui:undo-locked-p)
- by #'cddr
- when (not (funcall lock player ui))
- nconc (list tag t))))
- (loop for player in '(:white :black)
- for key in '(:white-name :black-name)
- for user = (player-user player ui)
- when user
- do (setf info (list* key (name user) info))
- finally (return `(:ui ,info)))))
- (defmethod refresh ((game game:game) (ui multiple-connections-ui))
- (loop with turn = (game:turn game)
- for connection in (connections ui)
- for user = (connection-user connection)
- for player = (flet ((user (player)
- (player-user player ui)))
- (find user '(:white :black) :key #'user))
- ;; TODO this is not pretty at all
- ;; avoid recalculating the game info
- ;; the `list "update"' stuff is just atrocious
- do (send-to-connection (cond ((and player (game:player-equal player turn))
- (list "update" (append (ui-info player ui) (game-info game t))))
- (player
- (list "update" (append (ui-info player ui) (game-info game nil))))
- (t (list "update" (game-info game nil))))
- connection)))
|