Tic-Tac-Toe
Tic-Tac-Toe demo built with Expressive MVC - the complete source below runs as an editable sandbox when JavaScript is enabled.
App.tsx
import './App.css';
import { Component, has } from '@expressive/react';
const LINES = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
];
class Game extends Component {
board = has<string>(Array(9).fill(''));
turn: 'X' | 'O' = 'X';
get winner() {
const { board } = this;
for (const line of LINES) {
const [a, b, c] = line.map((i) => board.get(i));
if (a && a === b && b === c) return { player: a, line };
}
}
get full() {
return [...this.board].every(Boolean);
}
play(i: number) {
if (this.board.get(i) || this.winner) return;
this.board.set(i, this.turn);
this.turn = this.turn === 'X' ? 'O' : 'X';
}
reset() {
for (let i = 0; i < 9; i++) this.board.set(i, '');
this.turn = 'X';
}
render() {
const { board, turn, winner, full, play, reset } = this;
return (
<div className="container">
<h1>Tic Tac Toe</h1>
<p className="status">
{winner ? `${winner.player} wins!` : full ? 'Draw!' : `${turn}'s turn`}
</p>
<div className={`board ${winner && 'done'}`}>
{board.map((cell, i) => (
<button
key={i}
onClick={() => play(i)}
className={`${cell} ${winner?.line.includes(i) ? 'wins' : ''}`}>
{cell}
</button>
))}
</div>
<button onClick={reset}>New game</button>
</div>
);
}
}
export default Game;
App.css
.status {
font-size: var(--t-lg);
color: var(--muted);
margin: 0;
}
/* Win highlight is local to this example - the only color it adds. */
.board {
--win: #16a34a;
--win-soft: #16a34a18;
display: grid;
grid-template-columns: repeat(3, 4rem);
grid-template-rows: repeat(3, 4rem);
gap: 6px;
margin: 0;
}
.board > button {
margin: 0;
padding: 0;
width: 4rem;
height: 4rem;
font-size: var(--t-2xl);
font-weight: 700;
color: var(--fg-soft);
border-radius: var(--r-sm);
cursor: pointer;
transition: background-color 0.2s;
}
/* Empty cells invite a move; decided cells don't react to hover. */
.board > button:hover {
background-color: var(--accent-soft);
}
.board > button.X {
color: var(--accent);
background: var(--bg);
cursor: default;
}
.board > button.O {
color: var(--muted);
background: var(--bg);
cursor: default;
}
.board > button.wins {
background: var(--win-soft);
border-color: var(--win);
color: var(--win);
cursor: default;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) .board {
--win: #22c55e;
--win-soft: #22c55e22;
}
}
:root[data-theme='dark'] .board {
--win: #22c55e;
--win-soft: #22c55e22;
}