ui.lisp 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. (in-package #:ui)
  2. #|
  3. While the game object represents the state of a game, a UI object is meant to
  4. mediate all its relations with the outer world.
  5. For one thing, it makes sense to restrict possible user actions depending on
  6. the game state. For instance, it is useful to prevent a player from moving
  7. checkers when his opponent is considering a cube decision. This makes it
  8. possible to filter out unwanted events without re-implementing a state machine
  9. on the client side. Thus, a UI object contains the information about which
  10. actions are admissible at a given state of the game.
  11. TODO add about pauses
  12. |#
  13. (defclass base-ui ()
  14. ((checkers-locked :initform (list :white t :black t))
  15. (offer-double-locked :initform (list :white t :black t))
  16. (accept-double-locked :initform (list :white t :black t))
  17. (finish-move-locked ::initform (list :white t :black t))
  18. (undo-locked :initform (list :white t :black t))
  19. (paused :accessor pausedp :initform nil)))
  20. #|
  21. For each of the slots, we define a getter (slot-p player ui) and a setter
  22. (setf (slot-pp player ui) val), which return and modify corresponding
  23. properties of the plists.
  24. |#
  25. (macrolet ((def (function slot)
  26. `(progn
  27. (defgeneric ,function (player ui))
  28. (defmethod ,function (player (ui base-ui))
  29. (or (pausedp ui) (getf (slot-value ui ',slot) player)))
  30. (defgeneric (setf ,function) (new-value player ui))
  31. (defmethod (setf ,function) (new-value player (ui base-ui))
  32. (setf (getf (slot-value ui ',slot) player) new-value)))))
  33. (def checkers-locked-p checkers-locked)
  34. (def offer-double-locked-p offer-double-locked)
  35. (def accept-double-locked-p accept-double-locked)
  36. (def finish-move-locked-p finish-move-locked)
  37. (def undo-locked-p undo-locked))
  38. (defun unlock-only (actions player ui)
  39. (macrolet ((foo (accessor code)
  40. `(setf (,accessor player ui) (if (member ,code actions) nil t)
  41. (,accessor (game:opponent player) ui) t)))
  42. (foo checkers-locked-p :checkers)
  43. (foo offer-double-locked-p :offer-double)
  44. (foo accept-double-locked-p :accept-double)
  45. (foo finish-move-locked-p :finish-move)
  46. (foo undo-locked-p :undo)))
  47. #|
  48. A UI object is responsible for updating users' views of the game.
  49. In a typical web setting, there are several views of a single game. The
  50. players are supposed to have it open in their browsers. A player can open it
  51. more than once. There can be spectators. All these views are managed by the
  52. same UI object. We say that the views are associated with *connections* taken
  53. in an abstract sense. The set of connections is dynamic (for instance, a user
  54. can close a browser tab with the game, or a new spectator can join). Now our
  55. aim is to provide a few primitives in terms of connections. We don't want to
  56. concretize the implementation of a connection, so we make the fundamental
  57. functions generic in order to establish methods in a more specific setting.
  58. Two possible specific settings are hunchensocket and websocket-driver
  59. connections.
  60. |#
  61. (defgeneric refresh (game ui))
  62. (defmethod refresh ((game game:game) (ui base-ui)))
  63. (defclass user ()
  64. ((id :reader id :initarg :id)
  65. (name :accessor name :initarg :name :initform nil)
  66. (preferences :reader preferences :initform (make-hash-table :test 'equal))))
  67. (defclass registered-user (user)
  68. ())
  69. (defclass guest-user (user)
  70. ())
  71. (defmethod name ((user guest-user))
  72. (or (call-next-method) (format nil "Guest ~D" (abs (id user)))))
  73. (defclass multiple-connections-ui (base-ui)
  74. ((player-users :initform (list :white nil :black nil))))
  75. (defmethod player-user (player (ui multiple-connections-ui))
  76. (getf (slot-value ui 'player-users) player))
  77. (defmethod (setf player-user) (new-value player (ui multiple-connections-ui))
  78. (setf (getf (slot-value ui 'player-users) player) new-value))
  79. (defgeneric connections (ui))
  80. ;; user or nil
  81. (defgeneric connection-user (connection))
  82. (defgeneric add-connection (connectiton ui))
  83. (defgeneric remove-connection (connectiton ui))
  84. (defgeneric send-to-connection (message connection))
  85. ;; user or nil
  86. (defgeneric player-user (player ui))
  87. (defgeneric user= (user1 user2))
  88. (defmethod user= (user1 user2)
  89. (eql user1 user2))
  90. (defun board-info (board)
  91. `(:bar (:white ,(board:checkers-on-bar :white board)
  92. :black ,(board:checkers-on-bar :black board))
  93. :off (:white ,(board:checkers-off :white board)
  94. :black ,(board:checkers-off :black board))
  95. :points ,(loop with k
  96. with player
  97. for i from 1 to 24
  98. do (setf (values k player) (board:checkers-on-point i board))
  99. collect `(:checkers ,k ,@(if player `(:player ,player))))))
  100. ;; todo typep => matchp
  101. (defun game-info (game &optional with-moves?)
  102. (let* ((session (game::session game))
  103. (info `(:board ,(board-info (game::partial-board game))
  104. :dice ,(game::dice game)
  105. :rest-dice ,(game::rest-dice game)
  106. :score ,(let ((score (game::game-score game)))
  107. (abs score))
  108. :dice-no ,(game::dice-no game)
  109. :game-no ,(if session
  110. (length (game::games session))
  111. 1))))
  112. (flet ((push-non-null (tag-value)
  113. (when (second tag-value)
  114. (setf info (append tag-value info)))))
  115. (mapcar #'push-non-null `((:cube ,(game::cube game))
  116. (:turn ,(game::turn game))
  117. (:cube-owner ,(game::cube-owner game))
  118. (:is-doubling ,(game::is-doubling game))
  119. (:winner ,(game::winner game))
  120. (:match-score ,(and session (game::score session)))
  121. (:match-limit ,(and (typep session 'game::match) (game::limit session)))
  122. (:crawford? ,(game::crawford-game-p game))
  123. (:jacoby? ,(and session (game::jacobyp session)))))
  124. (when with-moves?
  125. (push-non-null `(:moves ,(loop for seq in (game::remaining-moves (game::partial-moves game)
  126. (game::possible-moves game))
  127. collect (butlast (first seq))))))
  128. (list :game info))))
  129. (defun ui-info (player ui)
  130. (let ((info (loop for (tag lock) on '(:checkers-enabled ui:checkers-locked-p
  131. :offer-double-enabled ui:offer-double-locked-p
  132. :accept-double-enabled ui:accept-double-locked-p
  133. :move-completion-enabled ui:finish-move-locked-p
  134. :undo-enabled ui:undo-locked-p)
  135. by #'cddr
  136. when (not (funcall lock player ui))
  137. nconc (list tag t))))
  138. (loop for player in '(:white :black)
  139. for key in '(:white-name :black-name)
  140. for user = (player-user player ui)
  141. when user
  142. do (setf info (list* key (name user) info))
  143. finally (return `(:ui ,info)))))
  144. (defmethod refresh ((game game:game) (ui multiple-connections-ui))
  145. (loop with turn = (game:turn game)
  146. for connection in (connections ui)
  147. for user = (connection-user connection)
  148. for player = (flet ((user (player)
  149. (player-user player ui)))
  150. (find user '(:white :black) :key #'user))
  151. ;; TODO this is not pretty at all
  152. ;; avoid recalculating the game info
  153. ;; the `list "update"' stuff is just atrocious
  154. do (send-to-connection (cond ((and player (game:player-equal player turn))
  155. (list "update" (append (ui-info player ui) (game-info game t))))
  156. (player
  157. (list "update" (append (ui-info player ui) (game-info game nil))))
  158. (t (list "update" (game-info game nil))))
  159. connection)))