-
Notifications
You must be signed in to change notification settings - Fork 0
Start a Game
We are getting close to a working game. The next improvement we need to make is to prevent the game from starting after login. It would be nice if we could wait for more players to join before starting the game.
We will create a wait screen which will live between the login and game screens. After login a player will go to the wait page. As players join, each player will be displayed on the wait page. This page will also have a Start button. When this button is clicked the game will start with the current players. If a player joins the game after the game has started they will immediately enter the game.
While making these changes we will learn about the following Pedestal concepts:
- Continue Functions
- Changing focus based on state changes
- Generating new messages for the input queue
To add the wait screen, we will follow the same procedure that was followed when adding the login screen. We first add the new data to the data model, including transforms. We then move this to a new screen and then, finally, render it.
While making the following changes, have the Data UI running so that you can see the effect of each change.
The first thing we do is create the structure for the wait
screen. This screen will have a Start button. When this button is
clicked it should start the game locally and for each connected
player. We will create a transform which sends two messages: one to
change our local state and one to change the state of remote
players. The new init-wait function is shown below.
(defn init-wait [_]
(let [start-game {msg/type :swap msg/topic [:active-game] :value true}]
[{:wait
{:start
{:transforms
{:start-game [{msg/topic msg/output :payload start-game}
start-game]}}}}]))This function creates the initial structure for the wait page and adds
the :start-game transform to the [:wait :start] node. this
transform contains two messages, one of which we have not seen before.
{msg/topic msg/output :payload start-game}This message will place the :payload message directly on the output
(effect) queue.
Note: change msg/output to msg/effect.
Update the dataflow definition to receive :swap messages sent to
[:active-game].
[:swap [:active-game] swap-transform]The wait page will not only have a Start button but it will also
show a list of the players which are available to play a game. We
already have a list of players in [:players :*] which we can use to
populate the this list.
First we configure the init-wait function and add an emitter which
will emit [:players :*] under the :wait root node.
{:init init-wait}
{:in #{[:players :*]} :fn (app/default-emitter :wait) :mode :always}This emitter has something that we have not seen before. The :mode
is set to :always. The list of emitters is processed
sequentially. Normally, if an emitter emits something at a path, any
subsequent emitter will not see this change. This allows early
emitters to override the behavior of emitters that appear later.
To allow two emitters to emit changes for the same path, we must set
their mode to :always. We must do this for all emitters which handle
the same paths.
{:in #{[:players :*]} :fn (app/default-emitter :game) :mode :always}
{:in #{[:average-count]
[:total-count]
[:max-count]} :fn (app/default-emitter :game)}Finally, update the focus to show the :wait section of the tree on
the game screen.
:focus {:login [[:login]]
:game [[:game] [:pedestal] [:wait]]
:default :login}With these changes in place, the Data UI should print the
[:active-game] messages to the console when the Start button is
clicked.
As things are now, the game still starts when the app starts instead
of when the Start button is clicked. The service simulator, in the
namespace tutorial-client.simulated.service, must be updated to
simulate the new behavior.
Note: if we had used input-queue from the beginning there wouldn't be this many changes required.
(defn increment-counter [key t input-queue]
(p/put-message input-queue {msg/type :swap
msg/topic [:other-counters key]
:value (get (swap! counters update-in [key] inc) key)})
(platform/create-timeout t #(increment-counter key t input-queue)))
(defn receive-messages [input-queue]
(increment-counter "abc" 2000 input-queue)
(increment-counter "xyz" 5000 input-queue))
(defn start-game-simulation [input-queue]
(receive-messages input-queue))
(defrecord MockServices [app]
p/Activity
(start [this]
;; this will now simulate adding players
)
(stop [this]))
(defn services-fn [message input-queue]
(if (and (= (msg/topic message) [:active-game]) (:value message))
(start-game-simulation input-queue)
(.log js/console (str "Sending message to server: " message))))Instead of starting the game when calling start, the start function
will now do nothing. A game will be started when a message is sent
which sets [:active-game] to true.
Test this in the Data UI to ensure that a game starts properly.
Instead of starting the game when we call start on the service, we
will now slowly add some players. We do this by sending a
[:other-counters name] message for a player with a score of 0.
(defn add-player [name input-queue]
(p/put-message input-queue {msg/type :swap
msg/topic [:other-counters name]
:value 0}))
(defrecord MockServices [app]
p/Activity
(start [this]
(platform/create-timeout 10000 #(add-player "abc" (:input app)))
(platform/create-timeout 15000 #(add-player "xyz" (:input app))))
(stop [this]))Once again, check this behavior in the Data UI.
As we did in the previous section we will now move the wait page to
its own screen. We make the following changes in the
tutorial-client.behavior namespace.
Change the :set-focus message in the init-login emitter to change
focus to the :wait page.
(defn init-login [_]
[{:login
{:name
{:transforms {:swap [{msg/topic [:login :name] (msg/param :value) {}}
{msg/topic msg/app-model msg/type :set-focus :name :wait}]}}}}])Update the :focus section of the dataflow definition, giving :wait
its own name..
:focus {:login [[:login]]
:wait [[:wait]]
:game [[:game] [:pedestal]]
:default :login}We need to decide how to transition from the wait screen to the game
screen. The easy thing to do would be to add a :set-focus to the
:start-game transform. When we click the Start button, the game
would start.
But we would also like to navigate to the game page when someone else
clicks the Start button. Currently the :start-game transform
will do two things: it changes [:active :game] to true in our data
model and then sends a message to everyone else to set their [:active :game] value to true.
There are two conditions in the data model which, if met, should cause
us to navigate to the :game screen:
- the game is active and login name was just set
- login name is set and the game has just become active
The start-game function below will send a :set-focus message when
either of these conditions are met.
(defn start-game [inputs]
(let [old-active (-> inputs :old-model :active-game)
new-active (-> inputs :new-model :active-game)
old-login (-> inputs :old-model :login :name)
new-login (-> inputs :new-model :login :name)]
(when (or (and new-login (not old-active) new-active)
(and new-active (not old-login) new-login))
[^:input {msg/topic msg/app-model msg/type :set-focus :name :game}])))This is a Continue function. Continue functions are used to generate new messages to be processed by the dataflow engine. These functions return a sequence of messages. The default behavior of continue is to take the messages that it produces and run each of them through the dataflow engine until no messages are returned. This is done before effects and emitter deltas are generated.
In this example, we do something a bit different, :input metadata is
used to tag a message as something which should escape the dataflow
and be put on the input queue. :set-focus messages are handled by
the app engine before they get to the dataflow engine, so they must be
put on the input queue.
To configure a continue function, add a continue keyword to the dataflow definition.
:continue #{[#{[:login :name] [:active-game]} start-game]}The process of rendering something should now be familiar.
Add a link to tools/public/design.html for the new wait template.
<li><a href="/design/wait.html">Wait</a></li>Create the page app/templates/wait.html. This HTML contains two
templates, the main template for the wait page and the template for
each player.
<_within file="application.html">
<div id="content">
<div class="row-fluid" template="wait" field="id:id">
<div>
<button class="btn btn-success" id="start-button">Start Game</button>
</div>
<h2>Current Players</h2>
<div id="players">
<div template="player" class="player-row" field="id:id,content:player-name">Feanor</div>
<div class="player-row">Fingolfin</div>
<div class="player-row">Morgoth</div>
<div class="player-row">Morwen</div>
</div>
<!-- this is required because of a bug -->
<div field="content:something"></div>
</div>
</div>
</_within>Make some minor modifications to the CSS in
app/assets/stylesheets/tutorial-client.css.
#players {
margin-top: 20px;
}
.player-row {
font-size: 30px;
font-weight: bold;
padding-left: 10px;
margin-top: 20px;
color: #888;
}Slice the template, getting the two templates from wait.html.
(defmacro tutorial-client-templates
[]
{:tutorial-client-page (dtfn (tnodes "game.html" "tutorial") #{:id})
:login-page (tfn (tnodes "login.html" "login"))
:wait-page (dtfn (tnodes "wait.html" "wait" [[:#players]]) #{:id})
:player (tfn (tnodes "wait.html" "player"))})Remember to restart the development application after making changes
to html_templates.clj.
Rendering the wait page in tutorial-client.renderer involves adding
two templates to the DOM and hooking up the Start button
event. This is all code which we have seen before.
(defn add-wait-template [renderer [_ path :as delta] input-queue]
(let [parent (render/get-parent-id renderer path)
id (render/new-id! renderer path)
html (templates/add-template renderer path (:wait-page templates))]
(dom/append! (dom/by-id parent) (html {:id id}))))
(defn add-waiting-player [renderer [_ path :as delta] input-queue]
(let [parent (render/new-id! renderer (vec (butlast path)) "players")
id (render/new-id! renderer path)
html (:player templates)]
(dom/append! (dom/by-id parent) (html {:id id :player-name (last path)}))))Add the following to render-config.
[:node-create [:wait] add-wait-template]
[:node-destroy [:wait] h/default-destroy]
[:transform-enable [:wait :start] (h/add-send-on-click "start-button")]
[:transform-disable [:wait :start] (h/remove-send-on-click "start-button")]
[:node-create [:wait :players :*] add-waiting-player]
[:node-destroy [:wait :players :*] h/default-destroy]
Remember that add-send-on-click and remove-send-on-click will work
in this case because the messages don't require any parameters.
Once a game has started, it will go on forever. In the next section we will cause the game to stop and declare a winner.
The tag for this step is step16.