Skip to content
Brenton Ashworth edited this page Jun 28, 2013 · 10 revisions

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

Adding a waiting screen

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.

Updating the simulator

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.

Adding players in the waiting room

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.

Moving wait to its own screen

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}

Using a Continue function to Set focus based on state

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]}

Rendering the Wait screen

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.

Next steps

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.

Home | End a Game

Clone this wiki locally