<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>WebDevPlayground</title>
	<atom:link href="https://webdevplayground.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://webdevplayground.com</link>
	<description>Thoughts, tutorials, and experiments from WDP</description>
	<lastBuildDate>Wed, 14 Oct 2026 06:21:31 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.3</generator>

<image>
	<url>https://webdevplayground.com//uploads/2026/09/site-icon-150x150.png</url>
	<title>WebDevPlayground</title>
	<link>https://webdevplayground.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>How To: Add AI to our jQuery Tic-Tac-Toe</title>
		<link>https://webdevplayground.com/tutorials/jquery-tic-tac-toe-with-ai/</link>
					<comments>https://webdevplayground.com/tutorials/jquery-tic-tac-toe-with-ai/#respond</comments>
		
		<dc:creator><![CDATA[adi]]></dc:creator>
		<pubDate>Wed, 14 Oct 2026 08:01:20 +0000</pubDate>
				<category><![CDATA[Games]]></category>
		<category><![CDATA[Javascript]]></category>
		<category><![CDATA[Algorithm]]></category>
		<category><![CDATA[jQuery]]></category>
		<category><![CDATA[Minmax]]></category>
		<guid isPermaLink="false">https://webdevplayground.com/?post_type=tutorial&#038;p=327</guid>

					<description><![CDATA[<p>Previously, in the tic-tac-toe series, we programmed a simple game of tic-tac-toe. The version we built reminded me of two-player games in the early ’90s, when friends sat at the same computer to play. However, since playing with a friend is not always possible, it’s time to add a computer opponent to the mix. At [&#8230;]</p>
<p>The post <a href="https://webdevplayground.com/tutorials/jquery-tic-tac-toe-with-ai/">How To: Add AI to our jQuery Tic-Tac-Toe</a> first appeared on <a href="https://webdevplayground.com">WebDevPlayground</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Previously, in the tic-tac-toe series, we programmed a simple game of <a href="/tutorials/creating-a-tic-tac-toe-game-with-jquery/">tic-tac-toe</a>. The version we built reminded me of two-player games in the early ’90s, when friends sat at the same computer to play. However, since playing with a friend is not always possible, it’s time to add a computer opponent to the mix.<span id="more-327"></span> At the very least, now I won’t feel weird playing both sides myself.</p>
<p>This tutorial starts where the previous one finished. We will keep the adjustable board, SVG tokens, score counter, and two-human-player option. Instead of rebuilding all of that, we will focus on the code that changed: choosing an AI opponent, finding good moves, giving the AI several difficulty levels, and adding a little interface polish so the computer feels like part of the game.</p>
<h2 id="requirements">What are we adding?</h2>
<p>Before we begin, we need to know what we are actually adding to the game. In other words, what do we need to code so the new features work as expected? This also gives us a small task list to follow, which is much nicer than wandering into an AI algorithm armed only with optimism.</p>
<p>Our basic requirements:</p>
<ul>
<li><a href="#simple-ai-opponent">Player 2 can also be a simple AI</a></li>
<li><a href="#difficulty-without-the-always-win-or-tie-trap">Don’t fall into the “always win or tie” trap</a></li>
<li><a href="#interface-for-a-human-or-ai-opponent">Extend the interface to allow the new features</a></li>
<li><a href="#some-extra-polish-to-make-it-feel-good">Add some extra polish to make it feel good</a></li>
</ul>
<p>Some of those requirements can look a bit daunting, but we’ll tackle them one by one. We won’t repeat every ordinary HTML or CSS declaration from the first tutorial. The AI logic deserves a closer look, though, and we’ll cover every new JavaScript responsibility even when a few related lines are grouped into one snippet.</p>
<p>I&#8217;m getting a bit ahead of myself—you might be wondering if you need to know anything special before continuing. Don’t worry. Read the previous <a href="/tutorials/creating-a-tic-tac-toe-game-with-jquery/">tutorial</a>, and if its arrays, functions, and jQuery event handlers make sense, you are in a good place to tackle this one. Recursion will be our one big new idea, and we’ll take it slowly.</p>
<p>If you want to check out the final result, you can <a href="/demos/jquery-tic-tac-toe-with-ai/">play the demo</a> and see it all together. A full download of the source code is also available <a href="/demos/jquery-tic-tac-toe-with-ai/jquery-tic-tac-toe-with-ai.zip?x55466">here</a>.</p>
<p>Since we are only changing three first-party files in this version—<code>index.html</code>, <code>style.css</code>, and <code>tic_tac_toe.js</code>—you can copy the folder from the previous tutorial and edit it. Forgot where it is? No worries; you can <a href="/demos/creating-a-tic-tac-toe-game-with-jquery/creating-a-tic-tac-toe-game-with-jquery.zip?x55466">download the original project</a>. The jQuery file and <code>token-icons.svg</code> do not change.</p>
<h2 id="interface-for-a-human-or-ai-opponent">An interface for a human or AI opponent</h2>
<p>Player 1 stays human, while the old Player 2 card becomes an opponent card. It starts with two radio buttons: <strong>Human</strong> and <strong>AI</strong>. The human choice reveals the familiar name and token controls; the AI choice reveals a difficulty list instead.</p>
<h3 id="opponent-controls">Switching the opponent controls</h3>
<p>This is the meaningful new part of the Player 2 form. The full, slightly repetitive token picker is already in <code>index.html</code> inside the downloadable ZIP, so we can safely spare ourselves another wall of four nearly identical SVG labels.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="html">&lt;fieldset&gt;
	&lt;legend&gt;Opponent type&lt;/legend&gt;
	&lt;div class="choice-picker"&gt;
		&lt;label&gt;
			&lt;input type="radio" name="opponent-type" value="human" /&gt;
			&lt;span&gt;Human&lt;/span&gt;
		&lt;/label&gt;
		&lt;label&gt;
			&lt;input type="radio" name="opponent-type" value="ai" checked /&gt;
			&lt;span&gt;AI&lt;/span&gt;
		&lt;/label&gt;
	&lt;/div&gt;
&lt;/fieldset&gt;

&lt;div class="human-settings" id="human-settings" hidden&gt;
	&lt;label for="player-2-name"&gt;Display name&lt;/label&gt;
	&lt;input
		id="player-2-name"
		name="name"
		type="text"
		value="Player 2"
		maxlength="20"
	/&gt;

	&lt;!-- The existing Player 2 token picker goes here. --&gt;
&lt;/div&gt;

&lt;div class="ai-settings" id="ai-settings"&gt;
	&lt;label for="ai-difficulty"&gt;AI difficulty&lt;/label&gt;
	&lt;select id="ai-difficulty" name="difficulty"&gt;
		&lt;option value="easy"&gt;Easy&lt;/option&gt;
		&lt;option value="normal" selected&gt;Normal&lt;/option&gt;
		&lt;option value="hard"&gt;Hard&lt;/option&gt;
		&lt;option value="expert"&gt;Expert&lt;/option&gt;
	&lt;/select&gt;
	&lt;p class="field-help"&gt;
		The AI chooses its own name and available token.
	&lt;/p&gt;
&lt;/div&gt;</pre>
<p>The two settings groups always exist in the page; JavaScript only changes which one is available. Importantly, it disables the hidden group as well as hiding it:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">function toggleOpponentSettings() {
	const isAi =
		$('#player-2-form [name="opponent-type"]:checked').val() === 'ai';

	$('#human-settings')
		.prop('hidden', isAi)
		.find('input')
		.prop('disabled', isAi);
	$('#ai-settings')
		.prop('hidden', !isAi)
		.find('select')
		.prop('disabled', !isAi);
	$('#player-2-form .button--player').text(
		isAi ? 'Set AI opponent' : 'Set Player 2'
	);
	updateTokenAvailability();
}</pre>
<p>Doing this instead of only adding a CSS class has two benefits. Hidden controls cannot accidentally participate in validation, and a keyboard user cannot tab into controls they cannot see. The button text changes too, so the action still makes sense in either mode.</p>
<h3 id="new-game-state">Giving our game a little more state</h3>
<p>At the top of <code>tic_tac_toe.js</code>, we give the two array positions readable names and add the settings the AI will need:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">const humanPlayerIndex = 0;
const aiPlayerIndex = 1;
const lossesBeforeDifficultySuggestion = 2;
const winsBeforeDifficultySuggestion = 3;
const allowedIcons = ['hat', 'car', 'ship', 'boot'];
const boardSizeClasses =
	'board-size-3 board-size-4 board-size-5 board-size-6';
const currentPlayerClasses = 'current-player-one current-player-two';

// A short, gender-neutral name pool gives the AI a little personality.
const aiNames = [
	'Alex',
	'Avery',
	'Casey',
	'Jordan',
	'Morgan',
	'Parker',
	'Quinn',
	'Riley',
	'Skyler',
	'Taylor',
];

// Each difficulty controls how often the AI uses a calculated move.
const difficulties = {
	easy: { label: 'Easy', calculatedMoveChance: 0 },
	normal: { label: 'Normal', calculatedMoveChance: 0.5 },
	hard: { label: 'Hard', calculatedMoveChance: 0.85 },
	expert: { label: 'Expert', calculatedMoveChance: 1 },
};
const difficultyOrder = ['easy', 'normal', 'hard', 'expert'];

// Searching every possible move is quick on a 3 × 3 board. Bigger boards
// use fewer search levels so the browser stays responsive.
const searchDepthByBoardSize = {
	3: 9,
	4: 4,
	5: 3,
	6: 3,
};</pre>
<p><code>humanPlayerIndex</code> is nicer to read than a mysterious <code>0</code> inside an algorithm. We also changed <code>allowedIcons</code> from a <code>Set</code> to an array. A set was handy when we only asked “is this icon allowed?”, but an array can answer that with <code>includes()</code> <em>and</em> can be filtered when the AI needs a random unused icon.</p>
<p>Each player now records whether it is controlled by the AI and, if so, its difficulty:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">const players = [
	{
		name: 'Player 1',
		icon: 'hat',
		wins: 0,
		isReady: false,
		isAi: false,
		difficulty: null,
	},
	{
		name: 'Player 2',
		icon: 'car',
		wins: 0,
		isReady: false,
		isAi: false,
		difficulty: 'normal',
	},
];

let boardSize = 3;
let board = [];
let winningLines = [];
let currentPlayer = humanPlayerIndex;
let roundIsOver = false;
let aiTimer;
let humanWinStreak = 0;
let humanLossStreak = 0;
let showDifficultySuggestions = true;
let suggestedDifficulty = null;
let suggestionDirection = 0;
let pendingPlayerForm = null;
let confirmedTokenChangeForm = null;</pre>
<p>That looks like quite a shopping list, but each variable has one job. <code>winningLines</code> avoids rebuilding the same rows, columns, and diagonals during every step of the AI search. The timer controls the short “thinking” pause. The streak and suggestion variables belong to the difficulty helper, and the final two variables let us confirm a token change without losing track of which form started it.</p>
<h3 id="saving-human-or-ai-settings">Saving either kind of opponent</h3>
<p>The original <code>setPlayer()</code> only knew about a name and icon. The new version first decides which branch of the form is active. AI settings validate the difficulty; human settings keep the previous name and token checks.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">// Save one player's form and update the visible summary.
function setPlayer(event) {
	event.preventDefault();

	const $form = $(this);
	const playerIndex = Number($form.attr('data-player'));
	const otherPlayerIndex = (playerIndex + 1) % players.length;
	const player = players[playerIndex];
	const isAi =
		playerIndex === aiPlayerIndex &amp;&amp;
		$form.find('[name="opponent-type"]:checked').val() === 'ai';
	const $message = $form.find('.form-message');
	let name = null;
	let icon = null;
	let difficulty = null;

	$message.text('');

	if (isAi) {
		difficulty = String($form.find('[name="difficulty"]').val());

		if (!Object.hasOwn(difficulties, difficulty)) {
			$message.text('Please choose an AI difficulty.');
			return;
		}
	} else {
		name = String($form.find('[name="name"]').val()).trim();
		icon = String($form.find('[name="icon"]:checked').val());

		if (!name) {
			$message.text('Please enter a display name.');
			$form.find('[name="name"]').trigger('focus');
			return;
		}

		if (!allowedIcons.includes(icon)) {
			$message.text('Please choose a token icon.');
			return;
		}

		if (
			players[otherPlayerIndex].isReady &amp;&amp;
			players[otherPlayerIndex].icon === icon
		) {
			$message.text(
				'Please choose a token the other player is not using.'
			);
			updateTokenAvailability();
			return;
		}
	}

	// The token-change confirmation is added in the polish section below.

	if (isAi) {
		player.name = pickRandom(aiNames);
		player.icon = getRandomAiIcon();
		player.difficulty = difficulty;
	} else {
		player.name = name;
		player.icon = icon;
	}

	player.isAi = isAi;
	player.isReady = true;
	updateTokenAvailability();

	if (playerIndex === aiPlayerIndex) {
		humanWinStreak = 0;
		humanLossStreak = 0;
	}

	// Reset the board before the updated setup appears in the summary.
	startNewRound();
	updatePlayerSummary(playerIndex);

	$form
		.find('.button--player')
		.text(
			playerIndex === aiPlayerIndex &amp;&amp; isAi
				? 'Pick a new AI opponent'
				: `Update Player ${playerIndex + 1}`
		);
	$form
		.closest('.player-card')
		.find('.player-settings')
		.prop('open', false);
}</pre>
<p><code>Object.hasOwn(difficulties, difficulty)</code> checks the submitted value against our real difficulty table instead of trusting that nobody changed the HTML in the browser’s developer tools. The same idea protected the icon name in the first game.</p>
<p>An AI receives a random name and a token that Player 1 is not using:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">function pickRandom(items) {
	return items[Math.floor(Math.random() * items.length)];
}

function getRandomAiIcon() {
	const otherPlayer = players[humanPlayerIndex];
	const availableIcons = allowedIcons.filter(
		(icon) =&gt; !otherPlayer.isReady || icon !== otherPlayer.icon
	);

	return pickRandom(availableIcons);
}</pre>
<p>Keeping <code>pickRandom()</code> generic pays off twice: it chooses cosmetic names and icons here, then later chooses moves. A tiny helper with three jobs is better than copying the same <code>Math.floor(Math.random() * ...)</code> spell everywhere and eventually mistyping one of them.</p>
<p>The summary function now also describes the opponent type:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">function updatePlayerSummary(playerIndex) {
	const player = players[playerIndex];
	const $playerCard = $(`.player-card:eq(${playerIndex})`);
	const $summary = $playerCard.find('.player-summary');

	$summary.find('.summary-token').empty().append(createIcon(player.icon));
	$summary.find('.summary-name').text(player.name);
	$summary.find('.win-count').text(player.wins);

	if (playerIndex === aiPlayerIndex) {
		const details = player.isAi
			? `AI · ${difficulties[player.difficulty].label} difficulty`
			: 'Human opponent';

		$summary.find('.player-details').text(details);
	}

	$summary.prop('hidden', false);
}</pre>
<h2 id="simple-ai-opponent">A simple Tic-Tac-Toe AI</h2>
<p>Our AI is not a trained model and it does not secretly call a server. It is a small search algorithm running entirely in the browser. It imagines a move, imagines our reply, imagines its next reply, and scores the result. Very thoughtful for something that just received a racing-car token.</p>
<p>Before we reach that algorithm, both humans and computers need to use the same game rules.</p>
<h3 id="one-move-pipeline">One move pipeline for everybody</h3>
<p>The first version’s click handler both selected a cell and performed the whole move. That no longer works because an AI does not click. We split the work into three layers:</p>
<ul>
<li><code>playHumanMove()</code> reads the clicked cell;</li>
<li><code>playAiTurn()</code> chooses a cell with code;</li>
<li>both call <code>playMoveAt()</code>, which applies the shared win, draw, score, and turn rules.</li>
</ul>
<pre class="EnlighterJSRAW" data-enlighter-language="js">function markCell(cellIndex, playerIndex) {
	const player = players[playerIndex];
	const $cell = $(`#game-board .cell[data-index="${cellIndex}"]`);

	board[cellIndex] = playerIndex;
	$cell
		.append(createIcon(player.icon))
		.addClass(`is-marked player-${playerIndex === 0 ? 'one' : 'two'}`)
		.attr('aria-label', `${player.name} marked this square`)
		.attr('aria-disabled', 'true')
		.prop('disabled', true);
}

function playMoveAt(cellIndex) {
	if (roundIsOver || board[cellIndex] !== null || !playersAreReady()) {
		return;
	}

	const playerIndex = currentPlayer;
	const player = players[playerIndex];
	markCell(cellIndex, playerIndex);

	const winningLine = findWinningLine(playerIndex);

	if (winningLine) {
		winningLine.forEach((index) =&gt; {
			$(`#game-board .cell[data-index="${index}"]`).addClass(
				'is-winning'
			);
		});

		player.wins += 1;
		$(`.player-card:eq(${playerIndex}) .win-count`).text(player.wins);
		finishRound(`${player.name} wins this round!`, playerIndex);
		return;
	}

	if (board.every((cell) =&gt; cell !== null)) {
		finishRound('This round is a draw.');
		return;
	}

	currentPlayer = (currentPlayer + 1) % players.length;
	beginTurn();
}

function playHumanMove() {
	if (players[currentPlayer].isAi) {
		return;
	}

	playMoveAt(Number($(this).attr('data-index')));
}</pre>
<p>Doing this instead of creating a second “AI version” of the move code means a future rule change has one home. The guard inside <code>playHumanMove()</code> is deliberately repeated even though AI turns disable the board. The disabled buttons guide the interface; the state check protects the game.</p>
<h3 id="reusing-winning-lines">Reusing the winning lines</h3>
<p>The previous game called <code>getWinningLines()</code> each time it checked for a winner. Minimax will check thousands of pretend boards, so rebuilding those identical lines each time would be wasteful. A new round calculates them once, and <code>findWinningLine()</code> can now inspect either the real board or a board supplied by the search:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">// Return the winning positions or undefined when no line matches.
function findWinningLine(playerIndex, squares = board) {
	return winningLines.find((line) =&gt;
		line.every((cellIndex) =&gt; squares[cellIndex] === playerIndex)
	);
}

function getAvailableMoves(squares) {
	const moves = [];

	squares.forEach((square, index) =&gt; {
		if (square === null) {
			moves.push(index);
		}
	});

	return moves;
}

// Reset moves but keep names, tokens, and scores.
function startNewRound() {
	window.clearTimeout(aiTimer);
	closeDialog('round-actions');
	closeDialog('difficulty-suggestion');
	board = Array(boardSize * boardSize).fill(null);
	winningLines = getWinningLines();
	currentPlayer = humanPlayerIndex;
	roundIsOver = false;
	$('#hide-suggestions').prop('checked', false);
	renderBoard();

	if (playersAreReady()) {
		beginTurn();
	} else {
		$('#game-status').text('Set both players to begin.');
	}
}</pre>
<p>The <code>squares = board</code> default keeps old calls short, while Minimax can pass its temporary board explicitly. <code>getAvailableMoves()</code> returns the indexes of every <code>null</code> cell; those are the branches the search can explore.</p>
<h3 id="minimax-algorithm">The Minimax algorithm</h3>
<p><a href="https://en.wikipedia.org/wiki/Minimax">Minimax</a> is a decision-making algorithm for two-player games where one player’s gain is the other player’s loss. The AI is the <em>maximizing</em> player because it wants the largest score. The human is the <em>minimizing</em> player because their best reply should push that score downward.</p>
<p>The “what” is simple: try possible moves and give each future outcome a number. The “how” is recursion—a function calls itself to look one turn farther ahead. The “be careful” is important: without a stopping condition, the recursion never ends; without restoring each pretend move, one branch leaks into the next; and on a large board, looking all the way to the end can freeze the page.</p>
<p>We first order empty cells from nearest the center to farthest away:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">// Give promising center squares to minimax first. Move ordering does not
// change the answer, but it lets alpha-beta pruning skip more work.
function orderMoves(moves) {
	const center = (boardSize - 1) / 2;

	return moves.slice().sort((first, second) =&gt; {
		const firstRow = Math.floor(first / boardSize);
		const firstColumn = first % boardSize;
		const secondRow = Math.floor(second / boardSize);
		const secondColumn = second % boardSize;
		const firstDistance =
			Math.abs(firstRow - center) + Math.abs(firstColumn - center);
		const secondDistance =
			Math.abs(secondRow - center) + Math.abs(secondColumn - center);

		return firstDistance - secondDistance;
	});
}</pre>
<p><code>slice()</code> creates a copy before sorting, so a useful optimization does not unexpectedly rearrange the caller’s array. Center-first ordering does not make a move magically better; it simply helps the pruning step reach strong moves sooner.</p>
<p>Here is the recursive part:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">// Minimax tries an AI move, then a human move, until the game ends or the
// search limit is reached. Alpha and beta avoid branches that cannot improve
// the result we have already found.
function minimax(squares, depth, maximizing, alpha, beta, maxDepth) {
	if (findWinningLine(aiPlayerIndex, squares)) {
		return 100000 - depth;
	}

	if (findWinningLine(humanPlayerIndex, squares)) {
		return depth - 100000;
	}

	const availableMoves = getAvailableMoves(squares);

	if (availableMoves.length === 0) {
		return 0;
	}

	if (depth &gt;= maxDepth) {
		return evaluateBoard(squares);
	}

	if (maximizing) {
		let bestScore = -Infinity;

		for (const move of orderMoves(availableMoves)) {
			squares[move] = aiPlayerIndex;
			bestScore = Math.max(
				bestScore,
				minimax(squares, depth + 1, false, alpha, beta, maxDepth)
			);
			squares[move] = null;
			alpha = Math.max(alpha, bestScore);

			if (beta &lt;= alpha) {
				break;
			}
		}

		return bestScore;
	}

	let bestScore = Infinity;

	for (const move of orderMoves(availableMoves)) {
		squares[move] = humanPlayerIndex;
		bestScore = Math.min(
			bestScore,
			minimax(squares, depth + 1, true, alpha, beta, maxDepth)
		);
		squares[move] = null;
		beta = Math.min(beta, bestScore);

		if (beta &lt;= alpha) {
			break;
		}
	}

	return bestScore;
}</pre>
<p>The first three returns are terminal results: an AI win is a very large positive score, a human win is a very large negative score, and a draw is zero. Including <code>depth</code> means the AI prefers a quick win over a slow one and delays an unavoidable loss as long as it can. Petty? Maybe. Correct? Also yes.</p>
<p>Inside each loop we place a pretend token, recurse, and immediately set that square back to <code>null</code>. Mutating and restoring one array is cheaper than cloning a new board at every branch, but the restore line is non-negotiable. Forget it and your imaginary games begin sharing imaginary moves, which creates very real bugs.</p>
<p><code>alpha</code> is the best score the maximizing side can already guarantee; <code>beta</code> is the best score the minimizing side can already guarantee. Once <code>beta &lt;= alpha</code>, that branch cannot change the final decision, so we stop exploring it. This shortcut is called <a href="https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning">alpha-beta pruning</a>. It changes how much work we do, not which best move we find.</p>
<h3 id="scoring-larger-boards">Scoring unfinished larger boards</h3>
<p>A 3 × 3 board has a small enough game tree to search to the end. Our 4 × 4 through 6 × 6 boards do not. The number of possible move sequences grows wildly, so the depth table stops those searches early. When that happens, <code>evaluateBoard()</code> estimates who has the more promising position:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">// At the search limit, reward lines the AI can still complete and penalize
// lines the human can complete. More marks in a line are worth much more.
function evaluateBoard(squares) {
	let score = 0;

	winningLines.forEach((line) =&gt; {
		const aiMarks = line.filter(
			(index) =&gt; squares[index] === aiPlayerIndex
		).length;
		const humanMarks = line.filter(
			(index) =&gt; squares[index] === humanPlayerIndex
		).length;

		if (aiMarks &gt; 0 &amp;&amp; humanMarks === 0) {
			score += 10 ** aiMarks;
		}

		if (humanMarks &gt; 0 &amp;&amp; aiMarks === 0) {
			score -= 1.1 * 10 ** humanMarks;
		}
	});

	return score;
}</pre>
<p>A line containing marks from both players is blocked, so it contributes nothing. An open line with one AI mark earns <code>10</code>; two marks earn <code>100</code>; three earn <code>1000</code>. The exponential score makes nearly completed lines much more important than scattered tokens. Human threats are multiplied by <code>1.1</code>, giving defense a slight nudge when attack and defense otherwise look equal.</p>
<p>This is a <em>heuristic</em>: a useful estimate, not a promise of perfect play. That distinction matters. Expert is unbeatable on a normal 3 × 3 board because it can search the full remaining game, but on larger boards it is “our strongest search within a sensible browser budget,” not an all-knowing grid deity.</p>
<p>Finally, the top-level search tries each currently available move and keeps every move tied for the best score:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">function findCalculatedMove() {
	const availableMoves = orderMoves(getAvailableMoves(board));
	const maxDepth = Math.min(
		searchDepthByBoardSize[boardSize],
		availableMoves.length
	);
	let bestScore = -Infinity;
	let bestMoves = [];

	for (const move of availableMoves) {
		board[move] = aiPlayerIndex;
		const score = minimax(
			board,
			1,
			false,
			-Infinity,
			Infinity,
			maxDepth
		);
		board[move] = null;

		if (score &gt; bestScore) {
			bestScore = score;
			bestMoves = [move];
		} else if (score === bestScore) {
			bestMoves.push(move);
		}
	}

	return pickRandom(bestMoves);
}</pre>
<p>Choosing randomly from equally good moves instead of always taking the first one makes repeat games less mechanical without making the AI deliberately worse. It also avoids the board’s lowest index becoming its suspiciously favorite square.</p>
<h2 id="difficulty-without-the-always-win-or-tie-trap">Difficulty without the “always win or tie” trap</h2>
<p>A perfect tic-tac-toe AI sounds impressive until every game becomes the same loss or draw. We already did the hard work of finding a calculated move, so difficulty can stay wonderfully small: decide how often to use that move and how often to choose any empty square.</p>
<h3 id="mixing-calculated-and-random-moves">Mixing calculated and random moves</h3>
<pre class="EnlighterJSRAW" data-enlighter-language="js">function chooseAiMove() {
	const availableMoves = getAvailableMoves(board);
	const difficulty = difficulties[players[aiPlayerIndex].difficulty];
	const shouldCalculate = Math.random() &lt; difficulty.calculatedMoveChance;

	if (!shouldCalculate) {
		return pickRandom(availableMoves);
	}

	return findCalculatedMove();
}

function playAiTurn() {
	if (
		roundIsOver ||
		currentPlayer !== aiPlayerIndex ||
		!players[aiPlayerIndex].isAi
	) {
		return;
	}

	playMoveAt(chooseAiMove());
}</pre>
<p>Easy always picks randomly. Normal calculates half of its moves. Hard calculates 85 percent, and Expert calculates every move. That gives us noticeably different personalities without maintaining four separate algorithms.</p>
<p>The random branch is intentionally allowed to make a bad move. If we only randomized between the best Minimax moves, every level would still be optimal—just optimal in a less predictable order. The guards in <code>playAiTurn()</code> matter because its call is delayed. During that pause the round might be restarted, the opponent might be changed back to human, or another setting might invalidate the scheduled turn.</p>
<h3 id="suggesting-a-better-difficulty">Suggesting a better difficulty</h3>
<p>The game quietly watches streaks when Player 2 is an AI. After three human wins it offers the next harder level; after two human losses it offers the next easier one. Draws break both streaks, and a two-human game resets them because difficulty no longer means anything.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">function updateWinStreaks(winnerIndex) {
	if (!players[aiPlayerIndex].isAi) {
		humanWinStreak = 0;
		humanLossStreak = 0;
		return false;
	}

	if (winnerIndex === humanPlayerIndex) {
		humanWinStreak += 1;
		humanLossStreak = 0;

		if (humanWinStreak === winsBeforeDifficultySuggestion) {
			return showDifficultySuggestion(1);
		}
		return false;
	}

	if (winnerIndex === aiPlayerIndex) {
		humanLossStreak += 1;
		humanWinStreak = 0;

		if (humanLossStreak === lossesBeforeDifficultySuggestion) {
			return showDifficultySuggestion(-1);
		}
		return false;
	}

	// A draw breaks both winning and losing streaks.
	humanWinStreak = 0;
	humanLossStreak = 0;
	return false;
}</pre>
<p>The direction is <code>1</code> for harder and <code>-1</code> for easier. That lets one function move through <code>difficultyOrder</code> in either direction:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">function showDifficultySuggestion(direction) {
	if (!showDifficultySuggestions) {
		return false;
	}

	const currentDifficulty = players[aiPlayerIndex].difficulty;
	const currentIndex = difficultyOrder.indexOf(currentDifficulty);
	const nextIndex = currentIndex + direction;

	if (nextIndex &lt; 0 || nextIndex &gt;= difficultyOrder.length) {
		return false;
	}

	suggestedDifficulty = difficultyOrder[nextIndex];
	suggestionDirection = direction;
	const nextLabel = difficulties[suggestedDifficulty].label;
	const message =
		direction &lt; 0
			? `You lost ${lossesBeforeDifficultySuggestion} games in a row. Would you like to try ${nextLabel}?`
			: `You won ${winsBeforeDifficultySuggestion} games in a row. Ready to try ${nextLabel}?`;

	$('#difficulty-suggestion-message').text(message);
	$('#accept-difficulty').text(`Switch to ${nextLabel}`);
	$('#hide-suggestions').prop('checked', false);
	document.getElementById('difficulty-suggestion').showModal();
	return true;
}</pre>
<p>The boundary check prevents suggestions below Easy or above Expert. Returning a Boolean also tells <code>finishRound()</code> whether the suggestion dialog replaced the normal replay dialog.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">function rememberSuggestionPreference() {
	if ($('#hide-suggestions').prop('checked')) {
		showDifficultySuggestions = false;
	}
}

function closeDifficultyDialog() {
	closeDialog('difficulty-suggestion');
	suggestedDifficulty = null;
	suggestionDirection = 0;
}

// Refusing starts a fresh streak, so the question can appear again later.
function refuseDifficultySuggestion() {
	rememberSuggestionPreference();

	if (suggestionDirection &lt; 0) {
		humanLossStreak = 0;
	} else if (suggestionDirection &gt; 0) {
		humanWinStreak = 0;
	}

	closeDifficultyDialog();
	startNewRound();
}

function acceptDifficultySuggestion() {
	if (!suggestedDifficulty) {
		return;
	}

	const newDifficulty = suggestedDifficulty;
	rememberSuggestionPreference();
	humanWinStreak = 0;
	humanLossStreak = 0;
	closeDifficultyDialog();
	players[aiPlayerIndex].difficulty = newDifficulty;

	// The new round is visible before the summary reveals the new level.
	startNewRound();
	$('#ai-difficulty').val(newDifficulty);
	updatePlayerSummary(aiPlayerIndex);
}</pre>
<p>“Don’t show this again” lasts for the current page visit rather than being saved permanently. That keeps the demo simple and avoids introducing <code>localStorage</code> for one checkbox. Refusing resets only the streak that caused the suggestion, so the same question can return after another genuine streak instead of appearing after every following round.</p>
<h2 id="some-extra-polish-to-make-it-feel-good">Some extra polish to make it feel good</h2>
<p>The AI now works, so everything in this section is about making its behavior understandable and keeping small interface surprises out of the way. None of these pieces makes Minimax smarter, but together they make the game feel much less like a spreadsheet that occasionally places a hat.</p>
<h3 id="turn-status-and-board-locking">A visible thinking turn and a locked board</h3>
<p>The old <code>showTurn()</code> becomes <code>beginTurn()</code>. Human turns enable empty cells. AI turns lock every cell, show a thinking message, and wait 450 milliseconds before moving:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">// Enable empty cells only when it is a human player's turn.
function updateBoardAvailability() {
	const humanCanPlay =
		playersAreReady() &amp;&amp; !roundIsOver &amp;&amp; !players[currentPlayer].isAi;
	const currentPlayerClass =
		currentPlayer === humanPlayerIndex
			? 'current-player-one'
			: 'current-player-two';
	const $board = $('#game-board');

	$board.removeClass(currentPlayerClasses).addClass(currentPlayerClass);

	$board.find('.cell').each(function () {
		const cellIndex = Number($(this).attr('data-index'));
		const canUseCell = humanCanPlay &amp;&amp; board[cellIndex] === null;

		$(this)
			.prop('disabled', !canUseCell)
			.attr('aria-disabled', String(!canUseCell));
	});
}

function showPlayerStatus(playerIndex, message) {
	const player = players[playerIndex];
	const playerClass =
		playerIndex === humanPlayerIndex ? 'player-one' : 'player-two';
	const $playerName = $('&lt;span&gt;', {
		class: `game-status__player ${playerClass}`,
		text: player.name,
	});

	$('#game-status').empty().append($playerName, message);
}

function beginTurn() {
	if (!playersAreReady() || roundIsOver) {
		updateBoardAvailability();
		return;
	}

	const player = players[currentPlayer];

	if (player.isAi) {
		showPlayerStatus(currentPlayer, ' is thinking…');
		updateBoardAvailability();

		// The short pause makes the computer turn easy to follow.
		window.clearTimeout(aiTimer);
		aiTimer = window.setTimeout(playAiTurn, 450);
		return;
	}

	showPlayerStatus(currentPlayer, ', choose an empty square.');
	updateBoardAvailability();
}</pre>
<p>The delay is not pretending that the calculation takes 450 milliseconds—the search usually finishes much faster. It gives the player enough time to see that the turn changed. Using <code>setTimeout()</code> instead of a blocking delay keeps the page responsive, and every reset calls <code>clearTimeout()</code> so an old AI move cannot arrive late on a fresh board.</p>
<p><code>showPlayerStatus()</code> creates the name with jQuery’s <code>text</code> option rather than assembling an HTML string. That keeps user-entered names as text while still letting us color the name separately from the rest of the sentence.</p>
<p><code>renderBoard()</code> now creates every cell disabled. That is a safer starting point than guessing whether somebody can play while the buttons are being built; <code>beginTurn()</code> immediately calls <code>updateBoardAvailability()</code> with the complete current state.</p>
<h3 id="turn-colored-hover-shades">Turn-colored hover shades</h3>
<p>The board receives either <code>.current-player-one</code> or <code>.current-player-two</code>. Those classes fill two temporary custom properties used by the cell hover state:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="css">:root {
	--player-one-hover: color-mix(
		in oklab,
		var(--paper),
		var(--player-one) 10%
	);
	--player-two-hover: color-mix(
		in oklab,
		var(--paper),
		var(--player-two) 10%
	);
}

.board.current-player-one {
	--current-player: var(--player-one);
	--current-player-hover: var(--player-one-hover);
}

.board.current-player-two {
	--current-player: var(--player-two);
	--current-player-hover: var(--player-two-hover);
}

.cell:not(:disabled):hover {
	border-color: var(--current-player);
	background: var(--current-player-hover);
	transform: translateY(-2px);
}</pre>
<p>The hover now previews the current player’s color instead of always using Player 2’s green. We mix the pale backgrounds in <code>oklab</code> rather than <code>oklch</code>. Both are perceptual color spaces, but interpolating rectangular Lab coordinates behaved more smoothly here when the two player colors travel toward nearly white paper. With polar <code>oklch</code>, hue becomes unstable as chroma approaches zero, which can give very pale mixes an unexpected tint. The <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color-mix"><code>color-mix()</code> reference</a> has more examples if you want to experiment.</p>
<h3 id="keeping-token-choices-honest">Keeping token choices honest</h3>
<p>The first game rejected a duplicate token only after form submission. This version also disables any token already in use, and disables the entire human picker when AI mode is selected:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">function updateTokenAvailability() {
	$('.player-form').each(function () {
		const $form = $(this);
		const playerIndex = Number($form.attr('data-player'));
		const usesAiSettings =
			playerIndex === aiPlayerIndex &amp;&amp;
			$form.find('[name="opponent-type"]:checked').val() === 'ai';

		$form.find('[name="icon"]').each(function () {
			const $input = $(this);
			const icon = String($input.val());
			const isUsedByOtherPlayer = players.some(
				(player, index) =&gt;
					index !== playerIndex &amp;&amp;
					player.isReady &amp;&amp;
					player.icon === icon
			);

			$input.prop('disabled', usesAiSettings || isUsedByOtherPlayer);

			if (isUsedByOtherPlayer &amp;&amp; $input.prop('checked')) {
				$input.prop('checked', false);
			}
		});
	});
}</pre>
<p>This is interface help, not a replacement for validation, so <code>setPlayer()</code> still rejects a duplicate. Doing both means the form prevents the common mistake while the game rules still protect themselves if state changes in an unexpected order.</p>
<h3 id="native-dialogs-for-round-results">Native dialogs for results and confirmations</h3>
<p>The inline replay box becomes a native <code>&lt;dialog&gt;</code>, and two more dialogs confirm a mid-game token change and suggest a difficulty. Here is the token confirmation; the other two use the same pattern and are included in the full <code>index.html</code>:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="html">&lt;dialog
	class="restart-confirmation"
	id="token-change-confirmation"
	aria-labelledby="token-change-confirmation-title"
	aria-describedby="token-change-confirmation-message"
&gt;
	&lt;h3 id="token-change-confirmation-title"&gt;Restart the game?&lt;/h3&gt;
	&lt;p id="token-change-confirmation-message"&gt;&lt;/p&gt;
	&lt;div class="confirmation-actions"&gt;
		&lt;button
			class="button button--quiet"
			id="cancel-token-change"
			type="button"
		&gt;
			Cancel
		&lt;/button&gt;
		&lt;button class="button" id="confirm-token-change" type="button"&gt;
			Change token and restart
		&lt;/button&gt;
	&lt;/div&gt;
&lt;/dialog&gt;</pre>
<p>Using <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog">the native dialog element</a> instead of building a pretend modal from a positioned <code>&lt;div&gt;</code> gives us modal behavior, a backdrop, and Escape-key events. We still decide what Escape means so every closing path runs the same cleanup.</p>
<p>Inside <code>setPlayer()</code>, this check pauses a real token change only after the game has started. Changing a name or difficulty can continue normally:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">if (
	!isAi &amp;&amp;
	player.isReady &amp;&amp;
	gameHasStarted() &amp;&amp;
	icon !== player.icon &amp;&amp;
	confirmedTokenChangeForm !== this
) {
	pendingPlayerForm = this;
	window.clearTimeout(aiTimer);
	$('#token-change-confirmation-message').text(
		`Changing the token for ${player.name} will restart the current game and clear the board. Do you want to continue?`
	);
	document.getElementById('token-change-confirmation').showModal();
	return;
}</pre>
<pre class="EnlighterJSRAW" data-enlighter-language="js">function gameHasStarted() {
	return board.some((cell) =&gt; cell !== null);
}

function closeDialog(dialogId) {
	const dialog = document.getElementById(dialogId);

	if (dialog.open) {
		dialog.close();
	}
}

function cancelTokenChange() {
	if (pendingPlayerForm) {
		const $form = $(pendingPlayerForm);
		const playerIndex = Number($form.attr('data-player'));

		$form
			.find('[name="icon"]')
			.prop('checked', false)
			.filter(function () {
				return $(this).val() === players[playerIndex].icon;
			})
			.prop('checked', true);
	}

	pendingPlayerForm = null;
	closeDialog('token-change-confirmation');

	if (playersAreReady() &amp;&amp; !roundIsOver) {
		beginTurn();
	}
}

function confirmTokenChange() {
	const playerForm = pendingPlayerForm;

	if (!playerForm) {
		return;
	}

	pendingPlayerForm = null;
	confirmedTokenChangeForm = playerForm;
	closeDialog('token-change-confirmation');

	try {
		$(playerForm).trigger('submit');
	} finally {
		confirmedTokenChangeForm = null;
	}
}</pre>
<p>Canceling restores the radio selection to the saved player token. Confirming resubmits the original form, and <code>confirmedTokenChangeForm</code> tells <code>setPlayer()</code> not to ask the same question again. The <code>finally</code> block clears that temporary permission even if a future edit makes the submit handler throw an error.</p>
<p>Finishing a round now chooses exactly one modal: the difficulty suggestion when a streak reaches its threshold, otherwise the normal replay dialog.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">// End a round. A difficulty question takes priority over the replay dialog.
function finishRound(message, winnerIndex = null) {
	roundIsOver = true;
	$('#game-board .cell')
		.prop('disabled', true)
		.attr('aria-disabled', 'true');
	if (winnerIndex === null) {
		$('#game-status').text(message);
	} else {
		showPlayerStatus(winnerIndex, ' wins this round!');
	}
	$('#round-result').text(message);

	if (!updateWinStreaks(winnerIndex)) {
		document.getElementById('round-actions').showModal();
	}
}</pre>
<h3 id="connecting-the-new-events">Connecting the new events</h3>
<p>The last change is the event wiring at the bottom of <code>tic_tac_toe.js</code>:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">// Connect page events after the HTML is ready.
$(function () {
	$('.player-form').on('submit', setPlayer);
	$('#player-2-form [name="opponent-type"]').on(
		'change',
		toggleOpponentSettings
	);
	$('#size-form').on('submit', setBoardSize);
	$('#board-size').on('input', previewBoardSize);
	$('#game-board').on('click', '.cell', playHumanMove);
	$('#restart-game').on('click', startNewRound);
	$('#confirm-token-change').on('click', confirmTokenChange);
	$('#cancel-token-change').on('click', cancelTokenChange);
	$('#token-change-confirmation').on('cancel', function (event) {
		event.preventDefault();
		cancelTokenChange();
	});
	$('#round-actions').on('cancel', function (event) {
		// Escape has the same result as choosing to play another round.
		event.preventDefault();
		startNewRound();
	});
	$('#accept-difficulty').on('click', acceptDifficultySuggestion);
	$('#keep-difficulty, #close-difficulty').on(
		'click',
		refuseDifficultySuggestion
	);
	$('#difficulty-suggestion').on('cancel', function (event) {
		// Prevent the browser's automatic close so every refusal uses the
		// same counter-reset and preference logic.
		event.preventDefault();
		refuseDifficultySuggestion();
	});

	toggleOpponentSettings();
	startNewRound();
});</pre>
<p>The board still uses event delegation, but it now calls <code>playHumanMove</code> instead of the old all-purpose click function. Calling <code>toggleOpponentSettings()</code> once on startup makes the controls agree with the initially checked AI radio button before anyone touches the form.</p>
<p>Notice that every dialog’s button and Escape key lead to the same named function. Centralizing that cleanup instead of sprinkling <code>dialog.close()</code> calls everywhere prevents a closed dialog from leaving streak counters, selected radios, or pending forms in a half-finished state.</p>
<h2 id="ready-to-play">Ready to play</h2>
<p>That is our old two-player game upgraded with a computer opponent, without throwing away the code that already worked. Humans and the AI share one move pipeline, Minimax supplies strategic moves, random choices keep easier levels friendly, and the larger boards use a depth limit so “Expert” does not turn the browser into a small space heater.</p>
<p>The most useful pattern here is not specific to tic-tac-toe: keep the rules in shared functions, then let different controllers—mouse clicks, keyboard-friendly buttons, or an AI—choose the inputs. That approach can also help us add online play later, because a move received over the network could call the same <code>playMoveAt()</code> function.</p>
<p>Try the <a href="/demos/jquery-tic-tac-toe-with-ai/">finished demo</a>, then make it misbehave on purpose. Change the difficulty probabilities. Make the AI dramatically announce its thoughts. Adjust the search depths and watch the performance. Small games are still wonderful laboratories, especially when your lab partner is named Quinn and insists on being a boot.</p>
<ul class="tutorial-resources" aria-label="Tutorial resources">
<li class="tutorial-resource download"><a class="tutorial-resource__link" href="/demos/jquery-tic-tac-toe-with-ai/jquery-tic-tac-toe-with-ai.zip?x55466" rel="internal" download="jquery-tic-tac-toe-with-ai.zip" aria-label="Download the ZIP file for this tutorial"> Zip file for this tutorial </a></li>
<li class="tutorial-resource demo"><a class="tutorial-resource__link" href="/demos/jquery-tic-tac-toe-with-ai/" rel="internal" aria-label="Open the demo for this tutorial"> Demo for this tutorial </a></li>
</ul>
<p>Have fun!</p><p>The post <a href="https://webdevplayground.com/tutorials/jquery-tic-tac-toe-with-ai/">How To: Add AI to our jQuery Tic-Tac-Toe</a> first appeared on <a href="https://webdevplayground.com">WebDevPlayground</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://webdevplayground.com/tutorials/jquery-tic-tac-toe-with-ai/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Why Bother With Semantic HTML?</title>
		<link>https://webdevplayground.com/2026/10/why-bother-with-semantic-html/</link>
					<comments>https://webdevplayground.com/2026/10/why-bother-with-semantic-html/#respond</comments>
		
		<dc:creator><![CDATA[adi]]></dc:creator>
		<pubDate>Tue, 13 Oct 2026 20:53:46 +0000</pubDate>
				<category><![CDATA[General]]></category>
		<category><![CDATA[HTML]]></category>
		<category><![CDATA[Opinion]]></category>
		<guid isPermaLink="false">https://webdevplayground.com/?p=331</guid>

					<description><![CDATA[<p>Not too long ago (12 years or so), there appeared a new HTML version. It contained lots of cool stuff. It introduced us to some new semantic elements as well. Well, if you are anything like me, you will not blindly jump on everything new just because of how cool it sounds. Plus, there are [&#8230;]</p>
<p>The post <a href="https://webdevplayground.com/2026/10/why-bother-with-semantic-html/">Why Bother With Semantic HTML?</a> first appeared on <a href="https://webdevplayground.com">WebDevPlayground</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Not too long ago (12 years or so), there appeared a new HTML version. It contained lots of cool stuff. It introduced us to some new semantic elements as well.</p>
<p>Well, if you are anything like me, you will not blindly jump on everything new just because of how cool it sounds. Plus, there are media elements, storage, and lots of other exciting things that HTML5 brought us, so I was very busy.</p>
<p>However, after some time, I noticed that I was actually too busy and lazy. Those new elements could save me some work. More importantly, they can also save me some time on debugging code. At least, for me, less work is a good thing&#8230;<span id="more-331"></span></p>
<p>So, suddenly, I did not feel comfortable with the “I wrote it once, it still works, so why would I touch something that is not broken?” approach.</p>
<p>Want to know why? I hope by the end of this article, you will understand why I find the elements so cool. Maybe, even agree with me?</p>
<h2 id="what-are-the-semantic-html-elements">What are the semantic HTML elements?</h2>
<p>OK, so let’s talk about semantics first. Semantics simply means that an element has certain meaning in your code.</p>
<p>A <code>&lt;nav&gt;</code> element contains some navigation. A <code>&lt;button&gt;</code> is a button. An <code>&lt;article&gt;</code> can contain some meaningful text, like a blog post or news.</p>
<p>There is also <code>&lt;div&gt;</code>. I have nothing against <code>&lt;div&gt;</code>. I used plenty of them, enough to build a small country. However, the <code>&lt;div&gt;</code> element has no semantics. It is a box.</p>
<p>So, for example, we can create a web page using this HTML:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="html">&lt;div class="page-header"&gt;...&lt;/div&gt;
&lt;div class="main-menu"&gt;...&lt;/div&gt;
&lt;div class="content"&gt;...&lt;/div&gt;
&lt;div class="page-footer"&gt;...&lt;/div&gt;</pre>
<p>It works. It looks beautiful when we apply some CSS styles. There is nothing wrong with it.</p>
<p>However, we can also do the following:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="html">&lt;header&gt;...&lt;/header&gt;
&lt;nav aria-label="Main navigation"&gt;...&lt;/nav&gt;
&lt;main&gt;...&lt;/main&gt;
&lt;footer&gt;...&lt;/footer&gt;</pre>
<p>That will look the same when we apply CSS. The difference is that now the browser knows what kind of elements those are.</p>
<p>Also, we do not have to talk about the big pieces of HTML code only. The heading is a heading. List is a list. A date can go to <code>&lt;time&gt;</code> and a form field will tell the browser what type of input it expects.</p>
<p>You do not have to remember all of the HTML elements. I do not remember all of them. When I get a feeling that there is a better element for something, I check the <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements">MDN HTML element reference</a>. Easier than trying to keep it in your head.</p>
<h2 id="the-less-code-the-better">The less code – the better</h2>
<p>Here is the part that made me change my mind about those new elements.</p>
<p>So, suppose we need a button to send some message. We can create it in the following way:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="html">&lt;div class="submit-button"&gt;Send message&lt;/div&gt;</pre>
<p>Then, we can add some CSS styles and make it look like a button. We can also add a click event listener and make it behave like a button. Great, right?</p>
<p>Well, not exactly.</p>
<p>Is it reachable using the keyboard? What will happen if we press Enter or Spacebar? Does it have a focus style? Can it be disabled? Will a screen reader understand that it is a button?</p>
<p>Well, we can fix all that. We can add <code>tabindex</code>, <code>role</code>, keyboard events, some more CSS styles, test and debug it. And, hopefully, we will end up with something that behaves like&#8230; a button.</p>
<p>However, there is an easier way to do that.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="html">&lt;button type="submit"&gt;Send message&lt;/button&gt;</pre>
<p>That is it. The browser already knows what this element is. It receives a keyboard focus. It behaves as expected. It can be disabled. It can submit the form. Screen readers understand it.</p>
<p>Sure, we may still need some CSS. The default button is not going to magically match our styles. Also, we may need some JavaScript to handle the button click event.</p>
<p>However, why spend time implement all of those boring features? Personally I rather delagate as much as possible to the browser.</p>
<p>It is also true for other HTML elements. For example, if we need to navigate to some other page, an <code>&lt;a&gt;</code> element already knows how to behave. <code>&lt;label&gt;</code> already knows how to connect to the form field. Certain type of <code>&lt;input&gt;</code> already has a better keyboard appearance on a smartphone.</p>
<p>Remember how we spent time building and adjusting accordions? HTML5 lets us use <code>&lt;details&gt;</code> and <code>&lt;summary&gt;</code>. These elements already known how to expand/collapse some content on a page.</p>
<p>Will it replace every custom control? No. However, if the standard HTML element already does what I need, I take the free help.</p>
<p>If you want to see the rest of the features provided by the browser, like <code>&lt;dialog&gt;</code>, <code>&lt;meter&gt;</code>, <code>&lt;datalist&gt;</code>, and a lot more, <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements">MDN has it all</a>. It also has a nice <a href="https://developer.mozilla.org/en-US/curriculum/core/semantic-html/">semantic HTML introduction page</a>. And yes, I love the little <code>&lt;button&gt;</code> element, <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button">its documentation</a> is also worth a read. It is doing more than people think.</p>
<h2 id="not-everybody-uses-pages-the-way-i-do">Not everybody uses pages the way I do</h2>
<p>So, I spend most of the time looking at the websites on a computer screen. Usually, I also have a mouse with me. I can easily forget that not everybody uses a website in this way.</p>
<p>Some people use the keyboard to navigate the website. Some use screen readers. Some use voice commands. There are people who zoom the website and change its colors. And, of course, there are lots of different screens sizes.</p>
<p>The website is also visited by something that is not a person. Search engines read it. Browser extensions read it. Reader modes try to extract useful information from it.</p>
<p>Sure, they can guess what each of the <code>&lt;div&gt;</code> elements is supposed to do. Sometimes they guess correctly. I prefer not to make them guess.</p>
<p>So, the actual heading tells them, “I am a heading.” <code>&lt;nav&gt;</code> says, “Those links are for navigation.” <code>&lt;main&gt;</code> points to the main content of the page.</p>
<p>Assistive technology can convert such information into something called an accessibility tree. It is the representation of the page that emphasizes roles, names, and states rather than colors and positions.</p>
<p>I really like that I do not need to know which screen reader, browser, or device people will use. I only need to be honest with my content. The browser will do the rest.</p>
<p>Sure, this does not mean that I have to replace every <code>&lt;div&gt;</code> with something fancy. Not everything is an <code>&lt;article&gt;</code>. Not every group needs to be a <code>&lt;section&gt;</code>. Sometimes a box is a box.</p>
<p>If you want to know more about the structure of the webpage, W3C has a nice <a href="https://www.w3.org/WAI/tutorials/page-structure/">page structure tutorial</a>. It explains headers, navigation, main content, and footers without making it a novel.</p>
<h2 id="accessible-html-without-rewriting-everything">Accessible HTML without rewriting everything</h2>
<p>OK, so semantic HTML will not make a website accessible just like this. I wish it were possible.</p>
<p>We can still forget to provide alternative text. We can still use colors that cannot be read by anybody. We can still make the form too complicated to use.</p>
<p>However, semantic HTML gives us a better starting point.</p>
<p>So, for example, I can look at a page and see big texts. It tells me where the one content ends and the other starts. A user with screen reader may see the list of headings and jump to the desired one.</p>
<p>However, if my “headings” are actually some styled <code>&lt;div&gt;</code> elements, they will not appear in the list of headings. They look like headings to me. To the browser, they are just boxes.</p>
<p>The same applies to buttons, form controls, links, and page regions. When I use the right element, a lot of useful information is automatically provided with it.</p>
<p>So, I can add more information to my page using ARIA attributes if needed. ARIA is cool, especially when there is some interface that HTML cannot describe properly. However, adding <code>role="button"</code> to a <code>&lt;div&gt;</code> does not give it all button behavior. I have to implement that part.</p>
<p>W3C also has <a href="https://www.w3.org/WAI/ARIA/apg/practices/read-me-first/">more information about ARIA usage</a>. My short version of it is this: if HTML has an element I need, I use it first.</p>
<p>Why should I write more code then?</p>
<h2 id="how-about-search-engines">How about search engines?</h2>
<p>Sure, it can also help with search engines.</p>
<p>A search engine tries to understand what my page is about. Clear headings help to understand its structure. Descriptive links tell a search engine something about their destination. The text that actually exists in a webpage is easier to process than the text added with CSS or created in a canvas.</p>
<p>Did you know that Google recommends semantic HTML in its <a href="https://developers.google.com/search/docs/fundamentals/get-started-developers">SEO guide for web developers</a>?</p>
<p>So, does that mean that if I change several <code>&lt;div&gt;</code> elements to <code>&lt;section&gt;</code>, I will be on the top of Google tomorrow?</p>
<p>Unfortunately, no. I would love that too.</p>
<p>There are lots of things that search engines look for. Useful content is important. The titles, links, speed, and mobile optimization also matter.</p>
<p>Having said all that, I do not think of semantic HTML as an SEO trick. It is more like providing a cleaner copy of the page for the search engine to read. It may understand the messy version anyway. But I rather not make it work harder (afterall, I do want it to like mysite).</p>
<h2 id="it-helps-the-next-developer">It helps the next developer</h2>
<p>So, there is one more piece of equipment that has problems understanding my old code. Me.</p>
<p>I can return to a project after six months and not remember anything. I have to check Git, see my own name, and stop blaming somebody else.</p>
<p>Semantic HTML can also help here.</p>
<p>So, when I see <code>&lt;nav&gt;</code>, I know why those links are grouped together. I also know what <code>&lt;article&gt;</code>, <code>&lt;aside&gt;</code>, or <code>&lt;footer&gt;</code> elements tell me about the page structure.</p>
<p>Long stacks of nested <code>&lt;div&gt;</code> elements do not tell me anything. I have to analyze class names, CSS styles, and maybe stare at the screen for some time.</p>
<p>So, the elements with a meaning are like outdated comments. They will not explain everything. However, the first minutes in an old file will be less painful.</p>
<h2 id="so-am-i-convinced">So, am I convinced?</h2>
<p>Yes, but not because it is newer or shinier. These reasons are not convincing for me.</p>
<p>I use semantic HTML because it saves work. It provides me with the behavior that I do not have to write. It helps browsers, assistive technology, and search engines to understand the page better. It also makes my code a little easier to understand when I return back to it after some time.</p>
<p>I also use <code>&lt;div&gt;</code>. A lot. Sometimes there is no more meaningful element and that is exactly what <code>&lt;div&gt;</code> is for.</p>
<p>I just do not start using it immediately anymore.</p>
<p>Now, I stop and ask, “Does HTML have an element for this?” If it does, it will probably save me some code. And a bug as well.</p>
<p>So, if you are curious what your markup says, you can check it with the <a href="https://validator.w3.org/nu/">Nu HTML Checker</a>. You can also check the accessibility tree in your browser’s developer tools. It is interesting to compare the page on the screen and the page the browser understands.</p>
<p>And this is actually my opinion on semantic HTML. It is not about using fancy tags because somebody told me they are better.</p>
<p>It is about letting HTML to do the job for me. After all, Less code. Less bugs. Less work. Opps, I meant more time to work on other things <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f609.png" alt="😉" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p><p>The post <a href="https://webdevplayground.com/2026/10/why-bother-with-semantic-html/">Why Bother With Semantic HTML?</a> first appeared on <a href="https://webdevplayground.com">WebDevPlayground</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://webdevplayground.com/2026/10/why-bother-with-semantic-html/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Creating a Tic-Tac-Toe game with jQuery</title>
		<link>https://webdevplayground.com/tutorials/creating-a-tic-tac-toe-game-with-jquery/</link>
					<comments>https://webdevplayground.com/tutorials/creating-a-tic-tac-toe-game-with-jquery/#respond</comments>
		
		<dc:creator><![CDATA[adi]]></dc:creator>
		<pubDate>Sat, 10 Oct 2026 01:10:59 +0000</pubDate>
				<category><![CDATA[Games]]></category>
		<category><![CDATA[Javascript]]></category>
		<category><![CDATA[jQuery]]></category>
		<guid isPermaLink="false">https://webdevplayground.com/?post_type=tutorial&#038;p=316</guid>

					<description><![CDATA[<p>Let’s build a modern tic-tac-toe game with jQuery, complete with player names, SVG tokens, adjustable board sizes, scorekeeping, and replay controls. Along the way, we’ll use semantic HTML, CSS Grid, and beginner-friendly JavaScript to handle turns, wins, and draws.</p>
<p>The post <a href="https://webdevplayground.com/tutorials/creating-a-tic-tac-toe-game-with-jquery/">Creating a Tic-Tac-Toe game with jQuery</a> first appeared on <a href="https://webdevplayground.com">WebDevPlayground</a>.</p>]]></description>
										<content:encoded><![CDATA[<h1>Creating a Tic-Tac-Toe Game with jQuery</h1>
<p>A long time ago, I wrote a tutorial about building tic-tac-toe with jQuery. It did the job, but web development has moved on a little since then. We no longer need layout-clearing <code>&lt;div&gt;</code> elements, mouse events just to fake a hover effect, or code written with Internet Explorer 6 peeking over our shoulder. Phew.</p>
<p>So, let’s rebuild the game with modern HTML, CSS, JavaScript, and jQuery. It will still be the friendly little game we know, but this version gets a few toys: player names, choose-your-own SVG tokens, boards from 3 × 3 to 6 × 6, a running score, and a replay button.<span id="more-316"></span></p>
<p>You can <a href="/demos/creating-a-tic-tac-toe-game-with-jquery/">open the finished demo</a> whenever you want to see where we are heading. Peeking is allowed. This is a tutorial, not an exam.</p>
<h2>Before we begin</h2>
<p>I’m going to assume that you:</p>
<ul>
<li>know how tic-tac-toe works;</li>
<li>know a little semantic HTML and basic CSS;</li>
<li>have met JavaScript variables, arrays, functions, and loops before;</li>
<li>know that SVG is an image format, even if you have never written an SVG by hand.</li>
</ul>
<p>You do <strong>not</strong> need to be a jQuery wizard. We will meet each bit of jQuery as it appears.</p>
<p>Create a folder with these files:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">creating-a-tic-tac-toe-game-with-jquery/
├── index.html
├── style.css
├── tic_tac_toe.js
├── jquery-4.0.0.min.js
└── token-icons.svg
</pre>
<p>The demo keeps its own local copy of jQuery, so it does not depend on a CDN or an internet connection. Connect the files in the page’s <code>&lt;head&gt;</code>:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="html">&lt;link rel="stylesheet" href="style.css" /&gt;
&lt;!-- Defer keeps scripts from blocking the page while it loads. --&gt;
&lt;script src="jquery-4.0.0.min.js" defer&gt;&lt;/script&gt;
&lt;script src="tic_tac_toe.js" defer&gt;&lt;/script&gt;
</pre>
<p>Order matters: our game uses jQuery, so jQuery must load first. The <code>defer</code> attribute tells the browser to download both scripts without holding up the HTML, then run them in order after the page has been parsed.</p>
<h2>What’s in a game?</h2>
<p>Before we start flinging curly brackets around, let’s decide what our game actually has to do.</p>
<h3>The requirements</h3>
<ol>
<li>Start with a 3 × 3 board, with an option to choose a size up to 6 × 6.</li>
<li>Let two local players choose a display name and different token icons.</li>
<li>Let each player mark exactly one empty cell per turn.</li>
<li>Win when one player fills a complete row, column, or diagonal.</li>
<li>End in a draw when every cell is occupied and nobody has won.</li>
<li>Keep score and let the players begin another round.</li>
</ol>
<h3>Perhaps later</h3>
<ul>
<li>Play against the computer.</li>
<li>Add sound effects.</li>
<li>Let friends play over the internet.</li>
</ul>
<p>Those “later” ideas are parked safely outside today’s scope. Otherwise our tiny tutorial will put on a fake moustache and become a game engine.</p>
<h2>Building the page shell</h2>
<p>Our layout has three main pieces: Player 1 on the left, the game in the middle, and Player 2 on the right.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="html">&lt;header class="site-header"&gt;
	&lt;h1&gt;Tic-Tac-Toe Demo&lt;/h1&gt;
&lt;/header&gt;

&lt;main class="game-layout"&gt;
	&lt;section class="player-card player-one" aria-labelledby="player-1-heading"&gt;
		&lt;!-- Player 1 settings go here. --&gt;
	&lt;/section&gt;

	&lt;section class="game-panel" aria-labelledby="game-heading"&gt;
		&lt;h2 id="game-heading"&gt;Game board&lt;/h2&gt;
		&lt;!-- Size control, status, board, and replay button go here. --&gt;
	&lt;/section&gt;

	&lt;section class="player-card player-two" aria-labelledby="player-2-heading"&gt;
		&lt;!-- Player 2 settings go here. --&gt;
	&lt;/section&gt;
&lt;/main&gt;
</pre>
<p>CSS Grid is a very natural match for that structure:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="css">.game-layout {
	display: grid;
	grid-template-columns: minmax(14rem, 0.8fr) minmax(21rem, 1.5fr) minmax(
			14rem,
			0.8fr
		);
	align-items: start;
	gap: 1.25rem;
	width: min(100% - 2rem, 76rem);
	margin: 0 auto;
	padding-bottom: 3rem;
}

.player-card,
.game-panel {
	border: 1px solid var(--panel-line);
	border-radius: var(--radius);
	background: var(--panel-surface);
	box-shadow: var(--shadow);
}
</pre>
<p>The middle column receives a little more room than the two player cards. On narrower screens, we turn the three columns into one:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="css">@media (max-width: 56rem) {
	.game-layout {
		grid-template-columns: 1fr;
		width: min(100% - 2rem, 38rem);
	}
}
</pre>
<p>That is our stage built. Time to invite the players.</p>
<h2>Requirement 1: two players with names and tokens</h2>
<p>Each player gets a form inside a <code>&lt;details&gt;</code> element. <code>&lt;details&gt;</code> gives us an expandable panel without needing JavaScript for the opening and closing behavior.</p>
<p>Here is Player 1’s setup. We will use the same structure for Player 2, changing the IDs, <code>data-player</code> value, button text, and initially selected icon.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="html">&lt;details class="player-settings" open&gt;
	&lt;summary class="player-card__heading"&gt;
		&lt;span&gt;
			&lt;span class="player-number"&gt;Player 1&lt;/span&gt;
			&lt;span id="player-1-heading" class="player-settings__title"&gt;
				Choose your token
			&lt;/span&gt;
		&lt;/span&gt;
	&lt;/summary&gt;

	&lt;form class="player-form" id="player-1-form" data-player="0"&gt;
		&lt;label for="player-1-name"&gt;Display name&lt;/label&gt;
		&lt;input
			id="player-1-name"
			name="name"
			type="text"
			value="Player 1"
			maxlength="20"
			required
		/&gt;

		&lt;fieldset&gt;
			&lt;legend&gt;Token icon&lt;/legend&gt;
			&lt;div class="icon-picker"&gt;
				&lt;!-- Token choices go here. --&gt;
			&lt;/div&gt;
		&lt;/fieldset&gt;

		&lt;p class="form-message" aria-live="polite"&gt;&lt;/p&gt;
		&lt;button class="button button--player" type="submit"&gt;
			Set Player 1
		&lt;/button&gt;
	&lt;/form&gt;
&lt;/details&gt;

&lt;div class="player-summary" hidden&gt;
	&lt;p class="summary-label"&gt;Current setup&lt;/p&gt;
	&lt;div class="summary-token" aria-hidden="true"&gt;&lt;/div&gt;
	&lt;strong class="summary-name"&gt;&lt;/strong&gt;
	&lt;p&gt;Wins: &lt;span class="win-count"&gt;0&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
</pre>
<p>The <code>data-player="0"</code> attribute connects this form to player zero in our JavaScript array. Programmers usually start counting at zero; we are fun at parties.</p>
<p>The empty <code>.form-message</code> will hold validation errors. <code>aria-live="polite"</code> asks screen readers to announce new messages without rudely interrupting whatever they are already reading. The summary begins with the Boolean <code>hidden</code> attribute and appears after the form is successfully submitted.</p>
<h3>Making an SVG token picker</h3>
<p>The icons live in <code>token-icons.svg</code> as SVG <code>&lt;symbol&gt;</code> elements. Here is the top hat:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="xml">&lt;svg xmlns="http://www.w3.org/2000/svg"&gt;
	&lt;symbol id="token-hat" viewBox="0 0 64 64"&gt;
		&lt;g
			fill="none"
			stroke="currentColor"
			stroke-width="4"
			stroke-linecap="round"
			stroke-linejoin="round"
		&gt;
			&lt;path d="M20 43 24 16h16l4 27"&gt;&lt;/path&gt;
			&lt;path d="M22 34h20"&gt;&lt;/path&gt;
			&lt;path d="M12 43c7 4 33 4 40 0l-2 7H14z"&gt;&lt;/path&gt;
		&lt;/g&gt;
	&lt;/symbol&gt;
&lt;/svg&gt;
</pre>
<p><code>currentColor</code> is a handy little trick: the icon’s stroke inherits the CSS <code>color</code> of wherever we display it. One drawing can therefore be red for Player 1, green for Player 2, and another color when selected.</p>
<p>Each icon choice is still an ordinary radio button with an ordinary label:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="html">&lt;label class="icon-option" title="Top hat"&gt;
	&lt;input type="radio" name="icon" value="hat" checked /&gt;
	&lt;span&gt;
		&lt;svg aria-hidden="true"&gt;
			&lt;use href="token-icons.svg#token-hat"&gt;&lt;/use&gt;
		&lt;/svg&gt;
		&lt;span class="visually-hidden"&gt;Top hat&lt;/span&gt;
	&lt;/span&gt;
&lt;/label&gt;
</pre>
<p>The visible drawing is decorative, while “Top hat” remains available to a screen reader. Repeat this label for <code>car</code>, <code>ship</code>, and <code>boot</code>, changing both the radio value and the symbol after <code>#token-</code>.</p>
<p>We hide the small native radio circle and style its neighboring <code>&lt;span&gt;</code> as the big clickable choice:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="css">.visually-hidden {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect(0 0 0 0);
	white-space: nowrap;
	border: 0;
}

.icon-picker {
	display: grid;
	grid-template-columns: repeat(4, 1fr);
	gap: 0.5rem;
}

.icon-option {
	position: relative;
}

.icon-option input {
	position: absolute;
	opacity: 0;
	pointer-events: none;
}

.icon-option &gt; span {
	display: grid;
	place-items: center;
	min-height: 3rem;
	border: 1px solid var(--line);
	border-radius: var(--radius);
	color: var(--muted);
	background: var(--paper);
}

.icon-option input:checked + span {
	border-color: currentColor;
	color: var(--accent-dark);
	background: var(--surface-soft);
}

.icon-option input:focus-visible + span {
	outline: 3px solid var(--focus-ring);
	outline-offset: 2px;
}
</pre>
<p>Notice that the radio input itself is invisible but not <code>display: none</code>. It can still receive keyboard focus, and the final rule draws that focus around our custom-looking control.</p>
<h3>Our first JavaScript: players and game state</h3>
<p>Now open <code>tic_tac_toe.js</code>. We wrap everything in a function so our variables do not leak into the global scope:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">// Wrap the demo so its variables do not become global variables.
(function ($) {
	'use strict';

	// Only these local SVG symbols may be used as player tokens.
	const allowedIcons = new Set(['hat', 'car', 'ship', 'boot']);
	const boardSizeClasses =
		'board-size-3 board-size-4 board-size-5 board-size-6';

	// Player data stays together in one small array.
	const players = [
		{ name: 'Player 1', icon: 'hat', wins: 0, isReady: false },
		{ name: 'Player 2', icon: 'car', wins: 0, isReady: false },
	];

	let boardSize = 3;
	let board = [];
	let currentPlayer = 0;
	let roundIsOver = false;
    // The rest of the code...
})(jQuery);
</pre>
<p>The <code>$</code> parameter is our private nickname for <code>jQuery</code>. The four <code>let</code> variables describe the changing state of one round. Keeping that state in JavaScript—rather than trying to reconstruct it from colors and classes in the HTML—will make the game logic much easier to reason about.</p>
<p>We also need to create token images from JavaScript when somebody plays a move or saves their settings:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">	// Make a reusable SVG that points to the local icon file.
	function createIcon(iconName) {
		const svg = document.createElementNS(
			'http://www.w3.org/2000/svg',
			'svg'
		);
		const use = document.createElementNS(
			'http://www.w3.org/2000/svg',
			'use'
		);

		svg.setAttribute('aria-hidden', 'true');
		use.setAttribute('href', `token-icons.svg#token-${iconName}`);
		svg.appendChild(use);

		return svg;
	}
</pre>
<p>SVG elements have their own XML namespace, which is why we use <code>createElementNS()</code> rather than jQuery’s shorter <code>$('&lt;svg&gt;')</code> form here.</p>
<h3>Saving a player</h3>
<p>When a setup form is submitted, we validate it, update the matching player object, and fill the summary:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">	// Save one player's form and update the visible summary.
	function setPlayer(event) {
		event.preventDefault();

		const $form = $(this);
		const playerIndex = Number($form.attr('data-player'));
		const otherPlayerIndex = (playerIndex + 1) % players.length;
		const name = String($form.find('[name="name"]').val()).trim();
		const icon = String($form.find('[name="icon"]:checked').val());
		const $message = $form.find('.form-message');

		$message.text('');

		if (!name) {
			$message.text('Please enter a display name.');
			$form.find('[name="name"]').trigger('focus');
			return;
		}

		if (!allowedIcons.has(icon)) {
			$message.text('Please choose a token icon.');
			return;
		}

		if (
			players[otherPlayerIndex].isReady &amp;&amp;
			players[otherPlayerIndex].icon === icon
		) {
			$message.text(
				'Please choose a token the other player is not using.'
			);
			return;
		}

		players[playerIndex].name = name;
		players[playerIndex].icon = icon;
		players[playerIndex].isReady = true;

		const $playerCard = $form.closest('.player-card');
		const $summary = $playerCard.find('.player-summary');
		$summary.find('.summary-token').empty().append(createIcon(icon));
		$summary.find('.summary-name').text(name);
		$summary.find('.win-count').text(players[playerIndex].wins);
		$summary.prop('hidden', false);
		$form.find('.button--player').text(`Update Player ${playerIndex + 1}`);
		$playerCard.find('.player-settings').prop('open', false);

		// Changing a setup starts a fair, empty round.
		startNewRound();
	}
</pre>
<p>There are two different kinds of validation here. HTML’s <code>required</code> attribute helps the user, but JavaScript still checks the submitted data. The <code>allowedIcons</code> set also makes sure an unexpected token name cannot be turned into an SVG file reference. Finally, the two players cannot choose the same icon; a hat-versus-hat battle would get confusing rather quickly.</p>
<p>The names beginning with <code>$</code>, such as <code>$form</code> and <code>$summary</code>, are not special syntax. They are simply a convention that reminds us those variables contain jQuery objects.</p>
<h2>Requirement 2: a board from 3 × 3 to 6 × 6</h2>
<p>The board starts empty in the HTML because JavaScript is going to build the correct number of cells. Around it we place a size control, a status message, and the end-of-round controls:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="html">&lt;form class="size-control" id="size-form"&gt;
	&lt;div class="range-heading"&gt;
		&lt;label for="board-size"&gt;Board size&lt;/label&gt;
		&lt;output id="board-size-output" for="board-size"&gt;3 × 3&lt;/output&gt;
	&lt;/div&gt;
	&lt;input
		id="board-size"
		name="size"
		type="range"
		min="3"
		max="6"
		step="1"
		value="3"
		list="board-size-ticks"
	/&gt;
	&lt;datalist id="board-size-ticks"&gt;
		&lt;option value="3" label="3"&gt;&lt;/option&gt;
		&lt;option value="4" label="4"&gt;&lt;/option&gt;
		&lt;option value="5" label="5"&gt;&lt;/option&gt;
		&lt;option value="6" label="6"&gt;&lt;/option&gt;
	&lt;/datalist&gt;
	&lt;button class="button button--secondary" type="submit"&gt;
		Set board size
	&lt;/button&gt;
&lt;/form&gt;

&lt;p class="game-status" id="game-status" aria-live="polite"&gt;
	Set both players to begin.
&lt;/p&gt;

&lt;div
	class="board board-size-3"
	id="game-board"
	role="grid"
	aria-label="3 by 3 tic-tac-toe board"
&gt;&lt;/div&gt;

&lt;div class="round-actions" id="round-actions" hidden&gt;
	&lt;p id="round-result"&gt;&lt;/p&gt;
	&lt;button class="button" id="restart-game" type="button"&gt;
		Play another round
	&lt;/button&gt;
&lt;/div&gt;
</pre>
<p><code>&lt;output&gt;</code> is the semantic place to show the range’s current value. The board status is another polite live region, so changes such as “Ada, choose an empty square” and “Grace wins this round!” can be announced by assistive technology.</p>
<h3>Drawing boards with CSS Grid</h3>
<p>The board stays square thanks to <code>aspect-ratio</code>. A size class decides how many rows and columns it has:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="css">.board {
	display: grid;
	gap: 0.4rem;
	width: 100%;
	max-width: 34rem;
	aspect-ratio: 1;
	margin: 0 auto;
}

.board-size-3 {
	grid-template: repeat(3, minmax(0, 1fr)) / repeat(3, minmax(0, 1fr));
}

.board-size-4 {
	grid-template: repeat(4, minmax(0, 1fr)) / repeat(4, minmax(0, 1fr));
}

.board-size-5 {
	grid-template: repeat(5, minmax(0, 1fr)) / repeat(5, minmax(0, 1fr));
}

.board-size-6 {
	grid-template: repeat(6, minmax(0, 1fr)) / repeat(6, minmax(0, 1fr));
}
</pre>
<p>For example, <code>repeat(4, minmax(0, 1fr))</code> means “make four equal tracks, and allow them to shrink all the way down to zero if necessary.” We apply it once before and once after the slash: rows first, columns second.</p>
<p>Each cell is a real <code>&lt;button&gt;</code>, not a clickable <code>&lt;div&gt;</code>. Buttons already work with a mouse, keyboard, and touch, and support the <code>disabled</code> property we need:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="css">.cell {
	display: grid;
	place-items: center;
	min-width: 0;
	min-height: 0;
	width: 100%;
	height: 100%;
	aspect-ratio: 1;
	overflow: hidden;
	padding: 12%;
	border: 1px solid var(--line);
	border-radius: var(--radius);
	color: var(--muted);
	background: var(--paper);
	box-shadow: 0 0.2rem 0.6rem var(--shadow-soft);
}

.cell:not(:disabled):hover {
	border-color: var(--accent);
	background: var(--surface-hover);
	transform: translateY(-2px);
}

.cell svg {
	display: block;
	width: 76%;
	height: 76%;
	max-width: 100%;
	max-height: 100%;
}
</pre>
<h3>Creating the cells</h3>
<p>First we need one small question answered: are both players ready?</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">	// A round can begin only after both setup forms are submitted.
	function playersAreReady() {
		return players.every((player) =&gt; player.isReady);
	}
</pre>
<p><code>every()</code> returns <code>true</code> only if the callback returns true for every item in the array. With that answer, <code>renderBoard()</code> can build the buttons:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">	// Create a fresh board for the selected size.
	function renderBoard() {
		const $board = $('#game-board');
		const canPlay = playersAreReady() &amp;&amp; !roundIsOver;

		$board
			.empty()
			.removeClass(boardSizeClasses)
			.addClass(`board-size-${boardSize}`)
			.attr(
				'aria-label',
				`${boardSize} by ${boardSize} tic-tac-toe board`
			);

		for (let index = 0; index &lt; board.length; index += 1) {
			const row = Math.floor(index / boardSize) + 1;
			const column = (index % boardSize) + 1;
			const $cell = $('&lt;button&gt;', {
				type: 'button',
				class: 'cell',
				'data-index': index,
				'aria-label': `Row ${row}, column ${column}, empty`,
				'aria-disabled': String(!canPlay),
				disabled: !canPlay,
			});

			$board.append($cell);
		}
	}
</pre>
<p>The <code>data-index</code> on each button points to the same position in our <code>board</code> array. On a 3 × 3 board, the indexes look like this:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">0 1 2
3 4 5
6 7 8
</pre>
<p>To find a human-friendly row, we divide by the board size, discard the decimal with <code>Math.floor()</code>, then add one. To find a column, <code>%</code> gives us the remainder after division. That little operator is about to earn its keep.</p>
<h3>Starting a clean round</h3>
<pre class="EnlighterJSRAW" data-enlighter-language="js">	// Reset moves but keep names, tokens, and scores.
	function startNewRound() {
		board = Array(boardSize * boardSize).fill(null);
		currentPlayer = 0;
		roundIsOver = false;
		$('#round-actions').prop('hidden', true);
		renderBoard();

		if (playersAreReady()) {
			showTurn();
		} else {
			$('#game-status').text('Set both players to begin.');
		}
	}
</pre>
<p>For a 3 × 3 game, <code>boardSize * boardSize</code> is 9, so we create an array of nine <code>null</code> values. <code>null</code> means an empty cell; later, <code>0</code> will mean Player 1 owns it and <code>1</code> will mean Player 2 owns it.</p>
<p>The range slider has two jobs. It previews the size while it moves, then applies that size when its form is submitted:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">	// Show the range value while the user moves its thumb.
	function previewBoardSize() {
		const size = Number($(this).val());
		$('#board-size-output').text(`${size} × ${size}`);
	}

	// Apply the range value and rebuild the board.
	function setBoardSize(event) {
		event.preventDefault();

		const boardSizeEl = $('#board-size');
		const requestedSize = Number(boardSizeEl.val());
		boardSize = Math.min(6, Math.max(3, requestedSize));
		boardSizeEl.val(boardSize);
		$('#board-size-output').text(`${boardSize} × ${boardSize}`);
		startNewRound();
	}
</pre>
<p>The HTML slider already has <code>min="3"</code> and <code>max="6"</code>, but the JavaScript clamps the value too. Never assume that HTML controls are the only way data can reach your function.</p>
<h2>Requirement 3: exactly one move per turn</h2>
<p>Showing whose turn it is takes only a few lines:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">	// Update the sentence above the board for the next turn.
	function showTurn() {
		const player = players[currentPlayer];
		$('#game-status').text(`${player.name}, choose an empty square.`);
	}
</pre>
<p>The interesting work happens when a cell is selected:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">	// Put the current player's token into the selected cell.
	function playMove() {
		const $cell = $(this);
		const cellIndex = Number($cell.attr('data-index'));

		if (roundIsOver || board[cellIndex] !== null || !playersAreReady()) {
			return;
		}

		const player = players[currentPlayer];
		board[cellIndex] = currentPlayer;

		$cell
			.append(createIcon(player.icon))
			.addClass(`is-marked player-${currentPlayer === 0 ? 'one' : 'two'}`)
			.attr('aria-label', `${player.name} marked this square`)
			.attr('aria-disabled', 'true')
			.prop('disabled', true);

		const winningLine = findWinningLine(currentPlayer);

		if (winningLine) {
			winningLine.forEach((index) =&gt; {
				$(`#game-board .cell[data-index="${index}"]`).addClass(
					'is-winning'
				);
			});

			player.wins += 1;
			$(`.player-card:eq(${currentPlayer}) .win-count`).text(player.wins);
			finishRound(`${player.name} wins this round!`);
			return;
		}

		if (board.every((cell) =&gt; cell !== null)) {
			finishRound('This round is a draw.');
			return;
		}

		currentPlayer = (currentPlayer + 1) % players.length;
		showTurn();
	}
</pre>
<p>The opening <code>if</code> is a guard clause. If the round is over, the cell is already occupied, or the players are not ready, the function stops immediately. Disabling buttons in the interface is friendly; checking the state again in the function keeps our actual game rules safe.</p>
<p>Once a move is accepted, we update both versions of reality:</p>
<ul>
<li><code>board[cellIndex] = currentPlayer</code> records the move in our data;</li>
<li>the jQuery chain updates what the person sees and what assistive technology announces.</li>
</ul>
<p>Then we check for a win and a draw. Only after both checks fail do we rotate to the next player. <code>(currentPlayer + 1) % players.length</code> produces <code>1</code> after <code>0</code>, then wraps back to <code>0</code> after <code>1</code>. A two-player merry-go-round, powered by arithmetic.</p>
<p>Player colors come from two small modifier classes:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="css">.cell.player-one {
	color: var(--player-one);
}

.cell.player-two {
	color: var(--player-two);
}
</pre>
<p>Because the SVG uses <code>currentColor</code>, changing the cell’s color also changes the token.</p>
<h2>Requirement 4: detect rows, columns, and diagonals</h2>
<p>This is the game’s brainiest part, but it becomes pleasantly small when we split it into two functions.</p>
<p>First, <code>getWinningLines()</code> builds a list of every possible winning sequence for the current board size:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">	// Build one list for every possible winning row, column, and diagonal.
	function getWinningLines() {
		const lines = [];

		for (let row = 0; row &lt; boardSize; row += 1) {
			const rowLine = [];
			const columnLine = [];

			for (let column = 0; column &lt; boardSize; column += 1) {
				rowLine.push(row * boardSize + column);
				columnLine.push(column * boardSize + row);
			}

			lines.push(rowLine, columnLine);
		}

		const diagonalDown = [];
		const diagonalUp = [];

		for (let index = 0; index &lt; boardSize; index += 1) {
			diagonalDown.push(index * boardSize + index);
			diagonalUp.push(index * boardSize + (boardSize - index - 1));
		}

		lines.push(diagonalDown, diagonalUp);
		return lines;
	}
</pre>
<p>For a 3 × 3 board, it returns the equivalent of:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">[
	[0, 1, 2], // top row
	[0, 3, 6], // left column
	[3, 4, 5], // middle row
	[1, 4, 7], // middle column
	[6, 7, 8], // bottom row
	[2, 5, 8], // right column
	[0, 4, 8], // diagonal down
	[2, 4, 6], // diagonal up
];
</pre>
<p>The same formulas also work for 4 × 4, 5 × 5, and 6 × 6. That is much nicer than maintaining four hand-written piles of winning combinations.</p>
<p>Now we ask whether every cell in any line belongs to one player:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">	// Return the winning positions or return undefined when no line matches.
	function findWinningLine(playerIndex) {
		return getWinningLines().find((line) =&gt;
			line.every((cellIndex) =&gt; board[cellIndex] === playerIndex)
		);
	}
</pre>
<p>Read that inside-out:</p>
<ol>
<li><code>every()</code> asks, “Does this player own every position in this line?”</li>
<li><code>find()</code> returns the first line for which the answer is yes.</li>
<li>If no line matches, <code>find()</code> returns <code>undefined</code>, which is false-like in our <code>if (winningLine)</code> check.</li>
</ol>
<p>This also explains why we return the winning line rather than just <code>true</code>: <code>playMove()</code> uses those indexes to decorate the victorious cells.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="css">.cell.is-winning {
	border-color: var(--winner-line);
	background: var(--winner);
	animation: winner-pop 420ms ease both;
}

@keyframes winner-pop {
	50% {
		transform: scale(1.06);
	}
}
</pre>
<p>Tiny victory dance: unlocked.</p>
<h2>Requirement 5: finish wins and draws cleanly</h2>
<p>Whether we have a winner or a draw, the ending behavior is the same. Lock the remaining cells, announce the result, and reveal the replay controls:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">	// End a round and show a replay button.
	function finishRound(message) {
		roundIsOver = true;
		$('#game-board .cell')
			.prop('disabled', true)
			.attr('aria-disabled', 'true');
		$('#game-status').text(message);
		$('#round-result').text(message);
		$('#round-actions').prop('hidden', false);
	}
</pre>
<p>We used <code>.attr()</code> for text attributes and <code>.prop()</code> for live Boolean properties such as <code>disabled</code> and <code>hidden</code>. That distinction is worth remembering when working with forms in jQuery.</p>
<p>The draw check was already tucked into <code>playMove()</code>:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">		if (board.every((cell) =&gt; cell !== null)) {
			finishRound('This round is a draw.');
			return;
		}
</pre>
<p>It runs only after the win check. That order matters because the final empty cell might both fill the board <strong>and</strong> complete a winning line. A win should not be reported as a draw just because the board happens to be full.</p>
<p>Our reusable <code>startNewRound()</code> resets the moves but deliberately leaves <code>players[playerIndex].wins</code> alone. That is how “Play another round” clears the board without losing the match score.</p>
<h2>Connecting all the events</h2>
<p>We now have a collection of useful functions, but nothing calls them yet. Add this at the bottom of <code>tic_tac_toe.js</code>:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">	// Connect page events after the HTML is ready.
	$(function () {
		$('.player-form').on('submit', setPlayer);
		$('#size-form').on('submit', setBoardSize);
		$('#board-size').on('input', previewBoardSize);
		$('#game-board').on('click', '.cell', playMove);
		$('#restart-game').on('click', startNewRound);

		startNewRound();
	});
</pre>
<p><code>$(function () { ... })</code> waits until the document structure is ready. The player forms share a class, so one line connects both of them.</p>
<p>The board click uses <strong>event delegation</strong>:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">$('#game-board').on('click', '.cell', playMove);
</pre>
<p>We attach one listener to the permanent board and ask it to handle clicks coming from <code>.cell</code> buttons. The cells themselves are repeatedly destroyed and rebuilt when the board size or round changes, but this listener survives because <code>#game-board</code> survives. It is a small line with excellent job security.</p>
<p>Finally, the first <code>startNewRound()</code> creates our initial nine-cell board in its disabled state. Once both player forms have been submitted, the same function rebuilds it as a playable board.</p>
<h2>A few finishing CSS details</h2>
<p>The demo keeps its colors and repeated measurements in custom properties. You can change the whole mood of the game from one place:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="css">:root {
	color-scheme: light;
	--ink: oklch(24% 0.025 145);
	--muted: oklch(39% 0.025 145);
	--paper: oklch(100% 0 0);
	--page: oklch(98.5% 0 none);
	--line: oklch(82% 0.018 145);
	--transparent: transparent;
	--accent: oklch(46.9% 0.118 144.856);
	--accent-dark: color-mix(in oklch, var(--accent), var(--ink) 28%);
	--player-one: oklch(40% 0.13 25);
	--player-two: var(--accent);
	--highlight: oklch(57.564% 0.16657 44.302);
	--winner: color-mix(in oklch, var(--accent), var(--panel-surface) 95%);
	--winner-line: color-mix(in oklch, var(--accent), var(--ink) 18%);
	--surface-soft: oklch(97% 0 none);
	--surface-hover: color-mix(in oklch, var(--paper), var(--accent) 8%);
	--panel-surface: color-mix(in srgb, var(--paper) 92%, var(--transparent));
	--panel-line: color-mix(in srgb, var(--line) 85%, var(--transparent));
	--focus-ring: color-mix(in srgb, var(--ink) 35%, var(--transparent));
	--shadow-color: color-mix(in srgb, var(--ink) 10%, var(--transparent));
	--shadow-soft: color-mix(in srgb, var(--ink) 8%, var(--transparent));
	--button-shadow: color-mix(in srgb, var(--accent) 20%, var(--transparent));
	--shadow: 0 1rem 2.5rem var(--shadow-color);
	--radius: 0.25rem;
	--radius-round: 50%;
	font-family: Verdana, Arial, ui-sans-serif, "Segoe UI", sans-serif;
}
</pre>
<p>The demo also has one broad focus rule so keyboard users can always see where they are:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="css">input[type="text"]:focus-visible,
input[type="range"]:focus-visible,
.player-card__heading:focus-visible,
.icon-option input:focus-visible + span,
.button:focus-visible,
.cell:focus-visible {
	outline: 3px solid var(--focus-ring);
	outline-offset: 2px;
}
</pre>
<p>And because some CSS elsewhere on a site might try to give hidden elements a display value, the demo makes the meaning of <code>hidden</code> non-negotiable:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="css">[hidden] {
	display: none !important;
}
</pre>
<p>That is one of the rare places where <code>!important</code> is doing a clear, tightly scoped job instead of starting a specificity food fight.</p>
<h2>Game over—in the good way</h2>
<p>We now have a complete local two-player game. The HTML gives it meaningful controls, CSS Grid handles both the page and board layouts, our JavaScript array holds the truth about each move, and jQuery keeps the interface updates and event wiring pleasantly compact.</p>
<p>More importantly, the “extra” features did not require separate versions of the game. A 6 × 6 board travels through the same code as a 3 × 3 board. A boot token travels through the same code as a top hat. That is the quiet superpower of keeping state and rules separate from the pixels on screen.</p>
<p>Try the <a href="/demos/creating-a-tic-tac-toe-game-with-jquery/">finished tic-tac-toe demo</a>, then make it yours. Give the tokens ridiculous colors. Add a best-of-five message. Make the winning cells wobble with entirely unreasonable confidence. Small games are wonderful laboratories because breaking them is cheap—and fixing them teaches you a lot.</p>
<ul class="tutorial-resources" aria-label="Tutorial resources">
<li class="tutorial-resource download"><a class="tutorial-resource__link" href="/demos/creating-a-tic-tac-toe-game-with-jquery/creating-a-tic-tac-toe-game-with-jquery.zip?x55466" rel="internal" download="creating-a-tic-tac-toe-game-with-jquery.zip" aria-label="Download the ZIP file for this tutorial"> Zip file for this tutorial </a></li>
<li class="tutorial-resource info"><a class="tutorial-resource__link" href="/demos/creating-a-tic-tac-toe-game-with-jquery" rel="internal" aria-label="Open the demo for this tutorial"> Demo for this tutorial </a></li>
</ul>
<p>Have fun!</p><p>The post <a href="https://webdevplayground.com/tutorials/creating-a-tic-tac-toe-game-with-jquery/">Creating a Tic-Tac-Toe game with jQuery</a> first appeared on <a href="https://webdevplayground.com">WebDevPlayground</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://webdevplayground.com/tutorials/creating-a-tic-tac-toe-game-with-jquery/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Mahjong Game &#8211; Adding Features</title>
		<link>https://webdevplayground.com/2010/05/mahjong-game-adding-features/</link>
					<comments>https://webdevplayground.com/2010/05/mahjong-game-adding-features/#comments</comments>
		
		<dc:creator><![CDATA[adi]]></dc:creator>
		<pubDate>Thu, 20 May 2010 20:07:06 +0000</pubDate>
				<category><![CDATA[Games]]></category>
		<category><![CDATA[Javascript]]></category>
		<category><![CDATA[jQuery]]></category>
		<guid isPermaLink="false">http://www.webdevplayground.com/?p=235</guid>

					<description><![CDATA[<p>Though it&#8217;s been a while, I think the time has come to revisit one of my previous tutorials: the Mahjong game. For those of you that haven&#8217;t read it, the Mahjong tutorial went throughthe stages of creating a very basic Mahjong game: the graphics, the layouts, the boards and all the rest. Since I published [&#8230;]</p>
<p>The post <a href="https://webdevplayground.com/2010/05/mahjong-game-adding-features/">Mahjong Game – Adding Features</a> first appeared on <a href="https://webdevplayground.com">WebDevPlayground</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Though it&#8217;s been a while, I think the time has come to revisit one of my previous tutorials: the Mahjong game. For those of you that haven&#8217;t read it, the Mahjong tutorial went throughthe stages of creating a very basic Mahjong game: the <a href="http://www.webdevplayground.com/2009/10/mahjong-game-the-graphics/">graphics</a>, the <a href="http://www.webdevplayground.com/2009/10/mahjong-game-the-layout/">layouts</a>, the <a href="http://www.webdevplayground.com/2009/11/mahjong-game-the-board/">boards </a>and all the <a href="http://www.webdevplayground.com/2009/11/mahjong-game-the-end/">rest</a>. Since I published this series of tutorials, I&#8217;ve been asked about various of features such as hint and shuffle, and have decided that it&#8217;s about time for me to add them&#8230; so here goes <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f642.png" alt="🙂" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p><span id="more-235"></span></p>
<h3>Argh! Where is that last pair?</h3>
<p>Adding hints to the game is a fairly easy task, since we already have the ability to check which blocks are free. Even better: because we&#8217;ve marked the free blocks with a special class name, retrieving them at a later stage is just a matter of using a selector. Right now you may be thinking that showing a hint is a bit more complicated than just finding the free blocks; after all, a move requires two <i>matching</i> blocks. And you&#8217;re right, there is a bit more to it than that. Let&#8217;s see how we can do it:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="javascript">
function showHint(event){
	event.preventDefault();
	// Get the free blocks
	var $blocks = $(&quot;#game_board .block.free&quot;);
	var foundMatch = false;
	for(var x=0,total=$blocks.length;x&lt;total &amp;&amp; !foundMatch;++x){
		$block = $($blocks.get(x));
		var class_name = ($block.metadata()[&quot;single&quot;] ?
						 $block.metadata()[&quot;group_class&quot;]									
						: $block.metadata()[&quot;block_class&quot;]);
		// Check if this block class has at least two free blocks
		$match = $(&quot;#game_board .block.free.&quot;+class_name);
		if ( $match.length &gt; 1 ){
			$match.filter(&quot;:first,:last&quot;).addClass(&quot;hint&quot;);
			foundMatch = true;
		}
		$block = $match = null;
	}
	$blocks = null;
	return false;
};
</pre>
<p>What we&#8217;ve done here is checked each free block on the board to see if there are more free blocks of the same class. The logic here is very basic: there are no priorities, nor any forward planning. This means that while this method will always find  a free pair if one exists, it will by no means guarantee a &#8220;win&#8221; for the player.</p>
<h3>If I could just move things around a bit&#8230;</h3>
<p>The next feature we&#8217;re going to look at is shuffling the board. In order to shuffle the board we need to change the locations of all the blocks while maintaining the occupied spots in the layout, otherwise things could get a bit weird. But like the hint feature, it&#8217;s a bit more complicated then that. If you followed the previous posts you may remember that each block remembers its index in the layout, its layer, whether it&#8217;s covered or not, its z-index, and some more attributes, some of which help us determine whether the block is free. So in order not to mess up everything, we need to separate the position spot attributes (layer, index, z-index, top, left, covered) from the block attributes. Let&#8217;s see how we can do that:
</p>
<pre class="EnlighterJSRAW" data-enlighter-language="javascript">
function shuffleBoard(event){
	event.preventDefault();
	var spots = [];
	// Get all the blocks
	var $blocks = $(&quot;#game_board .block&quot;);
	$.each($blocks, function(index, el){
		// Save the used position so we can maintain the layout
		$block = $(el);
		var layer = $block.metadata()[&#039;layer&#039;];
		var index = $block.metadata()[&#039;index&#039;];
		spots.push({css: {
			top: $block.css(&quot;top&quot;),
			left: $block.css(&quot;left&quot;),
			&quot;z-index&quot;: $block.css(&quot;z-index&quot;)
		}, layer: layer, index: index, covered: $block.metadata()[&#039;covered&#039;]});
		$block.removeClass(&#039;layer_&#039;+layer).removeClass(&#039;index_&#039;+index);
		$block = null;
	});
	// Now shuffle the positions and return the blocks
	spots = shuffle(spots);
	// Place the blocks again
	$.each($blocks, function(index, el){
		$block = $(el);
		var spotInfo = spots.pop();
		$block.css(spotInfo.css);
		// Alter the block&#039;s saved metadata to match the new position
		var mdata = $block.metadata();
		mdata[&#039;index&#039;] = spotInfo.index;
		mdata[&#039;layer&#039;] = spotInfo.layer;
		mdata[&#039;covered&#039;] = spotInfo.covered;
		$block.data(&quot;metadata&quot;, mdata);
		$block.addClass(&#039;layer_&#039;+spotInfo.layer).addClass(&#039;index_&#039;+spotInfo.index);
		$block = null;
	});
	
	// Check for free blocks again, since we changed everything
	$blocks.removeClass(&quot;free&quot;).removeClass(&quot;hint&quot;);
	$(document).trigger(&quot;changed_num_blocks&quot;);
	
	$blocks = null;
	return false;
}
</pre>
<p>Thats about it: get all the blocks, extract their position information, and shuffle the positions array so we can put the blocks in a different order. After that, since we changed everything, we need to determine which blocks are free in the new order and inform the user of the number of pairs available.</p>
<h3>A new look</h3>
<p>They say you can&#8217;t have a new version without some graphical makeover, so I apologize that the game looks the same&#8230; but I do have a new, beetle-shaped layout for you, courtesy of Maurice de Regt. So head over to the demo and see how it looks :).
</p>
<p>Some more code is required to get the finishing touches and interconnect all the features, but I do believe we&#8217;ve got the core of it covered. As always, you can find the rest of the javascript in the game demo itself:</p>
<p class="download"><a href="/demos/mahjong2/mahjong2.zip?x55466" rel="internal">Zip file for this tutorial</a></p>
<p class="info"><a href="/demos/mahjong2/mahjong2.php" rel="internal">Demo for this tutorial</a></p>
<p>So until next time, have fun!</p>
<p>Adi Gabai</p><p>The post <a href="https://webdevplayground.com/2010/05/mahjong-game-adding-features/">Mahjong Game – Adding Features</a> first appeared on <a href="https://webdevplayground.com">WebDevPlayground</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://webdevplayground.com/2010/05/mahjong-game-adding-features/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
			</item>
		<item>
		<title>Using the keyboard arrow keys</title>
		<link>https://webdevplayground.com/2009/12/using-the-keyboard-arrow-keys/</link>
					<comments>https://webdevplayground.com/2009/12/using-the-keyboard-arrow-keys/#respond</comments>
		
		<dc:creator><![CDATA[adi]]></dc:creator>
		<pubDate>Sat, 05 Dec 2009 09:40:18 +0000</pubDate>
				<category><![CDATA[General]]></category>
		<category><![CDATA[Javascript]]></category>
		<category><![CDATA[jQuery]]></category>
		<guid isPermaLink="false">http://www.webdevplayground.com/?p=222</guid>

					<description><![CDATA[<p>Today&#8217;s post is a rather short and simple one. In most client-based games today, you can move using your keyboard. Browser-based games, however, are usually controlled using the mouse. But though using the arrow or WASD keys to interact with web pages is not too common, it can still be quite useful. In a web [&#8230;]</p>
<p>The post <a href="https://webdevplayground.com/2009/12/using-the-keyboard-arrow-keys/">Using the keyboard arrow keys</a> first appeared on <a href="https://webdevplayground.com">WebDevPlayground</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Today&#8217;s post is a rather short and simple one. In most client-based games today, you can move using your keyboard. Browser-based games, however, are usually controlled using the mouse. But though using the arrow or WASD keys to interact with web pages is not too common, it can still be quite useful.</p>
<p>In a web page, any key you press triggers an event, so using the keyboard to control a browser-based game is as simple as listening for and catching these events. Listening for keyboard events is fairly easy if you&#8217;re using JavaScript, and with jQuery it&#8217;s even easier. Before we get properly started, I&#8217;d like to introduce a very simple and useful jQuery plugin called HotKeys, which can help you attach to events in a simple manner. You can find it <a href="http://code.google.com/p/js-hotkeys/">here</a>, and binding with it is a simple matter of:
</p>
<pre class="EnlighterJSRAW" data-enlighter-language="javascript">
$(document).bind(&#039;keydown&#039;, &#039;Ctrl+c&#039;, fn);
</pre>
<p>Sometimes, however, you want to do things yourself and not use a plugin.<span id="more-222"></span> As I said, that is simple as well. You probably already know how to bind an input box to the Enter key:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="javascript">
$(&quot;#my_input_field&quot;).keydown(function(event){
	If ( event.keyCode == 13 ){
		// Do whatever – enter key was pressed
}
});
</pre>
<p>Binding to other keys is pretty much the same. All you need to know is the code for your wanted key. For example, if you want to support the arrow keys you can use something like this:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="javascript">
$(document).keydown(function(event){
	switch (event.keyCode) {
			case 37:
				moveLeft();
				break
			case 38:
				moveUp();
				break;
			case 39:
				moveRight();
				break;
			case 40:
				moveDown();
				break;
		}});
</pre>
<p>You can also add support for the space key if you want; its code is 32.</p>
<p>&#8217;till next time,<br />
Adi Gabai</p><p>The post <a href="https://webdevplayground.com/2009/12/using-the-keyboard-arrow-keys/">Using the keyboard arrow keys</a> first appeared on <a href="https://webdevplayground.com">WebDevPlayground</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://webdevplayground.com/2009/12/using-the-keyboard-arrow-keys/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Mahjong Game &#8211; The End</title>
		<link>https://webdevplayground.com/2009/11/mahjong-game-the-end/</link>
					<comments>https://webdevplayground.com/2009/11/mahjong-game-the-end/#comments</comments>
		
		<dc:creator><![CDATA[adi]]></dc:creator>
		<pubDate>Sat, 14 Nov 2009 22:20:56 +0000</pubDate>
				<category><![CDATA[Games]]></category>
		<category><![CDATA[Javascript]]></category>
		<category><![CDATA[jQuery]]></category>
		<guid isPermaLink="false">http://www.webdevplayground.com/?p=202</guid>

					<description><![CDATA[<p>Over the course of the previous posts, we&#8217;ve built and set up the entire infrastructure for our mahjong game. All that is left now is to create the actual game logic. Let&#8217;s start by going over the required functionality. What do we have in this game? The blocks are arranged in layers, and there can [&#8230;]</p>
<p>The post <a href="https://webdevplayground.com/2009/11/mahjong-game-the-end/">Mahjong Game – The End</a> first appeared on <a href="https://webdevplayground.com">WebDevPlayground</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Over the course of the previous posts, we&#8217;ve built and set up the entire infrastructure for our mahjong game. All that is left now is to create the actual game logic. Let&#8217;s start by going over the required functionality. What do we have in this game?</p>
<ol>
<li>The blocks are arranged in layers, and there can be multiple layers on a given board</li>
<li>In each turn, the player must select two free, matching blocks to remove them from the board</li>
<li>The game is won when there are no more blocks on the board</li>
<li>The game is lost if there are still blocks on the board, but no moves are available to the player</li>
</ol>
<p>In the previous <a href="http://www.webdevplayground.com/2009/11/mahjong-game-the-board/">post</a>, we created a script tag for each block on the board, to help us to transfer the block metadata to the game client. In order to be able to read this metadata, we are going to use one of my favorite jquery plugins: <span id="more-202"></span><a href="http://plugins.jquery.com/project/metadata">metadata</a>. The metadata plugin can read data about each element from attributes or script tags—in our case we used script tags—so using this method to fetch data is as simple as adding the following line somewhere at the start of the script: <code>$.metadata.setType("elem", "script");</code></p>
<h3>Selecting Blocks</h3>
<p>As stated above, each turn consists of the player selecting two blocks; if the blocks match each other and are not blocked by other blocks, they are removed from the board. Blocks are considered free if they can be moved either left or right without disturbing another block (i.e., there is no block immediately to their left or right, <i>and</i> there is no block above them). Let&#8217;s take a look at how we can check whether a block is free:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="javascript">
function freeToMove(el){
	var isFree = false;
	// If the element was marked free it can&#039;t be changed
	if ( el.is(&quot;.free&quot;) ){ 
		return true;
	}
        // Get some information with the metadata plugin
	var index = el.metadata()[&quot;index&quot;];
	var layer = el.metadata()[&quot;layer&quot;];
	
	// Check the same layer
	var $parent = el.parent(&quot;div#game_board&quot;);
	var cols = $parent.metadata()[&quot;cols&quot;];
	var current_col = (index+1)%cols;
	var same_layer_class = &quot;.layer_&quot;+layer;
	var left_side_class = false;
	var right_side_class = false;
	if ( current_col != 1 ){
		left_side_class = &quot;div.index_&quot;+(index-2)+same_layer_class;
		left_side_class += &quot;,div.index_&quot;+(index-2-cols)+same_layer_class;
		left_side_class += &quot;,div.index_&quot;+(index-2+cols)+same_layer_class;
	} 
	// Make sure there is room for a full block or dont check it
	if ( current_col &lt; (cols - 1) ){
		right_side_class = &quot;div.index_&quot;+(index+2)+same_layer_class;
		right_side_class += &quot;,div.index_&quot;+(index+2-cols)+same_layer_class;
		right_side_class += &quot;,div.index_&quot;+(index+2+cols)+same_layer_class;
	}
	if (( $parent.find(left_side_class).length == 0 ) ||
		( $parent.find(right_side_class).length == 0 )){
			/* 
			 * Nothing found on the right side or on the left side, 
			 * so the element is free from the sides...
			 * But is it free from above?  
			 * The server saved us some work by telling us if the block is
			 * covered or not.
			 */
			if ( el.metadata()[&quot;covered&quot;] ){
				var upper_layer_class = &quot;.layer_&quot;+(layer+1);
				// The blocks above can be in 0.5 distance apart so we need to check a few classes
				// half before, the same, half after and the rows...
			
				var upper_classes = &quot;div.index_&quot;+(index-1)+upper_layer_class; // a bit to the left
				upper_classes += &quot;,div.index_&quot;+(index)+upper_layer_class; // directory above
				upper_classes += &quot;,div.index_&quot;+(index+1)+upper_layer_class; // a bit to the right
				upper_classes += &quot;,div.index_&quot;+(index-cols)+upper_layer_class; // a bit above
				upper_classes += &quot;,div.index_&quot;+(index+cols)+upper_layer_class; // a bit below
				upper_classes += &quot;,div.index_&quot;+(index-1-cols)+upper_layer_class; // a bit above to the left
				upper_classes += &quot;,div.index_&quot;+(index-1+cols)+upper_layer_class; // a bit below to the left
				upper_classes += &quot;,div.index_&quot;+(index+1-cols)+upper_layer_class; // a bit above to the right
				upper_classes += &quot;,div.index_&quot;+(index+1+cols)+upper_layer_class; // a bit below to the right
				// Even one of those blocks will prevent us from taking the block out
				if ( $parent.find(upper_classes).length == 0 ){
					isFree = true;
				}
			} else {
				// Not covered we are ok
				isFree = true;
			}
	}
	$parent = null;
	return isFree;
}
</pre>
<p>Whew, that was a bit long. But at least it wasn&#8217;t complicated: What we did here was check whether the block has already been marked as free (since once a block is marked as free, it can&#8217;t ever be blocked again). If it&#8217;s not already marked as free, we then determine whether there are blocks to the left and right of the block. If there aren&#8217;t, we check the layer above the block, to see whether it&#8217;s covered by a block from that layer.<br />
Now, let&#8217;s see what happens when the user selects a block. To do that, we will use the toggleBlock function:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="javascript">
function toggleBlock(event){
	var $block = $(this);
	// Check if block is free
	var index = $block.metadata()[&quot;index&quot;];
	var layer = $block.metadata()[&quot;layer&quot;];
	$block.toggleClass(&quot;selected&quot;);
	// Did we just marked this block?
	if ( $block.is(&quot;.selected&quot;) ) {
		$(document).trigger(&quot;flipped_block&quot;);
		// Make sure the block is free for marking
		if ( freeToMove($block) ){
			$block.find(&quot;img&quot;).attr(&quot;src&quot;, &quot;images/selected_tile.png&quot;);
			// Check how many blocks selected if pair - act
			var $selected = $block.parent(&quot;div&quot;).find(&quot;.selected&quot;);
			if ($selected.length == 2){
				// Two blocks are free to move and selected, check if we 
				// have a match
				var block_class = $block.metadata()[&quot;single&quot;] ? 
								$block.metadata()[&quot;group_class&quot;] : 
								$block.metadata()[&quot;block_class&quot;];
											
				var $matched_blocks = $selected.filter(&quot;.&quot;+block_class);
				if ( $matched_blocks.length == 2 ){
					$(document).trigger(&quot;found_match&quot;, 
									{elements: $matched_blocks});
				} else {
					$(document).trigger(&quot;not_a_match&quot;, 
									{elements: $selected});
				}
				var $matched_cards = null;
			}			
			$selected = null;
		} else {
			$block.toggleClass(&quot;selected&quot;);
			$(document).trigger(&quot;blockNotFree&quot;, {element: $block});
		}
	} else {
		$block.find(&quot;img&quot;).attr(&quot;src&quot;, &quot;images/hovered_tile.png&quot;);
	}
	
	$block = null;
};
</pre>
<p>The toggleBlock function is basically our &#8220;onClick&#8221;-type function: it will be called every time a block is clicked and select or unselect the block, assuming it&#8217;s not blocked by another. As can be expected, it uses the freeToMove function we just defined to determine this. In addition, if two free blocks are selected, the toggleBlock function will check for a match, and raise the appropriate event. This event will in turn call a function that will handle the blocks&#8217; removal from the board, and check for a win/lose condition.</p>
<h3>Removing Blocks</h3>
<p>Removing blocks from the board and checking if the player won is fairly simple:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="javascript">
function foundMatchingBlocks(event, params){
	var $elements = params.elements;
	$elements.remove();

	// Check if we have blocks left
	if ( $(&quot;#game_board div.block&quot;).length == 0 ){
		// Game won
		$(document).trigger(&quot;game_won&quot;, {});
	}
	// Don&#039;t halt the progress to make this change
	setTimeout(function(){
		$(document).trigger(&quot;changed_num_blocks&quot;);
	}, 1);
	$elements = null;
};
</pre>
<p>One thing worth take a closer look at is the setTimeout function right in the middle: in my experience, when you start operations that can take a while it&#8217;s best to isolate them and separate them from our current call, which requires a more immediate response. You can see that within setTimeout&#8217;s scope we&#8217;re only triggering a custom event. We&#8217;re merely leaving a hook for future features &#8211; specifically, showing how many free blocks remain. This event can take a while to run, since it needs to scan the board for free elements (something that will take some time on IE), so putting these events in a different, unrelated scope can prevent our game from looking unresponsive. Please note that if the event was to trigger something which had to end before we could continue, we would have had to find a different method.</p>
<p>Now that we can handle two matching blocks, we also need to handle cases where the blocks do not match. This can be done using a simple function like this:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="javascript">
function clearMarkedBlocks(event, params){
	params.elements.removeClass(&quot;selected&quot;).find(&quot;img&quot;).attr(&quot;src&quot;, &quot;images/hovered_tile.png&quot;).hide();
};
</pre>
<h3>Someone Won?</h3>
<p>Up until now we&#8217;ve been happily toggling blocks and removing matched pairs, but what do we do if someone won? We&#8217;ve seen that our foundMatchingBlocks function checks whether there are no more blocks left (our winning condition) and triggers the proper event, but what happens then? And also, what if there are no more moves?</p>
<pre class="EnlighterJSRAW" data-enlighter-language="javascript">
function gameWon(){	
	var $game_board = $(&quot;#game_board_container&quot;);
	var $player_won = $(&quot;#player_won&quot;);
	$game_board.hide();
	$player_won.show();
	$(&quot;#start_again&quot;).show();
	$game_board = $player_won = null;
};

function gameOver(){
	var $game_board = $(&quot;#game_board_container&quot;);
	var $player_lost = $(&quot;#player_lost&quot;);
	$game_board.hide();
	$player_lost.show();
	$(&quot;#start_again&quot;).show();
	
	$game_board = $player_lost = null;
};
</pre>
<p>Above are two really simple functions that simply handle the display of winning and losing. But&#8230; wait a minute&#8230; when and how do we check for the losing condition (no matching blocks left)? Well&#8230; remember the setTimeout-triggered event we talked about? Every time we remove blocks from the board we also trigger the changed_num_blocks event, which fires the following function:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="javascript">

function updateFreeBlocks(){
	$(&quot;#availableMoves&quot;).text(&quot;...&quot;);
	setTimeout(function(){
		var free = findFreeBlocks();
		$(&quot;#availableMoves&quot;).text(free);
		if ( !free ){
			$(document).trigger(&quot;game_over&quot;);
		}
	}, 1);	
};
</pre>
<p>The updateFreeBlocks function checks how many matching pairs are left on the board. If there are no matching pairs left, updateFreeBlocks will trigger the game over event; otherwise, it will just update the display with the new number of remaining matches. Because we already know how to tell whether a block is free, checking if we have any matches left is pretty simple. We can do it by mapping the free blocks, and counting for possible pairs, like so:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="javascript">
function findFreeBlocks(){
	var $blocks = $(&quot;#game_board .block&quot;);
	var block_classes = {};
	var pairs = 0;
	$.each($blocks, function(index, el){
		var $block = $(el);
		// Check if the block is free
		if ( freeToMove($block) || $block.is(&quot;.free&quot;) ){
			$block.addClass(&quot;free&quot;);
			var class_name = ($block.metadata()[&quot;single&quot;] ?
								 $block.metadata()[&quot;group_class&quot;]									
								: $block.metadata()[&quot;block_class&quot;]);
			// If free set the counter 
			block_classes[class_name] = ++block_classes[class_name] || 1;
			if ( block_classes[class_name] == 2 ){
				++pairs;
				block_classes[class_name] = null;
				delete block_classes[class_name];
			}
		}
		$block = null;
	});

	$blocks = null;
	return pairs;
}
</pre>
<h3>That&#8217;s Not All</h3>
<p>Some more code is required to get the finishing touches done on the game, but I do believe we&#8217;ve got the core of the game covered. As always, you can find the rest of the javascript in the game demo itself:</p>
<p class="download"><a href="/demos/mahjong/mahjong.zip?x55466" rel="internal">Zip file for this tutorial</a></p>
<p class="info"><a href="/demos/mahjong/mahjong.php" rel="internal">Demo for this tutorial</a></p>
<p>So until next time, have fun!</p>
<p>Adi Gabai</p><p>The post <a href="https://webdevplayground.com/2009/11/mahjong-game-the-end/">Mahjong Game – The End</a> first appeared on <a href="https://webdevplayground.com">WebDevPlayground</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://webdevplayground.com/2009/11/mahjong-game-the-end/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
			</item>
		<item>
		<title>Mahjong Game &#8211; The Board</title>
		<link>https://webdevplayground.com/2009/11/mahjong-game-the-board/</link>
					<comments>https://webdevplayground.com/2009/11/mahjong-game-the-board/#respond</comments>
		
		<dc:creator><![CDATA[adi]]></dc:creator>
		<pubDate>Fri, 06 Nov 2009 15:37:04 +0000</pubDate>
				<category><![CDATA[Games]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[Layout]]></category>
		<guid isPermaLink="false">http://www.webdevplayground.com/?p=173</guid>

					<description><![CDATA[<p>In the previous posts we created most of the graphics needed for the mahjong game, and built the layout files that we will use. Now it&#8217;s time to put those layout files to some use. The Layout Class Our first step now will be to create a Layout class that will read our layout files. [&#8230;]</p>
<p>The post <a href="https://webdevplayground.com/2009/11/mahjong-game-the-board/">Mahjong Game – The Board</a> first appeared on <a href="https://webdevplayground.com">WebDevPlayground</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>In the previous posts we created most of the <a href="http://www.webdevplayground.com/2009/10/mahjong-game-the-graphics/">graphics</a> needed for the mahjong game, and built the <a href="http://www.webdevplayground.com/2009/10/mahjong-game-the-layout/">layout</a> files that we will use. Now it&#8217;s time to put those layout files to some use.<span id="more-173"></span></p>
<h3>The Layout Class</h3>
<p>Our first step now will be to create a Layout class that will read our layout files. Let&#8217;s say we have a mahjong directory in which we&#8217;ve created a layout directory, which in turn contains our layout files.<br />
The constructor for our Layout class can look something like this:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">
function __construct($layout_name) {
	$this-&gt;file = &quot;layouts&quot;.DIRECTORY_SEPARATOR.
                         $layout_name.&quot;.inc.php&quot;;
	$this-&gt;loaded = $this-&gt;load_layout_from_file();
}
</pre>
<p>Every time the class is loaded, it will go and fetch a layout file, which can then be processed like so:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">
private function load_layout_from_file(){
	if ( file_exists($this-&gt;file) ){
		include $this-&gt;file;
		$this-&gt;map = $layout;
		$this-&gt;rows = $rows;
		$this-&gt;cols = $cols;
		return is_array($this-&gt;map);
	} else {
		return FALSE;
	}
}
</pre>
<p>Since our layer file already contains the variables ($layout, $rows, $cols), the process of loading and processing takes little time. By including the file we gain access to those variables, and can save them in our class. The layout variable should always be an array, since the layout itself is basically an array of positions.</p>
<p>At this point we can read the layout file and store it in our class, but we still need to build a game board out of it. To do that, we&#8217;re going to need two more classes: one to handle the board and one to handle a block on the board (AKA tile). But before we rush off to build these classes, let&#8217;s take another look at our layout file, because there&#8217;s quite a bit more information we can get out of it. </p>
<p>Another responsibility of the Layout class is processing the location of each block in each layer of the layout, and providing relevant information about it, such as its position or whether or not it is covered by a higher layer.<br />
Here&#8217;s some code to clarify things:<br />
Numbers of layers in the layout:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">
public function num_of_layers(){
	return count($this-&gt;map);
}
</pre>
<p>The position of a block:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">
public function get_block_coords($index){
	++$index; // Easier to calc
	$b_width = constant(&quot;BLOCK_WIDTH&quot;);
	$b_height = constant(&quot;BLOCK_HEIGHT&quot;);
		
	// Find the index column
	$col = ($index % $this-&gt;cols) ? $index % $this-&gt;cols : $this-&gt;cols;
		
	// Find the index row
	$row = ceil($index / $this-&gt;cols);
		
	// Pieces on the board can be 0.5 apart
	$top = (($row - 1) * (constant(&#039;BLOCK_HEIGHT&#039;) / 2))-(($row-1)*2);
	$left = (($col - 1) * (constant(&#039;BLOCK_WIDTH&#039;) / 2))-(($col-1)*2);
		
	return array(&quot;pos&quot;=&gt;array(&quot;top&quot;=&gt;$top, &quot;left&quot;=&gt;$left));
}
</pre>
<p>In the above method you can see that we&#8217;re placing the blocks according to their index in the array, but since our array has in fact double the actual number of columns and rows, we need to halve the result in order to get the right position. The end result of this method is the left and top positions of the block&#8217;s html element. </p>
<p>In addition to providing the positions of the separate blocks, the Layout class also needs to know how to provide the coordinates of the entire layer. We can do this by adding the following loop:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">
public function get_active_coords_by_layer($layer){
	$active = $this-&gt;get_active_indexes_by_layer($layer);
	$coords = array();
	for($i=0,$total=count($active);$i &lt; $total;++$i){
		$coords[$i] = $this-&gt;get_block_coords($active[$i]);
		// Change the location to give a bit of a 3d look
		$coords[$i][&quot;pos&quot;][&quot;top&quot;] = $coords[$i][&quot;pos&quot;][&quot;top&quot;] - ($layer * 6);
		$coords[$i][&quot;pos&quot;][&quot;left&quot;] = $coords[$i][&quot;pos&quot;][&quot;left&quot;] - ($layer * 6);
		$coords[$i][&quot;layer&quot;] = $layer;
		$coords[$i][&quot;index&quot;] = $active[$i];
		$coords[$i][&quot;zIndex&quot;] = $this-&gt;map[$layer][$active[$i]];
		// Save some of the browser work since we are faster and see if this block is covered.
		// Indexes that might get in the way in upper layers are the ones that are
		// directly above, slightly to the left and slightly to the right of the block
		$covered = false;
		if ($this-&gt;is_block_spot($layer+1, $active[$i]) ||
			$this-&gt;is_block_spot($layer+1, $active[$i]-1) || 
			$this-&gt;is_block_spot($layer+1, $active[$i]+1) ||
			$this-&gt;is_block_spot($layer+1, $active[$i]+$this-&gt;cols) ||
			$this-&gt;is_block_spot($layer+1, $active[$i]-$this-&gt;cols) ||
			$this-&gt;is_block_spot($layer+1, $active[$i]-1+$this-&gt;cols) ||
			$this-&gt;is_block_spot($layer+1, $active[$i]+1+$this-&gt;cols) ||
			$this-&gt;is_block_spot($layer+1, $active[$i]-1-$this-&gt;cols) ||
			$this-&gt;is_block_spot($layer+1, $active[$i]+1-$this-&gt;cols)){
			$covered = true;
		}
		$coords[$i][&quot;covered&quot;] = ($covered) ? &#039;true&#039; : &#039;false&#039;;
	}		
	return $coords;
}
</pre>
<p>What we did here was take the array holding the current layer, filter out the indexes which don&#8217;t contain blocks, so that we&#8217;re left only with cells that actually represent blocks. For each one of these cells, we checked the position value for the block, and used that to see if it is covered by a block in a higher layer. If you&#8217;re wondering how we can filter the active cells, it can be done like this:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">
private function check_has_block($var){
	return($var&gt;0);
}
private function get_active_indexes_by_layer($layer){
	return array_keys(array_filter($this-&gt;map[$layer], array($this, &quot;check_has_block&quot;)));
}
</pre>
<p>First we filter the array to get just the cells whose value is higher than zero, and then we request the keys of those cells. </p>
<p>Now our Layout class can really supply us with what we need in order to build the board, but as you can see it doesn&#8217;t build anything; it just returns an array of coordinates, which will be used to actually build the board (like in the <a href="http://www.webdevplayground.com/2009/09/a-basic-memory-game-with-jquery-and-php/">Memory game</a>). The one that will actually build the board will be the Board class, which will use the Block and Layout classes to generate that bodacious board-building code. And since we&#8217;ve already completed our Layout class, let&#8217;s get started on the Block class.</p>
<h3>The Block Class</h3>
<p>The Block class is pretty simple. Upon its creation it will get its index in the graphic map file we created earlier (i.e., which block it represents), and will then position the background image in the map file. It will do this by checking which row and column it&#8217;s located in; combined with the (already known) block dimensions, this provides the needed information. The block&#8217;s index in the graphics file will also tell the Block class if it&#8217;s a normal numbered block, or a group of special blocks that can exist only once and match each other. </p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">
function __construct($index) {
	$this-&gt;index = ++$index;
		
	// The structure of the image file is known
	$cols = constant(&quot;COLS_IN_IMAGE&quot;);
	$last_row_index = constant(&quot;LAST_ROW_MARKER&quot;);
	$special_group_size = constant(&quot;SPECIAL_GROUP_SIZE&quot;);
	if ( $index &lt;= $last_row_index ){
		$this-&gt;row = ceil($index / $cols);
		$this-&gt;col = ($index % $cols) ? $index % $cols : $cols;
	} else {
		// We are at the last row - special groups
		$this-&gt;row = constant(&quot;ROWS_IN_IMAGE&quot;);;
		$this-&gt;col = $index - $last_row_index;
		$group_index = ceil($this-&gt;col / $special_group_size);
		$this-&gt;css_extra_class = &quot;special_group_&quot;.$group_index;
		$this-&gt;single = TRUE;
	}
	$this-&gt;top = 0 - (($this-&gt;row - 1) * constant(&#039;BLOCK_HEIGHT&#039;));
	$this-&gt;left = 0 - (($this-&gt;col - 1) * constant(&#039;BLOCK_WIDTH&#039;));
	$this-&gt;css_class = $this-&gt;prefix.$index;
}
</pre>
<p>After setting all of these values, the block knows how to generate its css and html markup:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">
public function get_css_block(){
	return &#039;.&#039;.$this-&gt;get_name().&#039;{background-position: &#039;.
		$this-&gt;get_left().&#039;px &#039;.$this-&gt;get_top().&#039;px;}&#039;;
}

public function get_positioned_html_block($params){
	$covered = $params[&quot;covered&quot;];
	$pos = $params[&quot;pos&quot;];
	$layer = $params[&quot;layer&quot;];
	$index = $params[&quot;index&quot;];
	$zIndex = $params[&quot;zIndex&quot;];
		
	$style = &#039; style=&quot;top:&#039;.$pos[&quot;top&quot;].&#039;px;left:&#039;.$pos[&quot;left&quot;].
		 &#039;px;z-index:&#039;.$zIndex.&#039;;&quot; &#039;;
	$classes = &#039;block &#039;.$this-&gt;get_name().&#039; &#039;.
		   $this-&gt;css_extra_class.&#039; index_&#039;.$index.
		   &#039; layer_&#039;.$layer;
	return &quot;r&quot;.
			&#039;&lt;div class=&quot;&#039;.$classes.&#039;&quot;&#039;.$style.&#039;&gt;&#039;.
				&#039;&lt;script type=&quot;data&quot;&gt;&#039;.
					&#039;{index: &#039;.$index.&#039;, layer: &#039;.$layer.
					&#039;, block_class: &#039;&#039;.$this-&gt;css_class.&#039;&#039;&#039;.
					&#039;, group_class: &#039;&#039;.$this-&gt;css_extra_class.
					&#039;&#039;, single: &#039;.
					($this-&gt;single ? &quot;true&quot; : &quot;false&quot;).
					&#039;, covered: &#039;.$covered.&#039;}&#039;.
				&#039;&lt;/script&gt;&#039;.
				&#039;&lt;img src=&quot;images/hovered_tile.png&quot; alt=&quot;&quot;/&gt;&#039;.
			&#039;&lt;/div&gt;&#039;;
}
</pre>
<p>As you can see in the above html method, the block will get its layout information as a parameter just to enable it to generate the markup; as an entity it only knows its type and which graphic to display.</p>
<h3>Finally, the Board Class</h3>
<p>As we&#8217;ve said before, the one that will bring all of the above together and use it all to create the actual board will be the Board class.<br />
When the board is initialized it will gather all the required details:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">
function __construct($layout) {
	$blocks = array();
	$s_blocks = array(); // Some blocks are special
		
	// The css for a generic board needs to be the first so that it can be overriden easily
	$this-&gt;css[] = &quot;.block{&quot;.
		&quot;width: &quot;.(constant(&#039;BLOCK_WIDTH&#039;)).&quot;px;&quot;.
		&quot;height:&quot;.(constant(&#039;BLOCK_HEIGHT&#039;)).&quot;px;&quot;.
		&quot;background: url(&quot;.constant(&#039;TILES_IMAGE&#039;).&quot;) no-repeat scroll;&quot;.
		&quot;background-position: 0px 0px;&quot;.
		&quot;position:absolute;&quot;.
		&quot;float:none;&quot;.
	&quot;}&quot;;
	// We run through all the blocks and get the block object and css for each of them
	// In addition to that, we need to separate the singles from the normal blocks so they will not be duplicated
	for ( $i = 0; $i &lt; constant(&#039;BASE_CARDS&#039;); ++$i ) {
		$block = new Block($i);
		if ( $block-&gt;can_duplicate() ){
			$blocks[] = $block;
		} else {
			$s_blocks[] = $block;
		}
		
		$this-&gt;css[] = $block-&gt;get_css_block();
	}
	// We need to have 2 pairs of each block, for a total of 4
	$this-&gt;blocks = array_merge($blocks, $blocks, $blocks, $blocks);
			
	// The special blocks are not duplicated, but we still need them
	$this-&gt;blocks = array_merge($this-&gt;blocks, $s_blocks);
			
	// Shuffle the blocks to create the order on the board
	shuffle($this-&gt;blocks);
			
	// Get the layout
	$this-&gt;layout = new Layout($layout);
}
</pre>
<p>After the Board class is initialized, we can easily get the html markup and the needed css:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">
function get_css(){
	return implode(&quot;n&quot;,$this-&gt;css);
}
		
function get_html(){
	$cols = $this-&gt;layout-&gt;get_visible_cols();
	$rows = $this-&gt;layout-&gt;get_visible_rows();
	$board_width =  $cols * constant(&quot;BLOCK_WIDTH&quot;);
	$board_height = $rows * constant(&quot;BLOCK_HEIGHT&quot;);
	$board_html = &#039;&lt;div id=&quot;game_board&quot; style=&quot;position:relative;width: &#039;
				.$board_width.&#039;px;height:&#039;.$board_height.&#039;px;&quot;&gt;&#039;;
	$board_html .= &#039;&lt;script type=&quot;data&quot;&gt;{rows: &#039;.$this-&gt;layout-&gt;get_rows().&#039;, &#039;.
				&#039;cols:&#039;.$this-&gt;layout-&gt;get_cols().
				&#039;, layers: &#039;.$this-&gt;layout-&gt;num_of_layers().&#039;}&lt;/script&gt;&#039;;
	$layer = 0; // The first layer
	$blocks_counter = 0;
	for ( $layer=0, $num_of_layers=$this-&gt;layout-&gt;num_of_layers(); $layer &lt; $num_of_layers ; ++$layer){		
		$active_blocks = $this-&gt;layout-&gt;get_active_coords_by_layer($layer);		
		// For each block in this layer
		for ($i=0,$total=count($active_blocks);$i&lt;$total;++$i,++$blocks_counter){
			$block = $this-&gt;get_block($blocks_counter);
			$params = array(&quot;pos&quot;=&gt;$active_blocks[$i][&quot;pos&quot;],
						&quot;index&quot;=&gt;$active_blocks[$i][&quot;index&quot;],
						&quot;layer&quot;=&gt;$layer+1,
						&quot;zIndex&quot;=&gt;$active_blocks[$i][&quot;zIndex&quot;],
						&quot;covered&quot;=&gt;$active_blocks[$i][&quot;covered&quot;]);
			$board_html .= $block-&gt;get_positioned_html_block($params);
		}
	}
	$board_html .= &#039;&lt;/div&gt;&#039;;		
	print $board_html;
}
</pre>
<p>Thats about it. A simple file with a call to the following code can get us a nice looking mahjong game board. But we are still only halfway there: we still need to add the game logic, and we will do just that in our next post.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">
&lt;?php	
	require &quot;block.php&quot;;
	require &quot;layout.php&quot;;
	require &quot;board.php&quot;;
	
	define(&quot;BLOCK_WIDTH&quot;, 54);
	define(&quot;BLOCK_HEIGHT&quot;, 72);
	define(&quot;BASE_CARDS&quot;, 39);
	define(&quot;TILES_IMAGE&quot;, &quot;images/tiles-2.png&quot;);
	define(&quot;COLS_IN_IMAGE&quot;, 9);
	define(&quot;ROWS_IN_IMAGE&quot;, 5);
	define(&quot;LAST_ROW_MARKER&quot;, 35);
	define(&quot;SPECIAL_GROUP_SIZE&quot;, 4);
		
	$layout_name = &quot;classic&quot;;
	if ( $_REQUEST[&quot;layout&quot;] ) $layout_name = $_REQUEST[&quot;layout&quot;];
	else {
		$layouts = array(&quot;classic&quot;, &quot;cat&quot;);
		$layout_name = $layouts[array_rand($layouts, 1)];
	}
	$board = new Board($layout_name);
?&gt;
&lt;style type=&quot;text/css&quot;&gt;
&lt;?php 
	print $board-&gt;get_css();
?&gt;
&lt;/style&gt;
...
&lt;div id=&quot;game_board_container&quot;&gt;
&lt;?php
	print $board-&gt;get_html();
?&gt;
&lt;/div&gt;
</pre>
<p>Until then, have fun!</p>
<p>Adi Gabai</p><p>The post <a href="https://webdevplayground.com/2009/11/mahjong-game-the-board/">Mahjong Game – The Board</a> first appeared on <a href="https://webdevplayground.com">WebDevPlayground</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://webdevplayground.com/2009/11/mahjong-game-the-board/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Mahjong Game &#8211; The Layout</title>
		<link>https://webdevplayground.com/2009/10/mahjong-game-the-layout/</link>
					<comments>https://webdevplayground.com/2009/10/mahjong-game-the-layout/#comments</comments>
		
		<dc:creator><![CDATA[adi]]></dc:creator>
		<pubDate>Fri, 09 Oct 2009 12:16:30 +0000</pubDate>
				<category><![CDATA[Games]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[Layout]]></category>
		<guid isPermaLink="false">http://www.webdevplayground.com/?p=142</guid>

					<description><![CDATA[<p>In the previous post we created most of the graphics needed for the mahjong game. Now, it is time to create the board itself. Since we are dealing with a lot of elements here (at least 144 objects for the tiles alone), it is better to build the layout on the server side. To do [&#8230;]</p>
<p>The post <a href="https://webdevplayground.com/2009/10/mahjong-game-the-layout/">Mahjong Game – The Layout</a> first appeared on <a href="https://webdevplayground.com">WebDevPlayground</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>In the previous <a href="http://www.webdevplayground.com/2009/10/mahjong-game-the-graphics">post</a> we created most of the graphics needed for the mahjong game. Now, it is time to create the board itself.</p>
</p>
<p>Since we are dealing with a lot of elements here (at least 144 objects for the tiles alone), it is better to build the layout on the server side. To do this, we will use PHP. We already know some of the code since we used it in the <a href="http://www.webdevplayground.com/2009/09/a-basic-memory-game-with-jquery-and-php/">Memory game</a> but we have a lot of new additions here.</p>
<ol>
<li>We know we only have one graphic file</li>
<li>The board does not have a quadrangular (square-like) shape.</li>
<li>The board is made up of several layers</li>
</ol>
<p>Before we start, remember: today we&#8217;re only going to create the logic for the creation of the game board; we&#8217;re not going to touch the gameplay logic just yet. Let&#8217;s start by creating a layout file.<span id="more-142"></span> There are number of ways to create a layout file. The one I&#8217;m about to show here is used mainly for its simplicity and is probably not the most efficient way, but it will do for now. We are going to use Excel, or any other application that can generate a csv from a table. I guess I could spend the time to build a layout builder for the game, but as I don&#8217;t have much time to spare, the csv will do for now.</p>
</p>
<p>What I did was to create a &#8220;table&#8221; with 30 columns and 16 rows in order to draw the first layout. Each tile will span 2 cells horizontally and vertically (a 4-cell square), but we will only use the top left cell to represent the tile. In this table we will fill in the first layer of the layout, but before we do that we first need to understand the limitations of our game client. The game is for a web browser, so in order to put one element on top of another element in a browser, we need to make use of positioning and z-index. Positioning allows us to say where we want each element to be, and the z-index allows us to tell the browser which element is higher than another. This wouldn&#8217;t be an issue for us if we were using 2D tiles, but because in the last post we created 3-dimensional tiles, adjacent tiles will by necessity overlap one another, and we will need to avoid situations like this one:<br />
<img decoding="async" src="http://www.webdevplayground.com/demos/mahjong/images/wdp_z-index.png?x55466" alt="Example of correct and incorrect z-index usage" /></p>
</p>
<p>If we left the z-index out of the picture, the browser would render the layout with the default behavior of placing each element on top of the previously created element, but that would leave us little room for adjustment if we needed to place a certain tile on top of another. Such a situation would give us quite a headache, even with the basic layout.</p>
</p>
<p>So now that we know that the z-index is a factor, we can start filling our table. In our table, each cell will contain one of the following: If the cell is not the top left cell of a tile, it will contain a 0; otherwise, it will contain the tile&#8217;s z-index. For the first layout, we&#8217;ll use incrementing z-index values to make it easy to understand; for the second, we will generally use the same small number and only use others when we need to change the default behavior, to make it quicker :). Getting back to our table for the first layer, go ahead and fill out the cells as in this <a title="" class="cboxModal" rel="lightbox[1]" href="http://www.webdevplayground.com/demos/mahjong/images/wdp_classic_layout1.png?x55466" data-rel="lightbox-image-0" data-rl_title="" data-rl_caption="">image</a>.
</p>
</p>
<p>As you can see, the filled blocks kind of look like a turtle, which is the classic layout for mahjong. If you take a closer look at the table, you will notice the value of cell A8 is inconsistent with the rest, since in this position the default behavior is not what we want (we want the tile in A8 to be &#8220;lower&#8221; than the tile in C7, even though it is created after it), and so we use a lower z-index. The same is true for cells AA8 and AC8, except in these cells we will use a higher z-index rather than a lower one (because we want them to be higher than the tile in cell Y9, even though they are created before it). Generally speaking, we only need to tweak the z-index values in situations where the tiles are not flush with each other, like in the picture above. </p>
</p>
<p>Now that we have our first layer, the second is even easier to build. Because each layer&#8217;s tiles are always placed on top of existing tiles in the previous layer(literally on top, not just overlapping like the situations we&#8217;ve had so far), we are limited in where we can place the tiles. Create another table below the first one with the same number of rows and columns and then fill it like in this <a title="" rel="lightbox[1]"  class="cboxModal" href="http://www.webdevplayground.com/demos/mahjong/images/wdp_classic_layout2.png?x55466" data-rel="lightbox-image-1" data-rl_title="" data-rl_caption="">image</a>.</p>
</p>
<p>Now that you&#8217;ve seen how to build the first and second layers, the rest are really easy, but in case you want to see how they&#8217;re done, you can view them in <a title="" rel="lightbox[1]"  class="cboxModal" href="http://www.webdevplayground.com/demos/mahjong/images/wdp_classic_layout3.png?x55466" data-rel="lightbox-image-2" data-rl_title="" data-rl_caption="">level 3</a>, <a name="ph"></a><a title="" rel="lightbox[1]"  class="cboxModal" href="http://www.webdevplayground.com/demos/mahjong/images/wdp_classic_layout4.png?x55466" data-rel="lightbox-image-3" data-rl_title="" data-rl_caption="">level 4</a> and <a name="ph2"></a><a title="" rel="lightbox[1]"  class="cboxModal" href="http://www.webdevplayground.com/demos/mahjong/images/wdp_classic_layout5.png?x55466" data-rel="lightbox-image-4" data-rl_title="" data-rl_caption="">level 5</a>.</p>
</p>
<p>Now that we have all the layers in our spreadsheet, we need to convert them to a format that PHP can work with quickly. To do so, we will use a middle man in the form of a csv (comma-separated values) file. Let&#8217;s create a directory called layout under our mahjong directory. In it we will put all the layout files. Create a new file called classic.inc.php to be included in our php script, and open this file for editing. In the beginning of the file we will have some details about our layout: the number of columns and rows and an array of levels for the layout, like so:</p>
<p><pre class="EnlighterJSRAW" data-enlighter-language="php">
$rows = 16;
$cols = 30;
$layout[0] = array(
	0,0,1,0,2,0,3,0,4,0,5,0,6,0,7,0,8,0,9,0,10,0,11,0,12,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,13,0,14,0,15,0,16,0,17,0,18,0,19,0,20,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,21,0,22,0,23,0,24,0,25,0,26,0,27,0,28,0,29,0,30,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,31,0,32,0,33,0,34,0,35,0,36,0,37,0,38,0,39,0,40,0,41,0,42,0,0,0,0,0,
	30,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,58,0,59,0,
	0,0,46,0,47,0,48,0,49,0,50,0,51,0,52,0,53,0,54,0,55,0,56,0,57,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,58,0,59,0,60,0,61,0,62,0,63,0,64,0,65,0,66,0,67,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,68,0,69,0,70,0,71,0,72,0,73,0,74,0,75,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,76,0,77,0,78,0,79,0,80,0,81,0,82,0,83,0,84,0,85,0,86,0,87,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
);
$layout[1] = array(
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,100,0,100,0,100,0,100,0,100,0,100,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,100,0,100,0,100,0,100,0,100,0,100,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,100,0,100,0,100,0,100,0,100,0,100,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,100,0,100,0,100,0,100,0,100,0,100,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,100,0,100,0,100,0,100,0,100,0,100,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,100,0,100,0,100,0,100,0,100,0,100,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
);
// $layout[2] = ... and so on
</pre>
</p>
<p>To keep the example short I left out some of the levels, but adding them is a simple matter of copying and pasting from your csv file and adding a comma to the end of each line (except for the last line in each layer). What we did here was create a layout array, which holds each layer as an array. Our array is one-dimensional, but since we know how many rows and columns comprise it, we can use that information to &#8220;divide&#8221; the array and build our layout.<br />
Now that we have the php file we are finally ready to read and process it, and this is what we will do in our next post. </p>
</p>
<p class="download">
If you want to see another example of a layout, I created another layout in the shape of a cat. You can download all the relevant files <a href="http://www.webdevplayground.com/demos/mahjong/mahjong_layout_files.zip?x55466">here</a>.</p>
<p>So until next time, have fun</p>
<p>Adi Gabai</p><p>The post <a href="https://webdevplayground.com/2009/10/mahjong-game-the-layout/">Mahjong Game – The Layout</a> first appeared on <a href="https://webdevplayground.com">WebDevPlayground</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://webdevplayground.com/2009/10/mahjong-game-the-layout/feed/</wfw:commentRss>
			<slash:comments>5</slash:comments>
		
		
			</item>
		<item>
		<title>Mahjong Game &#8211; The Graphics</title>
		<link>https://webdevplayground.com/2009/10/mahjong-game-the-graphics/</link>
					<comments>https://webdevplayground.com/2009/10/mahjong-game-the-graphics/#respond</comments>
		
		<dc:creator><![CDATA[adi]]></dc:creator>
		<pubDate>Sun, 04 Oct 2009 20:40:43 +0000</pubDate>
				<category><![CDATA[Games]]></category>
		<category><![CDATA[Graphics]]></category>
		<guid isPermaLink="false">http://www.webdevplayground.com/?p=115</guid>

					<description><![CDATA[<p>The next game we&#8217;re going to build is a Mahjong solitaire game. Mahjong is a tile matching game which I feel is a natural step forward from the Memory game we created in the previous post, so if you haven’t read that post yet, you might want to give it a look. Since this is [&#8230;]</p>
<p>The post <a href="https://webdevplayground.com/2009/10/mahjong-game-the-graphics/">Mahjong Game – The Graphics</a> first appeared on <a href="https://webdevplayground.com">WebDevPlayground</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>The next game we&#8217;re going to build is a Mahjong solitaire game. Mahjong is a tile matching game which I feel is a natural step forward from the Memory game we created in the previous <a href="http://www.webdevplayground.com/2009/09/a-basic-memory-game-with-jquery-and-php/">post</a>, so if you haven’t read that post yet, you might want to give it a look.</p>
<p>Since this is a more complicated game, I decided it would be best to tackle it in several posts instead of just one. In this first post we&#8217;ll create the graphics needed for the game. Let&#8217;s start by considering what this game needs to include. If you search the web for even a few minutes you will find that there are several versions of this game, each with its own set of rules. To make things easier, our own basic version of the game will use simplified versions of some rules, and ignore others (like score) entirely.</p>
<p>So what does that leave us with?<span id="more-115"></span></p>
<ol>
<li>Multiple sets of tiles:<br />
<strong>27 Common tiles</strong>, consisting of 3 sets of numbered tiles, each set with 1-9 images of the set icon (e.g., flowers, cars). A Common tile must be matched to its exact counterpart (e.g., six of cars matches six of cars)</p>
<p><strong>4 Picture tiles</strong>. Picture tiles have only a single large icon on them, and work the same as Common tiles, in that each tile can only be matched to its exact counterpart (e.g., pink clock matches pink clock)</p>
<p><strong>4 Single tiles</strong>. These tiles feature a picture (not an icon) but no numbers, and each tile can be matched to any of the others (e.g., squirrel matches beach)</li>
<li>With the exception of the Single tiles, there are 2 pairs of each tile (144 tiles total)</li>
<li>The board can have several different layouts.</li>
<li>In each turn the player must select two matching tiles, in accordance to the behaviors described above, and the rules described below.</li>
<li>If the player selects two valid, matching tiles, the tiles are removed from the board.</li>
<li>The game is won when there are no more tiles on the board.</li>
<li>The game is lost if there are still tiles on the board, but no more moves are available to the player.</li>
</ol>
<p>As I said, we are going to start by creating the game&#8217;s graphics. Since we have about 35 different tiles with different graphics on them, we will be dealing with a lot of objects, which means we are going to suffer a hit to performance. Therefore, whatever we do next must take performance into account. What does performance have to do with the graphics, you ask? To answer that question, let&#8217;s consider how we did things in the Memory game: In that game, we used separate image files for each card. If we did the same here, we would trigger a lot of different network calls, and even if we used caching it would still be slow, because we&#8217;d have to move around a whole bunch of different graphic files; unlike the Memory game, where we had relatively few cards, and therefore relatively few graphic files (actually, now that I&#8217;ve made that observation, I think I need to go and apply the solution which I&#8217;m about to share to the Memory game, too). To work around the performance issue, we could combine all the tiles into one big graphic file, which we could preload if we wanted. If we used caching, the performance would be even better. Because all of our tiles have the same dimensions, I think it would be very easy to use a single file for all of them.</p>
<p>So let’s get to it. Go ahead and open your favorite graphics software; in this example I’ll be using Adobe Fireworks, but I’m pretty sure any program will do. The first thing we need to create is our basic building block: the tile. Since Mahjong boards have layers, we need to make sure our tiles have a 3D look. To do that, we will draw a rectangle with a height of 68 and a width of 50. I chose white as the background color and a shade of gray for the stroke (or borders): #999999. To get the 3D effect, we will need to give this image some depth. We can do this by drawing a 4 pixel diagonal line from the upper right corner to the lower right side with the same color as the stroke. Do the same at both of the bottom corners. The end result will be a rectangle with 3 diagonal lines coming out of its corners:<br />
<img decoding="async" src="http://www.webdevplayground.com/demos/mahjong/images/wdp_creating_graphic_1.png?x55466" alt="Rectangle with 3 diagonal lines" /><br />
Draw lines to connect the diagonals and create the 3D rectangle:<br />
<img decoding="async" src="http://www.webdevplayground.com/demos/mahjong/images/wdp_creating_graphic_2.png?x55466" alt="Rectangle with 3 connected diagonal lines" /><br />
We now have 2 big empty spaces created by the lines we added. I filled them up with a lighter shade of gray: #cccccc.<br />
So the final image is:<br />
<img decoding="async" src="http://www.webdevplayground.com/demos/mahjong/images/wdp_creating_graphic_3.png?x55466" alt="Full tile" /><br />
YAY! We now have a basic empty tile! We can now create a new file with a height of 360 and a size of 486 and fill it with our new tiles (make sure you are accurate in positioning them side-by-side. The end result should look like this:<br />
<img decoding="async" src="http://www.webdevplayground.com/demos/mahjong/images/wdp_creating_graphic_4.png?x55466" alt="Tiles image file" /><br />
Excellent! We are now ready to fill in the tiles. The structure of this file is as follows: the 3 upper rows contain the Common tiles. The 4th contains two sets of Picture tiles, and the last row contains 4 Single tiles (remember, these tiles will not be duplicated).<br />
For icons you can just use whatever you want. Just make sure you are not violating any copyrights while doing so.<br />
My final image looks like this:<br />
<img decoding="async" src="http://www.webdevplayground.com/demos/mahjong/images/tiles-2.png?x55466" alt="Filled tiles image file" /></p>
<p>Now we have our tiles image ready. In my next post we will take this file and extract single blocks from it for display. We will also create the logic to duplicate the tiles according to our game guidelines.</p>
<p class="info">Remember: When using a lot of images in one webpage it is wise to reduce network call by having one file that includes all the images when possible (at least when they are for the same purpose and are the same size).</p>
<p>So until next time, have fun</p>
<p>Adi Gabai</p><p>The post <a href="https://webdevplayground.com/2009/10/mahjong-game-the-graphics/">Mahjong Game – The Graphics</a> first appeared on <a href="https://webdevplayground.com">WebDevPlayground</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://webdevplayground.com/2009/10/mahjong-game-the-graphics/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>A Basic Memory Game with jQuery and PHP</title>
		<link>https://webdevplayground.com/2009/09/a-basic-memory-game-with-jquery-and-php/</link>
					<comments>https://webdevplayground.com/2009/09/a-basic-memory-game-with-jquery-and-php/#comments</comments>
		
		<dc:creator><![CDATA[adi]]></dc:creator>
		<pubDate>Mon, 21 Sep 2009 18:14:38 +0000</pubDate>
				<category><![CDATA[Games]]></category>
		<category><![CDATA[Javascript]]></category>
		<category><![CDATA[PHP]]></category>
		<guid isPermaLink="false">http://www.webdevplayground.com/?p=90</guid>

					<description><![CDATA[<p>So, I got up this morning to find that my 3.5 year old daughter is busy playing on my laptop. She decided she wants to play so she found the CD for her game, opened the DVD drive and loaded her game. She was very busy playing when I got up, and needless to say [&#8230;]</p>
<p>The post <a href="https://webdevplayground.com/2009/09/a-basic-memory-game-with-jquery-and-php/">A Basic Memory Game with jQuery and PHP</a> first appeared on <a href="https://webdevplayground.com">WebDevPlayground</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>So, I got up this morning to find that my 3.5 year old daughter is busy playing on my laptop. She decided she wants to play so she found the CD for her game, opened the DVD drive and loaded her game. She was very busy playing when I got up, and needless to say I was quite surprised. I figured it was time to create some new games for her, so I thought I&#8217;d start with a basic Memory game (some may know it as Concentration). You know, the one with lots of face down cards where you need to find pairs of matching cards.<span id="more-90"></span></p>
<p>So let&#8217;s break down what&#8217;s in this game:</p>
<ol>
<li><a href="#game_board" rel="internal">Pairs of cards</a></li>
<li><a href="#game_levels" rel="internal">Raising the level increases the number of cards</a></li>
<li><a href="#turns" rel="internal">In each turn the player flips over 2 cards</a></li>
<li><a href="#match" rel="internal">Matching cards are removed from the board</a></li>
<li><a href="#win" rel="internal">The player wins when there are no more cards</a></li>
<li><a href="#effect" rel="internal">To keep things interesting &#8211; some effects</a></li>
</ol>
<p class="info">
We have a lot to cover in this little guide so in order to keep things as short and quick as we can I&#8217;m going to skip some of the phases in building this game. If you want to see those phases, they can all be found in the demo.
</p>
<p>To understand this guide you need to know some basic javascript &#038; jQuery, and some PHP. There are some excellent guides for these topics all over the web to get you started or answer your questions.</p>
<h2><a name="game_board"></a>1. Pairs of cards</h2>
<p>In order to create the cards that will be used in the game I used an array to hold the available image files. I used 20 different image files in order to create a randomness factor for the images (assuming I will use less than 40 cards). When the board needs to be initialized, we select some random elements from the array of image files according to the number of cards we need on the board, and then we create card objects from each file. Since we know we need two of each card on the board, we &#8220;double&#8221; the array by merging it with itself. And now, a quick shuffle and the array for the board is done.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">
// Shuffle the available image files array so  
// we won&#039;t pick the same ones every time
shuffle($card_files);
// Get the card objects
$cards = array();
for ( $i = 0; $i &lt; $num_of_cards; ++$i ){
	$cards[$i] = new Card($card_files[$i]);
	$this-&gt;css[] = $cards[$i]-&gt;get_css_block();
}
// Double the array so that we&#039;ll have pairs
$this-&gt;cards = array_merge($cards, $cards);
	
// Shuffle the cards to create their order on the board
shuffle($this-&gt;cards);
</pre>
<p>What is a card? Well a card will be displayed in the game, we will need to know how to build its css properties in such a way that we will have an element with the right background image and so on. As you can see below, the Card class is quite simple:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">
class Card{	
	private $css_class = &quot;&quot;;
	private $url = &quot;&quot;;
		
	function __construct($url) {
		$this-&gt;url = $url;
		$this-&gt;css_class = $this-&gt;extract_name($url);
	}
		
	function get_name(){
		return $this-&gt;css_class;
	}
		
	function get_css_block(){
		return &quot;n.&quot;.$this-&gt;get_name().&quot;{background:url(&quot;.$this-&gt;url.&quot;) center center no-repeat;}&quot;;
	}
		
	function get_html_simple_block(){
		return &quot;r&lt;div class=&quot;card {toggle:&#039;&quot;.$this-&gt;get_name().&quot;&#039;}&quot;&gt;&lt;/div&gt;&quot;;
	}
		
	function get_html_block(){
		return &quot;r&lt;div class=&quot;card {toggle:&#039;&quot;.$this-&gt;get_name().&quot;&#039;}&quot;&gt;
				r&lt;div class=&quot;off&quot;&gt;&lt;/div&gt;
				r&lt;div class=&quot;on&quot;&gt;&lt;/div&gt;
			&lt;/div&gt;&quot;;
	}
	private function extract_name($str){
		$tmp = pathinfo($str);
		return $tmp[&#039;filename&#039;];
	}
}
</pre>
<p>The Card object creates its css class name based on the file name. It handles the its own html markup and css markup, and that&#8217;s about it. The Board class that we saw pieces from earlier will handle the building of the board&#8217;s html and css using the Card class.</p>
<h2><a name="game_levels"></a>2. Raising the level increases the number of cards</h2>
<p>In the code above we used a variable called num_of_cards. This variable&#8217;s value is determined by the level of the current game: At low levels we will have relatively few cards, but as the level increases, so does the number of cards on the board.<br />
Let&#8217;s look at the code for this:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">
private var $modes = array(6, 8, 10, 12, 15, 18);
...
$num_of_cards = $this-&gt;modes[$level - 1];
</pre>
<p>This code was also taken from the Board class. Let&#8217;s see how it adds up by taking a look at how we initialize the Board class and set the level:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">
$level = 1;

if (isset($_REQUEST[&#039;level&#039;]) ) {
	$level = $_REQUEST[&#039;level&#039;];
		
	$board = new Board($level, $CARDS);
	$_SESSION[&#039;board&#039;] = $board;
} else {
	if (!isset($_SESSION[&#039;board&#039;])) {
		$board = new Board($level, $CARDS);
		$_SESSION[&#039;board&#039;] = $board;
	} else {
		$board = $_SESSION[&#039;board&#039;];
	}
}
</pre>
<p>If the user sends a request for a certain level, we use it for every game he plays from this moment on. If no request is sent, we use the default first level (1). In addition, we can see that we only set the board once per session, unless the a level request is sent, which is almost true (later on we will see that we also reset the board after the player wins). Saving the board in the session is done with the future in mind: as you will see in this post, the logic of matching the cards is carried out, for the moment, by the client. But if you want the game&#8217;s logic to be more secure, it should be carried out in the server, and then the server will need to know how the game board is built. For now, let&#8217;s go back to our subject.</p>
<p>Just before we move on to see how the player turns the cards we need to actually see how we build the board:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="html">
&lt;style&gt;
	&lt;?php 
		print $board-&gt;get_css();
	?&gt;
&lt;/style&gt;
...
&lt;div id=&quot;game_board&quot; style=&quot;width:&lt;?php print $board-&gt;get_cols()*75; ?&gt;px;&quot;&gt;
&lt;?php
	print $board-&gt;get_html();
?&gt;
&lt;/div&gt;
</pre>
<h2><a name="turns"></a>3. In each turn the player flips over 2 cards</h2>
<p>Now that we have our board, it&#8217;s time to add some interactivity and the main gameplay. We will use a special (and excellent) jQuery plugin to handle the flipping for us. You may have noticed before that, when we built the card&#8217;s html block, we actually built a wrapper and two elements inside; one for the back side and one for the front. This markup is recognized by the QuickFlip plugin (which you can get <a href="http://jonraasch.com/blog/quickflip-2-jquery-plugin">here</a>). Now there are two things we need to do. First, we need to register the Click events for the cards. Second, when a card is clicked, we need to trigger the flip and run a check to see if:</p>
<ul>
<li>The two cards match, but there are still cards on the board
<li>The two cards don&#8217;t match
<li>The two cards match, and there are no cards left on the board (i.e., the player has finished the game)
</ul>
<p>Let&#8217;s take a look:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="javascript">
var chk_cards_timeout = null;

$(document).ready(function(){
	$(&quot;.card&quot;).bind(&quot;click&quot;, toggleCard);
	$(&quot;.card&quot;).quickFlip();
});

function toggleCard(event){
	if ( chk_cards_timeout != null ){
		clearTimeout(chk_cards_timeout);
		chk_cards_timeout = null;
		checkCards();
	}
	var $card = $(this);
	if($card.children(&quot;.off&quot;).is(&quot;:visible&quot;)){
		$(document).trigger(&quot;flipping_cards&quot;);
		var num_already_opened = $card.parent(&quot;#game_board&quot;).find(&quot;.card&gt;.on:visible&quot;).length;
		var css_class = $card.metadata()[&quot;toggle&quot;];
		$card.children(&quot;.on&quot;).addClass(css_class);
		$card.quickFlipper();
		
		if ( num_already_opened == 1 ){
			chk_cards_timeout = setTimeout(checkCards, 1000);
		}
	}
	$card = null;
};
</pre>
<p>In order to do its job, QuickFlip will duplicate our elements, so we want our selectors to be more precise. That&#8217;s why we&#8217;re using the direct child (>) directive in our css selector. We can also see the use of the setTimeout function since in case we have two cards turned over, we want to give the user a second or two to look at them before we turn them face down again. We added the card&#8217;s css class to its element according to the metadata which we got when the Board class built the html (metadata is another excellent jquery plugin that can be found <a href="http://plugins.jquery.com/project/metadata">here</a>).<br />
If you noticed the line: $(document).trigger(&#8220;flipping_cards&#8221;); we are also triggering a custom event which we can bind to later with different plugins for the game if we will want to, if you haven&#8217;t already done so you can read about custom events in my previous post: <a href="http://www.webdevplayground.com/2009/09/jquery-custom-events/">jQuery custom events</a>.</p>
<h2><a name="match"></a>4. Matching cards are removed from the board</h2>
<p>Now that the player can flip a card to see its image, we need to check if we have a match or not. If we have a match, the cards will be removed from the board; if we don&#8217;t, the cards will be turned face down again.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="javascript">
$(document).ready(function(){
	$(document).bind(&quot;found_match&quot;, matchingCards);
	$(document).bind(&quot;no_match&quot;, resetOnCards);
});

function checkCards(){
	$on_cards = $(&quot;#game_board .card&gt;.on:visible&quot;);
	if ( $on_cards.length == 2 ){
		$(document).trigger(&quot;player_made_move&quot;);
		// Get the first object css class
		var css_class = $on_cards.parent(&quot;.card&quot;).metadata()[&quot;toggle&quot;];
		$matched_cards = $on_cards.filter(&quot;.&quot;+css_class);
		var event_name = &quot;no_match&quot;;
		if ( $matched_cards.length == 2 ){
			event_name = &quot;found_match&quot;;
		} 
		$(document).trigger(event_name, {css_class: css_class});
		$matched_cards = null;
	}
	clearTimeout(chk_cards_timeout);
	chk_cards_timeout = null;
	$on_cards = null;
};

function resetOnCards(event){
	$cards = $(&quot;.on:visible&quot;);
	$.each($cards, function(index, card){
		$card = $(card);
		var css_class = $card.parent(&quot;.card&quot;).metadata()[&quot;toggle&quot;];
		$card.trigger(&quot;card_closing&quot;);
		$card.removeClass(css_class);
		$card.parent(&quot;.card&quot;).quickFlipper();
		$card = null;
	});
	$cards = null;
};

function matchingCards(event, params){
	$cards = $(&quot;.&quot;+params.css_class+&quot;.on:visible&quot;);
	$.each($cards, function(index, card){
		var $card = $(card);
		$card.trigger(&quot;card_removed&quot;);
		$card.parent(&quot;.card&quot;).unbind(&quot;*&quot;).before(&quot;&lt;div class=&#039;card_ph&#039;&gt;&lt;/div&gt;&quot;).remove();
		$card = null;
	});
	
	$cards_left = $(&quot;#game_board&gt;.card&quot;);
	if ( $cards_left.length == 0 ){
		$(document).trigger(&quot;game_won&quot;, {});
		/* 
		 * quickFlip has a problem when working in IE: when the last 
		 * bound element is removed, a problem which is caused by the 
		 * bound resize event on the window causes
		 * the end game to get stuck when the game is over...
		 */
		$(window).unbind(&quot;resize&quot;);
	}
	$cards_left = $cards = null;
};
</pre>
<h2><a name="win"></a>5. The player wins when there are no more cards</h2>
<p>In  the above code we check if no more cards are left; if that is the case, the player_won event is triggered. When this event fires we need to do several things: We need to notify the server that the player won, and we need to show the player a message letting him know that he won. Let&#8217;s see how it goes:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="javascript">
$(document).ready(function(){
	$(document).bind(&quot;game_won&quot;, gameWon);
});

function gameWon(){
	$.getJSON(document.location.href, {won: 1}, notifiedServerWin);
	var $game_board = $(&quot;#game_board&quot;);
	var $player_won = $(&quot;#player_won&quot;);
	$game_board.hide();
	$player_won.show();
	$game_board = $player_won = null;
};

function notifiedServerWin(data){
	$(&quot;#start_again&quot;).show();
}
</pre>
<p>We can see that we notify the server with a simple ajax request. The server will then reset the board for us and remember the win.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">
if ( isset($_REQUEST[&quot;won&quot;]) ){
	// if we won we need to reset the game board
	unset($_SESSION[&#039;board&#039;]);
	$_SESSION[&#039;games_won&#039;] = ++$_SESSION[&#039;games_won&#039;];
	$response = array(&quot;status&quot; =&gt; &quot;ok&quot;);
	exit(json_encode($response));
}
</pre>
<h2><a name="effect"></a>6. To keep things interesting – some effects</h2>
<p>At this point the game is basically working, and it&#8217;s time to add some sound to it. Usually I absolutely hate sounds in web pages but since this is a game, it&#8217;s allowed :). So in order to add the sound, we are going to use the &#8220;hooks&#8221; we left in the code in the shape of custom events. We are going to use a Flash object with some preloaded mp3s for the sound effects. The communication between the JavaScript and the Flash (actionscript) is pretty simple these days, but it&#8217;s not in the scope of this guide. However, if you want a guide on this let me know in the comments and I&#8217;ll post one. For the purpose of this guide, let&#8217;s assume we have a Flash object that can accept commands and output sounds. We will use <a href="http://code.google.com/p/swfobject/">swfobject</a> to embed the flash object to the page.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="html">
&lt;script type=&quot;text/javascript&quot; src=&quot;swfobject.js&quot;&gt;&lt;/script&gt;
&lt;script type=&quot;text/javascript&quot;&gt;
	var flashvars = false;
	var attributes = {};
	var params = {
	  allowscriptaccess : &quot;always&quot;,
	  wmode : &quot;transparent&quot;,
	  menu: &quot;false&quot;
	};
	swfobject.embedSWF(&quot;sfx.swf&quot;, &quot;sfx_movie&quot;, &quot;1&quot;, &quot;1&quot;, &quot;9.0.0&quot;, 
				&quot;expressInstall.swf&quot;, flashvars, params, attributes);
&lt;/script&gt;
</pre>
<p>Now that&#8217;s done, it time to handle the binding the events and triggering the sounds. We want the sound to play when the player flips a card over, when we wins the game, and when a match is found.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="javascript">
$(document).ready(function(){
	$(document).bind(&quot;flipping_cards.sound&quot;, playFlip);
	$(document).bind(&quot;game_won.sound&quot;, playCheer);
	$(document).bind(&quot;found_match&quot;, playMatch);
});

function playCheer(){
	var sfxMovie=getFlashMovieObject(&quot;sfx_movie&quot;);
	try{
		sfxMovie.cheer();
	} catch(e) {};
};

function playFlip(){
	var sfxMovie=getFlashMovieObject(&quot;sfx_movie&quot;);
	try{
		sfxMovie.flip();
	} catch(e) {};
};

function playMatch(){
	var sfxMovie=getFlashMovieObject(&quot;sfx_movie&quot;);
	try{
		sfxMovie.match();
	} catch(e) {};
};
</pre>
<p>That&#8217;s it! You can check how everything fits in place and view all the code by looking at the demo.<br />
<a name="demo"></a></p>
<p class="download"><a href="/demos/memory/memory_game.zip?x55466" rel="internal">Zip file for this tutorial</a></p>
<p class="info"><a href="/demos/memory/memory_game.php" rel="internal">Demo for this tutorial</a></p>
<p>So until next time, have fun!</p>
<p>Adi Gabai</p><p>The post <a href="https://webdevplayground.com/2009/09/a-basic-memory-game-with-jquery-and-php/">A Basic Memory Game with jQuery and PHP</a> first appeared on <a href="https://webdevplayground.com">WebDevPlayground</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://webdevplayground.com/2009/09/a-basic-memory-game-with-jquery-and-php/feed/</wfw:commentRss>
			<slash:comments>32</slash:comments>
		
		
			</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Minified using Disk

Served from: webdevplayground.com @ 2026-10-14 09:53:35 by W3 Total Cache
-->