Untitled
Anonymous
plain_text
09/14/2026 7:47 AM
26.8 KB
2
Indexable
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Neon Tic-Tac-Toe</title>
<style>
:root {
--bg: #09090b;
--panel: rgba(24, 24, 27, 0.82);
--panel-light: rgba(39, 39, 42, 0.8);
--border: rgba(255, 255, 255, 0.1);
--text: #f4f4f5;
--muted: #a1a1aa;
--x: #22d3ee;
--x-glow: rgba(34, 211, 238, 0.55);
--o: #fb7185;
--o-glow: rgba(251, 113, 133, 0.55);
--success: #a3e635;
}
* {
box-sizing: border-box;
}
body {
min-height: 100vh;
margin: 0;
display: grid;
place-items: center;
padding: 24px;
color: var(--text);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", sans-serif;
background:
radial-gradient(circle at 15% 15%, rgba(34, 211, 238, 0.13), transparent 28%),
radial-gradient(circle at 85% 85%, rgba(251, 113, 133, 0.12), transparent 28%),
var(--bg);
overflow-x: hidden;
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
opacity: 0.16;
background-image:
linear-gradient(rgba(255,255,255,.035) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,.035) 1px, transparent 1px);
background-size: 36px 36px;
mask-image: linear-gradient(to bottom, black, transparent);
}
.app {
width: min(100%, 760px);
position: relative;
}
.header {
text-align: center;
margin-bottom: 24px;
}
.eyebrow {
margin: 0 0 8px;
color: var(--x);
font-size: 0.72rem;
font-weight: 800;
letter-spacing: 0.22em;
text-transform: uppercase;
}
h1 {
margin: 0;
font-size: clamp(2.2rem, 7vw, 4.2rem);
line-height: 0.95;
letter-spacing: -0.07em;
background: linear-gradient(110deg, #fff 20%, var(--x) 48%, var(--o) 80%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.subtitle {
margin: 13px 0 0;
color: var(--muted);
font-size: 0.98rem;
}
.game-shell {
padding: clamp(18px, 4vw, 30px);
border: 1px solid var(--border);
border-radius: 28px;
background: linear-gradient(145deg, rgba(39,39,42,.84), rgba(9,9,11,.88));
box-shadow:
0 30px 90px rgba(0, 0, 0, .45),
inset 0 1px 0 rgba(255,255,255,.07);
backdrop-filter: blur(20px);
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 12px;
justify-content: space-between;
align-items: center;
margin-bottom: 22px;
}
.mode-switcher {
display: flex;
padding: 4px;
gap: 4px;
border-radius: 13px;
background: rgba(0, 0, 0, .24);
border: 1px solid var(--border);
}
button {
border: 0;
color: var(--text);
font: inherit;
cursor: pointer;
}
.mode-button,
.action-button {
padding: 10px 14px;
border-radius: 9px;
font-size: .86rem;
font-weight: 700;
transition: .2s ease;
}
.mode-button {
color: var(--muted);
background: transparent;
}
.mode-button:hover {
color: var(--text);
background: rgba(255,255,255,.06);
}
.mode-button.active {
color: #09090b;
background: #f4f4f5;
box-shadow: 0 4px 18px rgba(255,255,255,.15);
}
.action-button {
background: rgba(255,255,255,.08);
border: 1px solid var(--border);
}
.action-button:hover {
transform: translateY(-2px);
background: rgba(255,255,255,.14);
}
.turn-status {
min-height: 56px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 22px;
padding: 13px 18px;
border: 1px solid var(--border);
border-radius: 15px;
color: var(--muted);
background: rgba(0,0,0,.18);
text-align: center;
font-weight: 700;
}
.turn-status strong {
margin-left: 6px;
}
.turn-x strong {
color: var(--x);
text-shadow: 0 0 18px var(--x-glow);
}
.turn-o strong {
color: var(--o);
text-shadow: 0 0 18px var(--o-glow);
}
.turn-win {
color: var(--success);
border-color: rgba(163, 230, 53, .3);
background: rgba(163, 230, 53, .08);
}
.turn-tie {
color: #facc15;
border-color: rgba(250, 204, 21, .25);
background: rgba(250, 204, 21, .07);
}
.board-wrap {
position: relative;
width: min(100%, 510px);
aspect-ratio: 1;
margin: 0 auto;
}
.board {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: clamp(8px, 2vw, 14px);
width: 100%;
height: 100%;
}
.cell {
position: relative;
display: grid;
place-items: center;
min-width: 0;
border: 1px solid rgba(255,255,255,.1);
border-radius: 20px;
background:
linear-gradient(145deg, rgba(63,63,70,.65), rgba(24,24,27,.9));
box-shadow:
inset 0 1px 0 rgba(255,255,255,.06),
0 7px 22px rgba(0,0,0,.2);
transition: transform .2s ease, border-color .2s ease, background .2s ease;
}
.cell:not(.occupied):not(.disabled):hover {
transform: translateY(-4px) scale(1.025);
border-color: rgba(255,255,255,.3);
background: linear-gradient(145deg, rgba(82,82,91,.7), rgba(39,39,42,.92));
}
.cell:not(.occupied):not(.disabled):active {
transform: scale(.97);
}
.cell.disabled {
cursor: default;
}
.cell.occupied {
cursor: default;
}
.mark {
animation: markIn .34s cubic-bezier(.2, .9, .25, 1.3) both;
font-size: clamp(3.1rem, 14vw, 6.4rem);
font-weight: 900;
line-height: 1;
user-select: none;
}
.mark.x {
color: var(--x);
text-shadow: 0 0 12px var(--x-glow), 0 0 38px var(--x-glow);
}
.mark.o {
color: var(--o);
text-shadow: 0 0 12px var(--o-glow), 0 0 38px var(--o-glow);
}
.cell.winner {
animation: winnerPulse 1s ease-in-out infinite alternate;
z-index: 1;
}
.cell.winner.x {
border-color: var(--x);
background: rgba(34,211,238,.12);
}
.cell.winner.o {
border-color: var(--o);
background: rgba(251,113,133,.12);
}
.winning-line {
position: absolute;
z-index: 3;
height: 6px;
width: 0;
opacity: 0;
border-radius: 999px;
transform-origin: left center;
pointer-events: none;
}
.winning-line.visible {
width: 94%;
opacity: 1;
animation: lineStrike .55s cubic-bezier(.2,.8,.2,1) forwards;
}
.winning-line.x {
background: var(--x);
box-shadow: 0 0 12px var(--x-glow), 0 0 28px var(--x-glow);
}
.winning-line.o {
background: var(--o);
box-shadow: 0 0 12px var(--o-glow), 0 0 28px var(--o-glow);
}
.scoreboard {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-top: 25px;
}
.score-card {
padding: 15px 10px;
text-align: center;
border: 1px solid var(--border);
border-radius: 15px;
background: rgba(0,0,0,.2);
}
.score-card .label {
color: var(--muted);
font-size: .72rem;
font-weight: 800;
letter-spacing: .12em;
text-transform: uppercase;
}
.score-card .value {
display: block;
margin-top: 5px;
font-size: 1.65rem;
font-weight: 900;
}
.score-card.x .value { color: var(--x); }
.score-card.o .value { color: var(--o); }
.score-card.tie .value { color: #facc15; }
.footer-actions {
display: flex;
justify-content: center;
gap: 12px;
margin-top: 22px;
}
.primary {
padding: 13px 20px;
border-radius: 12px;
color: #09090b;
background: linear-gradient(135deg, var(--x), #67e8f9);
box-shadow: 0 8px 25px rgba(34,211,238,.2);
font-weight: 900;
transition: .2s ease;
}
.primary:hover {
transform: translateY(-2px);
box-shadow: 0 12px 30px rgba(34,211,238,.35);
}
.primary:active {
transform: scale(.97);
}
@keyframes markIn {
0% { opacity: 0; transform: scale(.25) rotate(-18deg); }
70% { transform: scale(1.13) rotate(4deg); }
100% { opacity: 1; transform: scale(1) rotate(0); }
}
@keyframes winnerPulse {
from { transform: scale(1); }
to { transform: scale(1.035); }
}
@keyframes lineStrike {
from { clip-path: inset(0 100% 0 0); }
to { clip-path: inset(0 0 0 0); }
}
@media (max-width: 540px) {
body {
padding: 14px;
}
.toolbar {
justify-content: center;
}
.mode-switcher {
width: 100%;
}
.mode-button {
flex: 1;
padding-inline: 8px;
}
.action-button {
width: 100%;
}
.cell {
border-radius: 14px;
}
}
</style>
</head>
<body>
<main class="app">
<header class="header">
<p class="eyebrow">Local Multiplayer</p>
<h1>Tic-Tac-Toe</h1>
<p class="subtitle">Two players. One board. No distractions.</p>
</header>
<section class="game-shell">
<div class="toolbar">
<div class="mode-switcher" aria-label="Game mode">
<button class="mode-button active" data-mode="pvp">2 Players</button>
<button class="mode-button" data-mode="ai">Play vs AI</button>
</div>
<button class="action-button" id="resetScoresButton">
Reset Scores
</button>
</div>
<div class="turn-status turn-x" id="turnStatus" aria-live="polite">
<span id="statusText">Player turn:</span>
<strong id="currentPlayer">X</strong>
</div>
<div class="board-wrap">
<div class="board" id="board" aria-label="Tic-Tac-Toe board">
<button class="cell" data-index="0" aria-label="Top left"></button>
<button class="cell" data-index="1" aria-label="Top middle"></button>
<button class="cell" data-index="2" aria-label="Top right"></button>
<button class="cell" data-index="3" aria-label="Middle left"></button>
<button class="cell" data-index="4" aria-label="Center"></button>
<button class="cell" data-index="5" aria-label="Middle right"></button>
<button class="cell" data-index="6" aria-label="Bottom left"></button>
<button class="cell" data-index="7" aria-label="Bottom middle"></button>
<button class="cell" data-index="8" aria-label="Bottom right"></button>
</div>
<div class="winning-line" id="winningLine"></div>
</div>
<div class="scoreboard" aria-label="Scoreboard">
<div class="score-card x">
<span class="label">X Wins</span>
<span class="value" id="xScore">0</span>
</div>
<div class="score-card tie">
<span class="label">Ties</span>
<span class="value" id="tieScore">0</span>
</div>
<div class="score-card o">
<span class="label">O Wins</span>
<span class="value" id="oScore">0</span>
</div>
</div>
<div class="footer-actions">
<button class="primary" id="restartButton">Restart Game</button>
</div>
</section>
</main>
<script>
const boardElement = document.getElementById("board");
const cells = [...document.querySelectorAll(".cell")];
const winningLine = document.getElementById("winningLine");
const turnStatus = document.getElementById("turnStatus");
const statusText = document.getElementById("statusText");
const currentPlayerElement = document.getElementById("currentPlayer");
const restartButton = document.getElementById("restartButton");
const resetScoresButton = document.getElementById("resetScoresButton");
const xScoreElement = document.getElementById("xScore");
const oScoreElement = document.getElementById("oScore");
const tieScoreElement = document.getElementById("tieScore");
const modeButtons = [...document.querySelectorAll(".mode-button")];
const winningCombinations = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
];
let board = Array(9).fill("");
let currentPlayer = "X";
let gameOver = false;
let gameMode = "pvp";
let scores = {
X: 0,
O: 0,
ties: 0
};
cells.forEach(cell => {
cell.addEventListener("click", () => {
const index = Number(cell.dataset.index);
makeMove(index);
});
});
restartButton.addEventListener("click", restartGame);
resetScoresButton.addEventListener("click", () => {
scores = { X: 0, O: 0, ties: 0 };
updateScores();
restartGame();
});
modeButtons.forEach(button => {
button.addEventListener("click", () => {
gameMode = button.dataset.mode;
modeButtons.forEach(item => {
item.classList.toggle("active", item === button);
});
restartGame();
});
});
function makeMove(index) {
if (gameOver || board[index]) return;
if (gameMode === "ai" && currentPlayer === "O") return;
placeMark(index, currentPlayer);
const result = evaluateBoard(board);
if (result.winner) {
finishGame(result.winner, result.combo);
return;
}
if (result.tie) {
finishTie();
return;
}
currentPlayer = currentPlayer === "X" ? "O" : "X";
updateTurnStatus();
if (gameMode === "ai" && currentPlayer === "O") {
setTimeout(() => {
const aiMove = findBestMove(board);
placeMark(aiMove, "O");
const aiResult = evaluateBoard(board);
if (aiResult.winner) {
finishGame(aiResult.winner, aiResult.combo);
} else if (aiResult.tie) {
finishTie();
} else {
currentPlayer = "X";
updateTurnStatus();
}
}, 380);
}
}
function placeMark(index, player) {
board[index] = player;
const cell = cells[index];
cell.classList.add("occupied", player.toLowerCase());
cell.innerHTML = `<span class="mark ${player.toLowerCase()}">${player}</span>`;
cell.setAttribute("aria-label", `${player} marked this square`);
}
function evaluateBoard(currentBoard) {
for (const combo of winningCombinations) {
const [a, b, c] = combo;
if (
currentBoard[a] &&
currentBoard[a] === currentBoard[b] &&
currentBoard[a] === currentBoard[c]
) {
return {
winner: currentBoard[a],
combo,
tie: false
};
}
}
return {
winner: null,
combo: null,
tie: currentBoard.every(Boolean)
};
}
function finishGame(winner, combo) {
gameOver = true;
scores[winner]++;
updateScores();
combo.forEach(index => {
cells[index].classList.add("winner", winner.toLowerCase());
});
drawWinningLine(combo, winner);
turnStatus.className = "turn-status turn-win";
statusText.textContent = "Winner:";
currentPlayerElement.textContent = winner;
}
function finishTie() {
gameOver = true;
scores.ties++;
updateScores();
turnStatus.className = "turn-status turn-tie";
statusText.textContent = "It's a";
currentPlayerElement.textContent = "Tie";
}
function updateTurnStatus() {
turnStatus.className =
currentPlayer === "X"
? "turn-status turn-x"
: "turn-status turn-o";
if (gameMode === "ai" && currentPlayer === "O") {
statusText.textContent = "AI is thinking";
currentPlayerElement.textContent = "…";
} else {
statusText.textContent =
gameMode === "ai" ? "Your turn:" : "Player turn:";
currentPlayerElement.textContent = currentPlayer;
}
}
function updateScores() {
xScoreElement.textContent = scores.X;
oScoreElement.textContent = scores.O;
tieScoreElement.textContent = scores.ties;
}
function restartGame() {
board = Array(9).fill("");
currentPlayer = "X";
gameOver = false;
cells.forEach(cell => {
cell.className = "cell";
cell.innerHTML = "";
});
winningLine.className = "winning-line";
winningLine.removeAttribute("style");
updateTurnStatus();
}
function drawWinningLine(combo, winner) {
const boardRect = boardElement.getBoundingClientRect();
const firstRect = cells[combo[0]].getBoundingClientRect();
const lastRect = cells[combo[2]].getBoundingClientRect();
const startX =
firstRect.left - boardRect.left + firstRect.width / 2;
const startY =
firstRect.top - boardRect.top + firstRect.height / 2;
const endX =
lastRect.left - boardRect.left + lastRect.width / 2;
const endY =
lastRect.top - boardRect.top + lastRect.height / 2;
const deltaX = endX - startX;
const deltaY = endY - startY;
const length = Math.sqrt(deltaX ** 2 + deltaY ** 2);
const angle = Math.atan2(deltaY, deltaX) * 180 / Math.PI;
winningLine.style.left = `${startX}px`;
winningLine.style.top = `${startY - 3}px`;
winningLine.style.width = `${length}px`;
winningLine.style.transform = `rotate(${angle}deg)`;
winningLine.classList.add("visible", winner.toLowerCase());
}
// Minimax AI: O plays perfectly and X always starts.
function findBestMove(currentBoard) {
let bestScore = -Infinity;
let move;
for (let i = 0; i < currentBoard.length; i++) {
if (!currentBoard[i]) {
currentBoard[i] = "O";
const score = minimax(currentBoard, false, 0);
currentBoard[i] = "";
if (score > bestScore) {
bestScore = score;
move = i;
}
}
}
return move;
}
function minimax(currentBoard, isMaximizing, depth) {
const result = evaluateBoard(currentBoard);
if (result.winner === "O") return 10 - depth;
if (result.winner === "X") return depth - 10;
if (result.tie) return 0;
if (isMaximizing) {
let bestScore = -Infinity;
for (let i = 0; i < currentBoard.length; i++) {
if (!currentBoard[i]) {
currentBoard[i] = "O";
bestScore = Math.max(
bestScore,
minimax(currentBoard, false, depth + 1)
);
currentBoard[i] = "";
}
}
return bestScore;
} else {
let bestScore = Infinity;
for (let i = 0; i < currentBoard.length; i++) {
if (!currentBoard[i]) {
currentBoard[i] = "X";
bestScore = Math.min(
bestScore,
minimax(currentBoard, true, depth + 1)
);
currentBoard[i] = "";
}
}
return bestScore;
}
}
window.addEventListener("resize", () => {
const result = evaluateBoard(board);
if (result.winner) {
drawWinningLine(result.combo, result.winner);
}
});
updateScores();
updateTurnStatus();
</script>
</body>
</html>
Editor is loading...
Leave a Comment