Advent of SvelteKit 2022

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];
	}
}