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:
startbegins a game for whoever plays it.- Listening: a player says how many they take, to the pile.
winsreads out the table.
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:
{: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 self "There's no game of yours on. Start one with start stone pile.")The whole toy #
(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.")))))))))})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:
> 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:
> 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 #
- Plan the state first.
{:games {} :wins {}}, keyed bywho, let two players play at once without a line of extra code. - Small named helpers.
wonandoverare functions named in thelet, so each branch reads as what it does. - Play it as a stranger would. Mira's "4" and "I'll take 2" were checked: a toy that takes words should expect any words.
- Refuse what the rules don't allow, so nothing half-done is kept.
Try it #
- Let a player choose the size of the pile:
startgives 15, and a verbstart-biggives 21. - Make the pile say how many games it has played in all, kept in its state.
- Write a coin-guessing game:
flippicks heads or tails at random and keeps it for that player; saying "heads" or "tails" to the toy says whether they were right. - Make
winslist the winners best first, withsort-by.
Recap #
- A game keeps a state for each player, in a map keyed by
who. - Words said to it carry the moves; verbs start, report and stop.
refusesays no and keeps nothing.- Functions named in a
letkeep a long toy readable.
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.