Part II. Making things · this page as markdown

A small game

A game is a toy with rules, a state for each player, and a way to say no. This chapter builds one from start to finish: a pile of stones you play against, where each player has a game of their own and the stone pile keeps a table of wins.

The rules #

Fifteen stones. You and the pile take turns; on each turn you take one, two or three. Whoever takes the last stone wins. There is a way to win every time, if you go second and know it, and the pile knows it: after its turn it always tries to leave a multiple of four.

That gives the toy three jobs:

Each player's own game #

Two visitors can be playing at once, so the state keeps a game for each player, in a map from who to the stones left in their game, and a map of wins:

#game-state
{:games {"@mira" 11}
 :wins {"@mira" 0 "pile" 2}}

Planning the state first, as a small map with a key for each thing to remember, makes the rest of the toy easy to write: each verb reads one or two keys and puts one or two back.

Saying no #

refuse is say for a verb that won't do what was asked. Its line is said, and nothing the verb did is kept: no put! stands. In a chat app, the reply is marked as not done. Use it when a player asks for something the rules don't allow:

#refuse-example
(refuse self "There's no game of yours on. Start one with start stone pile.")

The whole toy #

#stone-pile
(defobject stone-pile
  {:name "Stone pile"
   :state {:games {} :wins {}}
   :verbs {:start (fn [self who]
                    (put! self :games (assoc (:games self) who 15))
                    (say self "Fifteen stones. Take 1, 2 or 3 a turn; whoever takes the last wins. Say to the stone pile how many you take."))
           :wins (fn [self who]
                   (let [table (:wins self)]
                     (if (empty? table)
                       (say self "Nobody has won yet.")
                       (say self (join ", " (map (fn [[k v]] (str k ": " v)) table))))))
           :give-up (fn [self who]
                      (if (contains? (:games self) who)
                        (do (put! self :games (dissoc (:games self) who))
                            (say self "The stones go back in a heap."))
                        (refuse self "There's no game of yours on.")))}
   :on-hear (fn [self who text]
              (when (:spoken-to self)
                (let [left (get (:games self) who)
                      n (parse-long (or (re-find #"[0-9]+" text) ""))
                      won (fn [k] (put! self :wins (update (:wins self) k (fnil inc 0))))
                      over (fn [] (put! self :games (dissoc (:games self) who)))]
                  (cond
                    (nil? left) (say self "There's no game of yours on. Start one with start stone pile.")
                    (not (and n (<= 1 n 3) (<= n left))) (say self (str "Take 1, 2 or 3, and no more than the " left " left."))
                    (= n left) (do (won who) (over) (say self "You take the last stone. You win."))
                    :else (let [after-you (- left n)
                                m (mod after-you 4)
                                mine (if (zero? m) 1 m)
                                after-me (- after-you mine)]
                            (if (zero? after-me)
                              (do (won "pile") (over)
                                  (say self (str "You take " n "; " after-you " left. I take the last " mine ". I win.")))
                              (do (put! self :games (assoc (:games self) who after-me))
                                  (say self (str "You take " n "; " after-you " left. I take " mine "; " after-me " left.")))))))))})
It answers
The stone pile is ready.

Read it from the bottom up. When a player takes some stones, the pile works out what's left (after-you), and takes enough to leave a multiple of four, or one stone when it can't. If that empties the pile, the pile has won. won adds a win to the table, and over ends that player's game; both are small functions named in the let, so each branch of the cond reads as what it does.

Mira plays, and loses:

Played
> start stone pile
[Juniper's stone pile] Fifteen stones. Take 1, 2 or 3 a turn; whoever takes the last wins. Say to the stone pile how many you take.
> say to the stone pile: I'll take 2
[Juniper's stone pile] You take 2; 13 left. I take 1; 12 left.
> say to the stone pile: 4
[Juniper's stone pile] Take 1, 2 or 3, and no more than the 12 left.
> say to the stone pile: 3
[Juniper's stone pile] You take 3; 9 left. I take 1; 8 left.
> say to the stone pile: 3
[Juniper's stone pile] You take 3; 5 left. I take 1; 4 left.
> say to the stone pile: 1
[Juniper's stone pile] You take 1; 3 left. I take the last 3. I win.

Then she works out the trick. Going first from 15, a player who leaves the pile a multiple of four each time wins instead:

Played
> start stone pile
[Juniper's stone pile] Fifteen stones. …
> say to the stone pile: 3
[Juniper's stone pile] You take 3; 12 left. I take 1; 11 left.
> say to the stone pile: 3
[Juniper's stone pile] You take 3; 8 left. I take 1; 7 left.
> say to the stone pile: 3
[Juniper's stone pile] You take 3; 4 left. I take 1; 3 left.
> say to the stone pile: 3
[Juniper's stone pile] You take the last stone. You win.
> wins stone pile
[Juniper's stone pile] pile: 1, Mira: 1
> give-up stone pile
[Juniper's stone pile] There's no game of yours on.

The last line was a refusal: give-up with no game on said no, and kept nothing. And "@mira" in the table of wins was said aloud as her name.

What the game teaches #

Try it #

  1. Let a player choose the size of the pile: start gives 15, and a verb start-big gives 21.
  2. Make the pile say how many games it has played in all, kept in its state.
  3. Write a coin-guessing game: flip picks heads or tails at random and keeps it for that player; saying "heads" or "tails" to the toy says whether they were right.
  4. Make wins list the winners best first, with sort-by.

Recap #

Next: pictures and dances.

Written by Claude, Plazmi's writer, for Plazmi, a service of Manatee Media LLC. Text © 2026 Manatee Media LLC. The examples are free to copy, change and run.