Skip to content

UI State Tic Tac Toe

A full game in three stores, with no JavaScript on the page. $cells holds nine numbers: 1 for X, -1 for O, and 0 for free. The square is an sv-template stamped nine times.

A line sums to 3 or -3 only when one player holds all of it, so a $peak of 3 means someone has won.

Code

The following example assumes that you have imported the component and set up the theme. This one needs sv-template as well.

Tic Tac Toe
<!-- One square, stamped nine times. [[ i ]] is its seat in the board array -->
<sv-template id="ttt-square">
10 collapsed lines
<template>
<button
type="button"
bind-attr-toggle="disabled:$cells.at([[ i ]]) !== 0 || $winner !== 0"
bind-class-toggle="is-o:$cells.at([[ i ]]) === -1"
bind-event="click:$cells($cells.with([[ i ]], $turn)), $turn(-$turn), $moves($moves + 1)"
bind-text="$cells.at([[ i ]]) === 0 ? '' : $cells.at([[ i ]]) === 1 ? 'X' : 'O'"
template-for="i of 9"
></button>
</template>
</sv-template>
<sv-ui-state>
49 collapsed lines
<!-- X plays 1, O plays -1, an empty square is 0 -->
<sv-store type="json" name="$cells" value="[0,0,0, 0,0,0, 0,0,0]"></sv-store>
<sv-store type="number" name="$turn" value="1"></sv-store>
<sv-store type="number" name="$moves" value="0"></sv-store>
<!-- A line can only reach 3 or -3 when one player holds all three of it -->
<sv-store
derive="
Math.max(
Math.abs($cells[0] + $cells[1] + $cells[2]),
Math.abs($cells[3] + $cells[4] + $cells[5]),
Math.abs($cells[6] + $cells[7] + $cells[8]),
Math.abs($cells[0] + $cells[3] + $cells[6]),
Math.abs($cells[1] + $cells[4] + $cells[7]),
Math.abs($cells[2] + $cells[5] + $cells[8]),
Math.abs($cells[0] + $cells[4] + $cells[8]),
Math.abs($cells[2] + $cells[4] + $cells[6])
)
"
name="$peak"
></sv-store>
<!-- Only the player who just moved can have won, and $turn has already flipped -->
<sv-store derive="$peak === 3 ? -$turn : 0" name="$winner"></sv-store>
<sv-store
derive="
$winner !== 0
? ($winner === 1 ? 'X' : 'O') + ' wins'
: $moves === 9 ? 'Draw' : ($turn === 1 ? 'X' : 'O') + ' to play'
"
name="$status"
></sv-store>
<div class="game">
<span class="game-status" bind-class-toggle="is-over:$winner !== 0 || $moves === 9" bind-text="$status"></span>
<div class="game-board">
<sv-template use="ttt-square"></sv-template>
</div>
<button
type="button"
class="game-reset"
bind-attr-toggle="disabled:$moves === 0"
bind-event="click:$cells(new Array(9).fill(0)), $turn(1), $moves(0)"
>
New game
</button>
</div>
</sv-ui-state>
<style>
69 collapsed lines
.game {
display: grid;
gap: 0.75em;
justify-items: center;
inline-size: min(15rem, 100%);
padding-block: 1em;
margin-inline: auto;
.game-status {
font-size: 0.9em;
color: var(--sv-gray-3);
white-space: nowrap;
&.is-over {
color: var(--sv-accent);
}
}
.game-board {
display: grid;
grid-template-rows: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.3em;
inline-size: 100%;
aspect-ratio: 1;
button {
box-sizing: border-box;
padding: 0;
font: inherit;
font-size: 1.6em;
line-height: 1;
color: var(--sv-accent);
cursor: pointer;
background-color: var(--sv-gray-6);
border: none;
border-radius: var(--sv-bdr-rad-sm);
&.is-o {
color: var(--sv-gray-2);
}
&:disabled {
cursor: default;
}
&:hover:not(:disabled) {
background-color: var(--sv-gray-5);
}
}
}
.game-reset {
padding: 0.4em 1.1em;
font: inherit;
font-size: 0.85em;
color: var(--sv-gray-1);
cursor: pointer;
background-color: var(--sv-gray-5);
border: none;
border-radius: var(--sv-bdr-rad-sm);
&:disabled {
color: var(--sv-gray-4);
cursor: not-allowed;
background-color: transparent;
}
}
}
</style>