Tic-tac-toe
It's O's turn
Challenge
Source code
+page.svelte
<script lang="ts">
import { Move, checkWinner, State } from './util';
import Icon from './Icon.svelte';
import EmptyCell from './EmptyCell.svelte';
import { tick } from 'svelte';
let board = getEmptyBoard();
let turn = Move.O;
let state = State.Playing;
let boardEl: HTMLElement, statusEl: HTMLElement;
function place(row: number, col: number) {
board[row][col] = turn;
turn = turn === Move.O ? Move.X : Move.O;
tick().then(focusNextAvailableTile);
}
function focusNextAvailableTile() {
const nextTile = boardEl.querySelector('button:not(:disabled)');
if (nextTile) {
(nextTile as HTMLElement).focus();
} else {
statusEl.focus();
}
}
$: winner = checkWinner(board);
$: state = getGameState(winner, board);
function reset() {
board = getEmptyBoard();
tick().then(focusNextAvailableTile);
}
function getEmptyBoard() {
return [
[Move.Empty, Move.Empty, Move.Empty],
[Move.Empty, Move.Empty, Move.Empty],
[Move.Empty, Move.Empty, Move.Empty]
];
}
function getGameState(winner: Move | undefined, board: Move[][]) {
if (winner) {
return State.Won;
} else if (board.every((row) => row.every((col) => col !== Move.Empty))) {
return State.Draw;
} else {
return State.Playing;
}
}
</script>
<h1>Tic-tac-toe</h1>
<div class="board" bind:this={boardEl}>
{#each board as row, r}
{#each row as col, c}
<div class="cell">
{#if col !== Move.Empty}
<Icon move={col} />
{:else}
<EmptyCell on:click={() => place(r, c)} disabled={state !== State.Playing}>
<span class="visually-hidden">Place row {r + 1} column {c + 1}</span>
</EmptyCell>
{/if}
</div>
{/each}
{/each}
</div>
<div class="status" bind:this={statusEl} tabindex="-1">
{#if state === State.Won}
{winner} won.
{:else if state === State.Draw}
It's a draw!
{:else}
It's {turn}'s turn
{/if}
</div>
{#if state !== State.Playing}
<button on:click={reset}>Play again?</button>
{/if}
<style>
.board {
display: grid;
grid-template-columns: repeat(3, minmax(auto, 100px));
grid-template-rows: repeat(3, auto);
gap: 0.25rem;
background: var(--gray-4);
}
.cell {
aspect-ratio: 1/1;
background: white;
padding: 0.5rem;
height: 100px;
}
.status {
font-size: var(--size-4);
}
button {
background-color: var(--blue-3);
border: none;
border-radius: var(--radius-2);
padding: 0.25rem 1rem;
cursor: pointer;
font-size: var(--size-5);
color: black;
}
button:hover {
transform: translateY(2px);
}
button:active {
transform: scale(0.95);
transition: transform 0.3s;
}
</style>
+page.ts
export const prerender = true;
EmptyCell.svelte
<script>
export let disabled = false;
</script>
<button on:click {disabled}>
<slot />
</button>
<style>
button {
width: 100%;
height: 100%;
appearance: none;
border: none;
background: none;
border-radius: var(--border-size-3);
}
button:hover,
button:focus-visible {
background-color: var(--gray-2);
box-shadow: var(--shadow-3);
cursor: pointer;
}
button:focus-visible {
outline: solid var(--svelte);
}
button:disabled {
cursor: not-allowed;
}
</style>
Icon.svelte
<script lang="ts">
import { Move } from './util';
export let move: Move;
</script>
{#if move === Move.X}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{/if}
<span class="visually-hidden">
{#if move === Move.X}
X
{:else}
O
{/if}
</span>
util.ts
export enum Move {
X = 'X',
O = 'O',
Empty = ''
}
export enum State {
Playing,
Draw,
Won
}
export function checkWinner(board: Move[][]) {
for (const row of board) {
if (row.every((v) => v === Move.X) || row.every((v) => v === Move.O)) {
return row[0];
}
}
for (let i = 0; i < board[0].length; i++) {
if (board[0][i] && board[0][i] === board[1][i] && board[1][i] === board[2][i]) {
return board[0][i];
}
}
if (board[1][1] === Move.Empty) {
return;
}
if (board[0][0] === board[1][1] && board[1][1] == board[2][2]) {
return board[0][0];
}
if (board[0][2] && board[0][2] === board[1][1] && board[1][1] == board[2][0]) {
return board[0][2];
}
}