From 690ca4fe4130bf7570f7bc3b7de97629a8c542fc Mon Sep 17 00:00:00 2001 From: Carl Kristian Ortiz <72757862+cikeyz@users.noreply.github.com> Date: Mon, 26 May 2025 00:40:40 +0800 Subject: [PATCH] experiment: add Super TICTACTOE side project --- README.md | 91 +- Super-TTT.html | 2227 +++++++++++++++++++++++++++++++++++++++++++ TTT-Basic-Mode.html | 1866 ++++++++++++++++++++++++++++++++++++ game.js | 682 ------------- index.html | 169 ---- styles.css | 1015 -------------------- 6 files changed, 4097 insertions(+), 1953 deletions(-) create mode 100644 Super-TTT.html create mode 100644 TTT-Basic-Mode.html delete mode 100644 game.js delete mode 100644 index.html delete mode 100644 styles.css diff --git a/README.md b/README.md index fc4a887..fa3faf0 100644 --- a/README.md +++ b/README.md @@ -1,89 +1,6 @@ -# XOXO.array +# Super TICTACTOE (experiment) -

- Browser tic-tac-toe with array-backed state, undo, and a minimax opponent.
- Vanilla HTML, CSS, and JavaScript. No build step. -

+Post-submission playground branched from XOXO.array. Open `Super-TTT.html` in a browser. -

- Live Demo -  ·  - GitHub Pages -  ·  - Quick Start -  ·  - Structure -  ·  - License -

- -

- HTML5 - CSS3 - JavaScript - License MIT -

- -## Contents - -- [Overview](#overview) -- [Features](#features) -- [Quick Start](#quick-start) -- [Project Structure](#project-structure) -- [Other experiments](#other-experiments) -- [License](#license) -- [Course Note](#course-note) - -## Overview - -XOXO.array is a polished tic-tac-toe client that keeps the board, move history, -and win checks in plain JavaScript arrays. Play local two-player or against a -minimax AI, track scores and move timing, and switch themes without leaving the page. - -## Features - -| Feature | Description | -|---------|-------------| -| Array board | 9-cell board with move history for undo | -| Modes | Local two-player or single-player vs minimax AI | -| Scoreboard | Running X/O scores, move count, and session timer | -| Themes | Multiple visual themes from the side panel | -| Controls | New game, undo last move, reset stats | - -## Quick Start - -`ash -git clone https://github.com/cikeyz/xoxo-array.git -cd xoxo-array -python -m http.server 8000 -` - -Open http://127.0.0.1:8000/ - -## Project Structure - -` ext -xoxo-array/ -├── index.html -├── game.js -├── styles.css -├── LICENSE -├── README.md -└── .gitignore -` - -## Other experiments - -| Branch | Notes | -|--------|-------| -| `experiment/super-tictactoe` | Post-course Super TICTACTOE playground. Not the submitted case study; do not merge into `main`. | - -## License - -MIT. See [LICENSE](LICENSE). - -## Course Note - -Built for CMPE 201 (Data Structures and Algorithms), Polytechnic University of -the Philippines, under Engr. Julius S. Cansino. Final project case study. -Published here as a standalone project. +This branch is a side experiment. `main` remains the submitted CMPE 201 case study. +Do not merge this branch into `main`. diff --git a/Super-TTT.html b/Super-TTT.html new file mode 100644 index 0000000..a5b5191 --- /dev/null +++ b/Super-TTT.html @@ -0,0 +1,2227 @@ + + + + + + Super XOXO.array - Multi-Mode Tic-Tac-Toe + + + +
+ +
+ +
+

Super TIC-TAC-TOE

+

Multi-Mode Array Game

+
+ + +
+

Game Mode

+
+ + + + +
+
+ + +
+
+ AI Opponent + +
+
+ Dark Theme + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + +
+ + +
+
+ + +
+ +
+ +
+ + + +
+ + +
+
+ + + +
+
+
+ + +
+ +
+ +
+
X: 0
+
O: 0
+
+ +
+
Current Player: X
+
Time: 00:00:00
+
Moves: 0
+
Classic Mode
+
+
+ + +
+

Move History

+
+
+
+
+ + +
+ + + + \ No newline at end of file diff --git a/TTT-Basic-Mode.html b/TTT-Basic-Mode.html new file mode 100644 index 0000000..dcb5d92 --- /dev/null +++ b/TTT-Basic-Mode.html @@ -0,0 +1,1866 @@ + + + + + + + XOXO.array + + + + + + +
+ +
+ +
+

TIC-TAC-TOE

+

Powered by Arrays

+
+ +
+

SCOREBOARD

+
+
X: 0
+
O: 0
+
+
+ +
+
+
Current Player: X
+
Time: 00:00:00
+
Moves: 0
+
+
+
+ + +
+ +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+ + +
+
+ + + +
+
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+

Move History

+
+
+ + +
+ +
+ + Single Player Mode +
+ +
+ + Dark Mode +
+
+
+
+ + + + + \ No newline at end of file diff --git a/game.js b/game.js deleted file mode 100644 index 82b60a0..0000000 --- a/game.js +++ /dev/null @@ -1,682 +0,0 @@ -// Game configuration constants -const BOARD_SIZE = 9; // 3x3 board -const AI_MOVE_DELAY = 500; // 500ms delay for AI moves to make them visible - -class TicTacToe { - constructor() { - // Initialize game state variables - this.board = Array(BOARD_SIZE).fill(''); // Empty board array - this.currentPlayer = 'X'; // X starts first - this.gameOver = false; - this.moveHistory = []; // Array to store move history - this.playerXScore = 0; - this.playerOScore = 0; - this.playerNames = { X: 'X', O: 'O' }; // Default player names - this.timerRunning = false; - this.timeElapsed = 0; - this.singlePlayerMode = false; // Two-player mode by default - this.moves = 0; // Counter for number of moves made - - // Cache for AI minimax algorithm optimization - this.minimaxCache = new Map(); - this.MAX_CACHE_SIZE = 1000; - - // Get and store DOM element references - this.cells = Array.from(document.querySelectorAll('.cell')); - this.newGameButton = document.getElementById('new-game'); - this.undoMoveButton = document.getElementById('undo-move'); - this.resetStatsButton = document.getElementById('reset-stats'); - this.modeSwitch = document.getElementById('single-player-mode'); - this.playerXInput = document.getElementById('player-x'); - this.playerOInput = document.getElementById('player-o'); - this.currentPlayerDisplay = document.getElementById('current-player'); - this.timerDisplay = document.getElementById('timer'); - this.movesDisplay = document.getElementById('moves'); - this.historyText = document.querySelector('.history-container'); - - // Scoreboard element references - this.xScoreDisplay = document.getElementById('x-score'); - this.oScoreDisplay = document.getElementById('o-score'); - - // Utility function for input debouncing - this.debounce = (func, wait) => { - let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; - }; - - // Initialize game components - this.setupEventListeners(); - this.updateScoreDisplay(); - - // Initially disable reset button until first move - this.resetStatsButton.disabled = true; - - // Timer animation frame ID for cleanup - this.timerFrameId = null; - } - - // Set up all event listeners for game interactions - setupEventListeners() { - // Optimize board click handling with event delegation - const gameBoard = document.querySelector('.game-board'); - gameBoard.addEventListener('click', (event) => { - const cell = event.target.closest('.cell'); - if (cell) { - this.handleMove(parseInt(cell.dataset.index)); - } - }); - - // Control buttons event delegation - const controlButtons = document.querySelector('.control-buttons'); - controlButtons.addEventListener('click', (event) => { - const button = event.target.closest('.control-btn'); - if (!button || button.disabled) return; - - switch (button.id) { - case 'new-game': - this.resetBoard(); - break; - case 'undo-move': - this.undoMove(); - break; - case 'reset-stats': - this.resetStats(); - break; - } - }); - - // Player name input handling with debouncing - const updatePlayerName = this.debounce((player, input) => { - const name = this.sanitizeInput(input.value.trim()) || player; - this.playerNames[player] = name; - this.updateScoreDisplay(); - this.updateCurrentPlayerDisplay(); - }, 250); - - // Add input listeners for player names - this.playerXInput.addEventListener('input', () => updatePlayerName('X', this.playerXInput)); - this.playerOInput.addEventListener('input', () => updatePlayerName('O', this.playerOInput)); - - // Game mode toggle listener - this.modeSwitch.addEventListener('change', () => this.toggleGameMode()); - } - - // Update scoreboard display with current scores and player names - updateScoreDisplay() { - const xName = this.playerNames.X || 'X'; - const oName = this.playerNames.O || 'O'; - this.xScoreDisplay.parentElement.innerHTML = `${xName}: ${this.playerXScore}`; - this.oScoreDisplay.parentElement.innerHTML = `${oName}: ${this.playerOScore}`; - - // Re-cache score display elements after innerHTML update - this.xScoreDisplay = document.getElementById('x-score'); - this.oScoreDisplay = document.getElementById('o-score'); - } - - // Update current player display with name and turn indicator - updateCurrentPlayerDisplay() { - this.currentPlayerDisplay.textContent = `Current Player: ${this.playerNames[this.currentPlayer]}`; - this.currentPlayerDisplay.classList.remove('x-turn', 'o-turn'); - this.currentPlayerDisplay.classList.add(this.currentPlayer.toLowerCase() + '-turn'); - } - - // Handle player move on cell click - handleMove(index) { - if (this.board[index] === '' && !this.checkWinner() && !this.gameOver) { - // Start timer on first move - if (!this.timerRunning) { - this.timerRunning = true; - this.startTime = Date.now(); - this.updateTimer(); - } - - // Make the player's move - this.makeMove(index); - - // Handle AI move in single player mode - if (this.singlePlayerMode && - this.currentPlayer === 'O' && - !this.checkWinner() && - this.board.includes('') && - !this.gameOver) { - setTimeout(() => { - if (!this.gameOver) { - this.makeAIMove(); - } - }, AI_MOVE_DELAY); - } - } - } - - // Process a move at the given index - makeMove(index) { - this.updateBoard(index); - this.updateMoveHistory(index); - this.checkGameState(); - - // Enable reset button after first move - this.resetStatsButton.disabled = false; - } - - // Update the game board with the current move - updateBoard(index) { - this.board[index] = this.currentPlayer; - const cell = this.cells[index]; - cell.textContent = this.currentPlayer; - cell.classList.add(this.currentPlayer.toLowerCase()); - - // Update move counter - this.moves++; - this.movesDisplay.textContent = `Moves: ${this.moves}`; - } - - // Record move in history and update display - updateMoveHistory(index) { - const row = Math.floor(index / 3) + 1; - const col = (index % 3) + 1; - const moveTime = this.timerDisplay.textContent.replace('Time: ', ''); - const playerName = this.playerNames[this.currentPlayer]; - const moveText = `[${moveTime}] ${playerName}: (${row},${col})\n`; - this.historyText.textContent += moveText; - this.historyText.scrollTop = this.historyText.scrollHeight; - - // Store move for undo functionality - this.moveHistory.push({ index, player: this.currentPlayer }); - this.undoMoveButton.disabled = false; - } - - // Check game state after each move - checkGameState() { - if (this.checkWinner()) { - this.handleWin(); - } else if (!this.board.includes('')) { - this.handleDraw(); - } else { - this.switchPlayer(); - } - } - - // Switch to the next player - switchPlayer() { - this.currentPlayer = this.currentPlayer === 'X' ? 'O' : 'X'; - this.updateCurrentPlayerDisplay(); - } - - // AI move calculation and execution - makeAIMove() { - const move = this.findBestMove(); - if (move !== null) { - this.makeMove(move); - } - } - - // Find the best move using minimax algorithm with improved evaluation - findBestMove() { - let bestScore = -Infinity; - let bestMoves = []; - let alpha = -Infinity; - let beta = Infinity; - - // First two moves strategy - if (this.moveHistory.length === 0) { - // First move: Take center or corner - return 4; // Always take center first - } else if (this.moveHistory.length === 2) { - // Second move: If center is taken, take corner. If corner is taken, take center - if (this.board[4] === 'X') { - const corners = [0, 2, 6, 8]; - return corners[Math.floor(Math.random() * corners.length)]; - } else { - return 4; - } - } - - // Try each available move - for (let i = 0; i < 9; i++) { - if (this.board[i] === '') { - this.board[i] = 'O'; - let score = this.minimax(this.board, 0, false, alpha, beta) + this.evaluatePosition(i); - this.board[i] = ''; - - if (score > bestScore) { - bestScore = score; - bestMoves = [i]; - } else if (score === bestScore) { - bestMoves.push(i); - } - alpha = Math.max(alpha, bestScore); - } - } - - // If we can win immediately, do it - for (const move of bestMoves) { - this.board[move] = 'O'; - if (this.checkWinnerForMinimax() === 'O') { - this.board[move] = ''; - return move; - } - this.board[move] = ''; - } - - // If opponent can win next move, block it - for (let i = 0; i < 9; i++) { - if (this.board[i] === '') { - this.board[i] = 'X'; - if (this.checkWinnerForMinimax() === 'X') { - this.board[i] = ''; - return i; - } - this.board[i] = ''; - } - } - - // Choose randomly from best moves for less predictability - return bestMoves[Math.floor(Math.random() * bestMoves.length)]; - } - - // Evaluate the strategic value of a position - evaluatePosition(index) { - let score = 0; - const board = this.board; - - // Strategic position values - const positionValues = [ - 5, 3, 5, // Corners are highly valued - 3, 8, 3, // Center is most valuable - 5, 3, 5 // Corners are highly valued - ]; - score += positionValues[index] * 0.5; - - // Check for potential fork opportunities - if (this.canCreateFork(index, 'O')) { - score += 50; - } - - // Block opponent's fork opportunities - if (this.canCreateFork(index, 'X')) { - score += 40; - } - - // Evaluate lines (rows, columns, diagonals) - score += this.evaluateLines(index); - - return score; - } - - // Check if a move can create a fork (two winning opportunities) - canCreateFork(index, player) { - if (this.board[index] !== '') return false; - - this.board[index] = player; - let winningLines = 0; - const lines = [ - [0, 1, 2], [3, 4, 5], [6, 7, 8], // rows - [0, 3, 6], [1, 4, 7], [2, 5, 8], // columns - [0, 4, 8], [2, 4, 6] // diagonals - ]; - - for (const line of lines) { - let playerCount = 0; - let emptyCount = 0; - for (const pos of line) { - if (this.board[pos] === player) playerCount++; - if (this.board[pos] === '') emptyCount++; - } - if (playerCount === 2 && emptyCount === 1) winningLines++; - } - - this.board[index] = ''; - return winningLines >= 2; - } - - // Evaluate potential lines (rows, columns, diagonals) - evaluateLines(index) { - let score = 0; - const lines = this.getLinesForPosition(index); - - for (const line of lines) { - let oCount = 0; - let xCount = 0; - let emptyCount = 0; - - for (const pos of line) { - if (this.board[pos] === 'O') oCount++; - else if (this.board[pos] === 'X') xCount++; - else emptyCount++; - } - - // Evaluate line potential - if (oCount === 2 && emptyCount === 1) score += 30; // Near win - if (xCount === 2 && emptyCount === 1) score += 25; // Block opponent - if (oCount === 1 && emptyCount === 2) score += 5; // Potential line - if (xCount === 1 && emptyCount === 2) score += 3; // Block potential line - } - - return score; - } - - // Get all lines that contain the given position - getLinesForPosition(index) { - const lines = [ - [0, 1, 2], [3, 4, 5], [6, 7, 8], // rows - [0, 3, 6], [1, 4, 7], [2, 5, 8], // columns - [0, 4, 8], [2, 4, 6] // diagonals - ]; - - return lines.filter(line => line.includes(index)); - } - - // Enhanced minimax algorithm with strategic position evaluation - minimax(board, depth, isMaximizing, alpha, beta) { - const boardKey = board.join(''); - - if (this.minimaxCache.has(boardKey)) { - return this.minimaxCache.get(boardKey); - } - - let result = this.checkWinnerForMinimax(); - if (result !== null) { - const score = result === 'O' ? 1000 - depth : depth - 1000; // Much higher stakes - this.minimaxCache.set(boardKey, score); - return score; - } - if (!board.includes('')) { - this.minimaxCache.set(boardKey, 0); - return 0; - } - - if (isMaximizing) { - let bestScore = -Infinity; - for (let i = 0; i < BOARD_SIZE; i++) { - if (board[i] === '') { - board[i] = 'O'; - let score = this.minimax(board, depth + 1, false, alpha, beta); - board[i] = ''; - bestScore = Math.max(score, bestScore); - alpha = Math.max(alpha, bestScore); - if (beta <= alpha) break; - } - } - this.minimaxCache.set(boardKey, bestScore); - return bestScore; - } else { - let bestScore = Infinity; - for (let i = 0; i < BOARD_SIZE; i++) { - if (board[i] === '') { - board[i] = 'X'; - let score = this.minimax(board, depth + 1, true, alpha, beta); - board[i] = ''; - bestScore = Math.min(score, bestScore); - beta = Math.min(beta, bestScore); - if (beta <= alpha) break; - } - } - this.minimaxCache.set(boardKey, bestScore); - return bestScore; - } - } - - // Enhanced win checking for minimax - checkWinnerForMinimax() { - const winPatterns = [ - [0, 1, 2], [3, 4, 5], [6, 7, 8], // rows - [0, 3, 6], [1, 4, 7], [2, 5, 8], // columns - [0, 4, 8], [2, 4, 6] // diagonals - ]; - - for (const pattern of winPatterns) { - const [a, b, c] = pattern; - if (this.board[a] && - this.board[a] === this.board[b] && - this.board[a] === this.board[c]) { - return this.board[a]; - } - } - - return null; - } - - checkWinner() { - const winPatterns = [ - [0, 1, 2], [3, 4, 5], [6, 7, 8], // rows - [0, 3, 6], [1, 4, 7], [2, 5, 8], // columns - [0, 4, 8], [2, 4, 6] // diagonals - ]; - - return winPatterns.some(pattern => { - const [a, b, c] = pattern; - return this.board[a] && - this.board[a] === this.board[b] && - this.board[a] === this.board[c]; - }); - } - - handleWin() { - this.stopTimer(); - const winnerName = this.playerNames[this.currentPlayer]; - const totalTime = this.timerDisplay.textContent.replace('Time: ', ''); - const summaryText = `\n${winnerName} wins!\nTotal Moves: ${this.moves}, Total Time: ${totalTime}\n`; - this.historyText.textContent += summaryText; - - // Update score - if (this.currentPlayer === 'X') { - this.playerXScore++; - } else { - this.playerOScore++; - } - - // Update scoreboard display - this.updateScoreDisplay(); - - // Highlight winning combination - this.highlightWinningCombination(); - - // Show win message - setTimeout(() => { - alert(`${winnerName} wins!`); - this.resetBoard(); - }, 100); - } - - highlightWinningCombination() { - const winPatterns = [ - [0, 1, 2], [3, 4, 5], [6, 7, 8], // rows - [0, 3, 6], [1, 4, 7], [2, 5, 8], // columns - [0, 4, 8], [2, 4, 6] // diagonals - ]; - - for (const pattern of winPatterns) { - const [a, b, c] = pattern; - if (this.board[a] && - this.board[a] === this.board[b] && - this.board[a] === this.board[c]) { - // Highlight winning cells - pattern.forEach(index => { - this.cells[index].style.backgroundColor = - this.currentPlayer === 'O' ? 'var(--o-color)' : 'var(--x-color)'; - this.cells[index].style.color = 'var(--frame-color)'; - }); - break; - } - } - } - - handleDraw() { - this.stopTimer(); - const totalTime = this.timerDisplay.textContent.replace('Time: ', ''); - const summaryText = `\nGame Draw!\nTotal Moves: ${this.moves}, Total Time: ${totalTime}\n`; - this.historyText.textContent += summaryText; - setTimeout(() => { - alert("It's a Draw!"); - this.resetBoard(); - }, 100); - } - - undoMove() { - if (this.moveHistory.length > 0) { - const lastMove = this.moveHistory.pop(); - this.board[lastMove.index] = ''; - const cell = this.cells[lastMove.index]; - cell.textContent = ''; - cell.classList.remove('x', 'o'); - cell.style.backgroundColor = ''; - cell.style.color = ''; - - // Update moves counter - this.moves--; - this.movesDisplay.textContent = `Moves: ${this.moves}`; - - this.currentPlayer = lastMove.player; - this.updateCurrentPlayerDisplay(); - - this.historyText.textContent += "Move undone\n"; - this.historyText.scrollTop = this.historyText.scrollHeight; - - if (this.moveHistory.length === 0) { - this.undoMoveButton.disabled = true; - this.stopTimer(); - this.startTime = null; - this.timerDisplay.textContent = "Time: 00:00:00"; - - // Disable reset button if no moves are left - this.resetStatsButton.disabled = true; - } - } - } - - updateTimer() { - if (this.timerRunning && this.startTime) { - const updateTimerDisplay = () => { - const elapsed = Date.now() - this.startTime; - const minutes = Math.floor(elapsed / 60000); - const seconds = Math.floor((elapsed % 60000) / 1000); - const milliseconds = Math.floor((elapsed % 1000) / 10); - this.timerDisplay.textContent = - `Time: ${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}:${milliseconds.toString().padStart(2, '0')}`; - - if (this.timerRunning) { - this.timerFrameId = requestAnimationFrame(updateTimerDisplay); - } - }; - - this.timerFrameId = requestAnimationFrame(updateTimerDisplay); - } - } - - stopTimer() { - this.timerRunning = false; - if (this.timerFrameId) { - cancelAnimationFrame(this.timerFrameId); - this.timerFrameId = null; - } - } - - startTimer() { - if (!this.timerRunning) { - this.timerRunning = true; - this.startTime = Date.now(); - this.updateTimer(); - } - } - - toggleGameMode() { - this.singlePlayerMode = this.modeSwitch.checked; - if (this.singlePlayerMode) { - this.playerOInput.value = "AI"; - this.playerOInput.disabled = true; - this.playerNames.O = "AI"; - } else { - this.playerOInput.value = "O"; - this.playerOInput.disabled = false; - this.playerNames.O = "O"; - } - } - - resetBoard() { - this.board = Array(BOARD_SIZE).fill(''); - this.currentPlayer = 'X'; - this.moveHistory = []; - this.stopTimer(); - this.startTime = null; - this.timerDisplay.textContent = "Time: 00:00:00"; - this.undoMoveButton.disabled = true; - - // Reset moves counter - this.moves = 0; - this.movesDisplay.textContent = "Moves: 0"; - - // Update current player display - this.updateCurrentPlayerDisplay(); - - // Update player names and scoreboard - this.playerNames.X = this.playerXInput.value.trim() || 'X'; - this.playerNames.O = this.singlePlayerMode ? 'AI' : (this.playerOInput.value.trim() || 'O'); - this.updateScoreDisplay(); - - // Reset cells - this.cells.forEach(cell => { - cell.textContent = ''; - cell.classList.remove('x', 'o'); - cell.style.backgroundColor = ''; - cell.style.color = ''; - }); - - // Add reset message to history with player names - this.historyText.textContent += `\n=== New Game: ${this.playerNames.X} vs ${this.playerNames.O} ===\n`; - this.historyText.scrollTop = this.historyText.scrollHeight; - - // Clear minimax cache on new game - this.clearMinimaxCache(); - } - - resetStats() { - // Reset scores - this.playerXScore = 0; - this.playerOScore = 0; - this.updateScoreDisplay(); - - // Reset timer - this.stopTimer(); - this.timeElapsed = 0; - this.startTime = null; - this.timerDisplay.textContent = "Time: 0:00"; - - // Reset moves - this.moves = 0; - this.movesDisplay.textContent = "Moves: 0"; - - // Clear move history - this.historyText.textContent = "=== Game Stats Reset ===\n"; - this.historyText.scrollTop = this.historyText.scrollHeight; - - // Disable reset button after reset - this.resetStatsButton.disabled = true; - - // Reset the board - this.resetBoard(); - } - - sanitizeInput(input) { - const div = document.createElement('div'); - div.textContent = input; - return div.innerHTML; - } - - clearMinimaxCache() { - if (this.minimaxCache.size > this.MAX_CACHE_SIZE) { - this.minimaxCache.clear(); - } - } -} - -// Initialize the game when the DOM is loaded -document.addEventListener('DOMContentLoaded', () => { - new TicTacToe(); -}); \ No newline at end of file diff --git a/index.html b/index.html deleted file mode 100644 index 6df8de7..0000000 --- a/index.html +++ /dev/null @@ -1,169 +0,0 @@ - - - - - - - XOXO.array - - - - - - -
- -
- -
-

TIC-TAC-TOE

-

Powered by Arrays

-
- -
-

SCOREBOARD

-
-
X: 0
-
O: 0
-
-
- -
-
-
Current Player: X
-
Time: 00:00:00
-
Moves: 0
-
-
-
- - -
- -
- -
- - - -
- -
- - - -
- -
- - - -
-
- - -
-
- - - -
-
-
- - -
- -
-
- - -
-
- - -
-
- - -
-

Move History

-
-
- - -
- -
- - Single Player Mode -
- -
- - Dark Mode -
-
-
-
- - - - - - - \ No newline at end of file diff --git a/styles.css b/styles.css deleted file mode 100644 index 616d979..0000000 --- a/styles.css +++ /dev/null @@ -1,1015 +0,0 @@ -/* Root CSS Variables - Light Theme */ -:root { - /* Color variables */ - --x-color: #FF0000; /* Player X color (Bright Red) */ - --o-color: #0000FF; /* Player O color (Bright Blue) */ - --bg-color: #E8ECF3; /* Background color */ - --frame-color: #FFFFFF; /* Component frame color */ - --text-color: #1A202C; /* Main text color */ - --button-color: #6C5CE7; /* Primary button color */ - --button-hover: #5D4ED6; /* Button hover state */ - --accent-color: #93D7BE; /* Accent highlights */ - --border-color: #E2E8F0; /* Border elements */ - --shadow-color: rgba(108, 92, 231, 0.2); /* Shadow color with opacity */ - - /* Spacing variables for consistent layout */ - --spacing-xs: 4px; - --spacing-sm: 8px; - --spacing-md: 12px; - --spacing-lg: 16px; - --spacing-xl: 20px; - - /* Border styling variables */ - --border-width: 2px; - --border-radius: 12px; - --border-radius-sm: 8px; - --border-radius-lg: 16px; - - /* Shadow effects with layered depth */ - --shadow-main: 0 4px 12px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.04); - --shadow-inner: inset 0 2px 4px rgba(0, 0, 0, 0.06); - --shadow-hover: 0 6px 15px rgba(0, 0, 0, 0.1), 0 3px 6px rgba(0, 0, 0, 0.05); - - /* Other visual effects */ - --gradient-main: linear-gradient(135deg, var(--frame-color), var(--bg-color)); - --transition-main: all 0.3s ease; - - /* Standardized shadow system */ - --shadow-raised: 0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06); - --shadow-inset: inset 0 2px 4px rgba(0, 0, 0, 0.06); - --shadow-hover: 0 6px 12px rgba(0, 0, 0, 0.12), 0 3px 6px rgba(0, 0, 0, 0.08); -} - -/* Dark Theme Variables */ -[data-theme="dark"] { - --bg-color: #0F172A; - --frame-color: #1E293B; - --text-color: #F8FAFC; - --button-color: #6366F1; - --button-hover: #4F46E5; - --accent-color: #475569; - --border-color: #334155; - --shadow-color: rgba(0, 0, 0, 0.3); - --shadow-main: 0 8px 20px var(--shadow-color); - --shadow-inner: inset 0 2px 4px rgba(255, 255, 255, 0.05); - --gradient-main: linear-gradient(135deg, var(--frame-color), var(--bg-color)); - - /* Dark theme shadow adjustments */ - --shadow-raised: 0 4px 6px rgba(0, 0, 0, 0.25), 0 2px 4px rgba(0, 0, 0, 0.15); - --shadow-inset: inset 0 2px 4px rgba(0, 0, 0, 0.2); - --shadow-hover: 0 6px 12px rgba(0, 0, 0, 0.3), 0 3px 6px rgba(0, 0, 0, 0.2); -} - -/* Global Reset and Base Styles */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; - font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; -} - -/* Body Layout */ -body { - background: var(--gradient-main); - min-height: 100vh; - display: flex; - justify-content: center; - align-items: center; - padding: 20px; - color: var(--text-color); -} - -/* Main Container Layout */ -.container { - display: flex; - gap: var(--spacing-xl); - padding: var(--spacing-xl); -} - -/* Left Panel Layout */ -.left-panel { - display: flex; - flex-direction: column; - gap: var(--spacing-xl); - width: 300px; -} - -/* Main Game Area Styling */ -.game-area { - flex: 1; - background-color: var(--frame-color); - padding: var(--spacing-xl); - border-radius: var(--border-radius); - box-shadow: var(--shadow-raised); - display: flex; - flex-direction: column; - align-items: center; - gap: var(--spacing-xl); - min-width: 0; -} - -/* Side Panel Layout */ -.side-panel { - width: 300px; - display: flex; - flex-direction: column; - gap: var(--spacing-xl); - position: sticky; - top: var(--container-padding); -} - -/* Title and Scoreboard Styling */ -.title-container, .score-board { - background: var(--frame-color); - border-radius: var(--border-radius); - padding: var(--spacing-md); - box-shadow: var(--shadow-raised); - width: 100%; -} - -/* Common Panel Styling */ -.game-status, .move-history, .player-names { - background: var(--frame-color); - border-radius: var(--border-radius); - padding: var(--spacing-md); - box-shadow: var(--shadow-raised); -} - -/* Game Title Section */ -.title-container { - text-align: center; - position: relative; - padding: var(--spacing-md) var(--spacing-lg); - margin: 0 auto; - background: var(--frame-color); - border-radius: var(--border-radius); - box-shadow: var(--shadow-raised); - width: 100%; - border: 1px solid var(--border-color); -} - -/* Main Title Styling */ -h1 { - font-size: min(2.0em, 5vw); - font-weight: 900; - text-transform: uppercase; - margin: 0; - padding: 0; - letter-spacing: min(4px, 0.5vw); - white-space: nowrap; - background: linear-gradient(135deg, - var(--x-color) 0%, - #A3B8D9 50%, - var(--o-color) 100% - ); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; - line-height: 1.2; -} - -/* Subtitle Styling */ -.subtitle { - font-size: min(0.95em, 3vw); - font-weight: 500; - background: linear-gradient(135deg, #A3B8D9, var(--button-color)); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; - letter-spacing: min(3px, 0.3vw); - text-transform: uppercase; - white-space: nowrap; -} - -/* Dark Theme Title Adjustments */ -[data-theme="dark"] h1 { - background: linear-gradient(135deg, - var(--x-color) 0%, - #A3B8D9 50%, - var(--o-color) 100% - ); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; -} - -[data-theme="dark"] .subtitle { - background: linear-gradient(135deg, #A3B8D9, var(--button-color)); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; -} - -/* Player Info Section */ -.player-info { - display: flex; - flex-direction: column; - gap: var(--panel-gap); - width: 100%; - max-width: 380px; - align-items: center; -} - -/* Player Names Input Section */ -.player-names { - display: flex; - flex-direction: column; - gap: var(--spacing-md); - padding: var(--spacing-md); - background: var(--frame-color); - border-radius: var(--border-radius); - box-shadow: var(--shadow-main); -} - -/* Player Input Field Container */ -.player-input { - display: flex; - align-items: center; - padding: var(--spacing-md); - background: var(--bg-color); - border-radius: var(--border-radius-sm); - box-shadow: var(--shadow-inset); - border: 2px solid transparent; - transition: var(--transition-main); -} - -/* Player X Input Styling */ -.player-input:first-child { - border-color: var(--x-color); -} - -/* Player O Input Styling */ -.player-input:last-child { - border-color: var(--o-color); -} - -/* Input Labels */ -.player-input label { - white-space: nowrap; - font-weight: 600; - min-width: 85px; - font-size: 0.95em; -} - -/* Player X Label Color */ -.player-input:first-child label { - color: var(--x-color); -} - -/* Player O Label Color */ -.player-input:last-child label { - color: var(--o-color); -} - -/* Input Field Styling */ -.player-input input { - flex: 1; - padding: var(--spacing-sm) var(--spacing-md); - border: none; - border-radius: var(--border-radius-sm); - outline: none; - transition: var(--transition-main); - background-color: var(--frame-color); - color: var(--text-color); - font-size: 0.95em; - min-width: 0; - box-shadow: var(--shadow-main); -} - -/* Input Focus States */ -.player-input input:focus { - box-shadow: 0 0 0 2px var(--button-color); -} - -.player-input:first-child input:focus { - box-shadow: 0 0 0 2px var(--x-color); -} - -.player-input:last-child input:focus { - box-shadow: 0 0 0 2px var(--o-color); -} - -/* Scoreboard Styles */ -.score-board { - background: var(--frame-color); - border-radius: var(--border-radius); - padding: var(--spacing-md); - box-shadow: var(--shadow-raised); - width: 100%; - max-width: 380px; -} - -.score-title { - text-align: center; - font-size: 0.85em; - font-weight: 600; - margin-bottom: 6px; - letter-spacing: 1px; - color: var(--text-color); - opacity: 0.8; - text-transform: uppercase; -} - -.score-container { - display: flex; - flex-direction: column; - gap: var(--spacing-sm); - background: var(--bg-color); - padding: var(--spacing-sm); - border-radius: var(--border-radius-sm); - box-shadow: var(--shadow-inset); -} - -.score { - padding: var(--spacing-sm) var(--spacing-md); - border-radius: var(--border-radius-sm); - font-size: 16px; - font-weight: bold; - text-align: center; - transition: var(--transition-main); - background: #FFFFFF; - box-shadow: var(--shadow-main); - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--spacing-md); - border: 2px solid; -} - -.score span { - background: var(--bg-color); - padding: var(--spacing-xs) var(--spacing-sm); - border-radius: var(--border-radius-sm); - min-width: 28px; - font-size: 0.95em; - box-shadow: var(--shadow-inner); -} - -.x-score { - color: var(--x-color); - border-color: var(--x-color); -} - -.o-score { - color: var(--o-color); - border-color: var(--o-color); -} - -.x-score:hover, .o-score:hover { - transform: translateY(-1px); - box-shadow: var(--shadow-hover); -} - -/* Game Board */ -.game-board { - display: flex; - flex-direction: column; - gap: var(--spacing-md); - width: 100%; - max-width: 500px; - padding: var(--spacing-md); - background: var(--bg-color); - border-radius: var(--border-radius); - box-shadow: var(--shadow-inset); -} - -.board-row { - display: flex; - gap: var(--spacing-md); - justify-content: center; -} - -.cell { - width: 150px; - height: 150px; - border: none; - background-color: var(--frame-color); - border-radius: var(--border-radius-sm); - font-size: 48px; - font-weight: bold; - cursor: pointer; - transition: var(--transition-main); - box-shadow: var(--shadow-main); -} - -.cell:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-hover); - background-color: #FFFFFF; - border: 2px solid var(--button-color); -} - -.cell.x { - color: var(--x-color); - background: linear-gradient(135deg, #FFE8E8, #FFF); - border: 2px solid var(--x-color); -} - -.cell.o { - color: var(--o-color); - background: linear-gradient(135deg, #E8FFF8, #FFF); - border: 2px solid var(--o-color); -} - -/* Game Controls */ -.game-controls { - width: 100%; - max-width: 380px; - padding: var(--spacing-md); - background: var(--bg-color); - border-radius: var(--border-radius); - box-shadow: var(--shadow-inset); -} - -.control-buttons { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: var(--spacing-md); - width: 100%; -} - -.control-btn { - padding: var(--spacing-sm) var(--spacing-md); - border: none; - border-radius: var(--border-radius-sm); - background-color: var(--button-color); - color: white; - font-weight: 600; - cursor: pointer; - transition: var(--transition-main); - box-shadow: var(--shadow-main); - width: 100%; - text-align: center; - white-space: nowrap; - font-size: 0.9em; - height: 38px; - display: flex; - align-items: center; - justify-content: center; -} - -.control-btn:nth-child(2), .control-btn:nth-child(3) { - background-color: var(--accent-color); - color: var(--text-color); -} - -.control-btn:nth-child(2):hover:not(:disabled), -.control-btn:nth-child(3):hover:not(:disabled) { - background: var(--accent-color); - opacity: 0.9; -} - -.control-btn:hover:not(:disabled) { - transform: translateY(-2px); - box-shadow: var(--shadow-hover); - background: var(--button-hover); -} - -.control-btn:disabled { - background: #B0B0B0; /* Gray color for disabled state */ - cursor: not-allowed; - transform: none; - box-shadow: none; - opacity: 0.6; -} - -/* Game Info */ -.game-info { - display: grid; - grid-template-columns: 1fr; - gap: var(--spacing-md); - padding: var(--spacing-md); - background: var(--bg-color); - border-radius: var(--border-radius-sm); - box-shadow: var(--shadow-inset); -} - -#current-player, #timer, #moves { - padding: var(--spacing-md); - border-radius: var(--border-radius-sm); - background: var(--frame-color); - box-shadow: var(--shadow-main); - font-weight: 600; - color: var(--text-color); - transition: var(--transition-main); - font-size: 0.95em; - text-align: center; - height: 38px; - display: flex; - align-items: center; - justify-content: center; - gap: var(--spacing-md); - border: 2px solid var(--border-color); - position: relative; -} - -#current-player::before { - content: ""; - width: 20px; - height: 20px; - background-color: var(--button-color); - border-radius: 50%; - opacity: 0.8; - position: absolute; - left: var(--spacing-md); -} - -#timer::before { - content: ""; - width: 20px; - height: 20px; - background: var(--button-color); - -webkit-mask: url('data:image/svg+xml,') center/contain; - mask: url('data:image/svg+xml,') center/contain; - opacity: 0.8; - position: absolute; - left: var(--spacing-md); -} - -#moves::before { - content: ""; - width: 20px; - height: 20px; - background: var(--button-color); - -webkit-mask: url('data:image/svg+xml,') center/contain; - mask: url('data:image/svg+xml,') center/contain; - opacity: 0.8; - position: absolute; - left: var(--spacing-md); -} - -#current-player.x-turn::before { - background-color: var(--x-color); - opacity: 1; -} - -#current-player.o-turn::before { - background-color: var(--o-color); - opacity: 1; -} - -/* Dark mode adjustments */ -[data-theme="dark"] #current-player, -[data-theme="dark"] #timer, -[data-theme="dark"] #moves { - background: var(--bg-color); - border-color: var(--border-color); -} - -[data-theme="dark"] #current-player.x-turn { - border-color: var(--x-color); -} - -[data-theme="dark"] #current-player.o-turn { - border-color: var(--o-color); -} - -/* Move History */ -.move-history h2 { - color: var(--text-color); - margin-bottom: 10px; - text-align: center; - font-size: 0.85em; - font-weight: 600; - letter-spacing: 1px; - text-transform: uppercase; - opacity: 0.8; -} - -.history-container { - height: 180px; - overflow-y: auto; - padding: var(--spacing-md); - background: var(--bg-color); - border-radius: var(--border-radius-sm); - box-shadow: var(--shadow-inset); - margin-top: var(--spacing-md); - white-space: pre-line; - line-height: 1.5; - font-size: 0.9em; - min-height: 180px; - border: 2px solid var(--border-color); -} - -/* Switch Styles */ -.switch { - position: relative; - display: inline-block; - width: 60px; - height: 30px; -} - -.switch input { - opacity: 0; - width: 0; - height: 0; -} - -.slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: var(--bg-color); - transition: .4s; - border-radius: 34px; - box-shadow: var(--shadow-inner); - border: 2px solid var(--border-color); -} - -.slider:before { - position: absolute; - content: ""; - height: 22px; - width: 22px; - left: 4px; - background: var(--frame-color); - transition: .4s; - border-radius: 50%; - box-shadow: var(--shadow-main); - font-size: 16px; - text-align: center; - line-height: 22px; - top: 50%; - transform: translateY(-50%); -} - -input:checked + .slider { - background: linear-gradient(135deg, var(--button-color), var(--button-hover)); - border-color: var(--button-color); -} - -input:checked + .slider:before { - transform: translate(26px, -50%); - content: ""; -} - -/* Responsive Design */ -@media (max-width: 1024px) { - .container { - flex-direction: column; - align-items: center; - gap: var(--spacing-lg); - padding: var(--spacing-lg); - } - - .side-panel, .game-area { - width: 100%; - max-width: 380px; - } -} - -@media (max-width: 768px) { - .container { - padding: var(--spacing-md); - gap: var(--spacing-md); - } - - .title-container { - padding: var(--spacing-sm) var(--spacing-md); - } - - h1 { - font-size: 2em; - letter-spacing: 3px; - } - - .subtitle { - font-size: 0.9em; - letter-spacing: 1.5px; - } - - .cell { - width: 80px; - height: 80px; - font-size: 32px; - } - - .control-btn { - height: 34px; - font-size: 0.85em; - } - - .history-container { - height: 150px; - min-height: 150px; - } -} - -@media (max-width: 480px) { - .cell { - width: 70px; - height: 70px; - font-size: 28px; - } - - .control-buttons { - grid-template-columns: 1fr; - } - - .score { - font-size: 14px; - } - - #current-player, #timer, #moves { - font-size: 0.85em; - height: 34px; - } - - h1 { - font-size: 1.8em; - letter-spacing: 2px; - } - - .subtitle { - font-size: 0.8em; - letter-spacing: 1px; - } -} - -/* Touch device optimizations */ -@media (hover: none) { - .cell:hover { - transform: none; - box-shadow: var(--shadow-main); - } - - .control-btn:hover:not(:disabled) { - transform: none; - } -} - -.theme-switch { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 10px; -} - -.theme-switch span { - color: var(--text-color); - font-size: 14px; -} - -/* Dark mode specific overrides */ -[data-theme="dark"] .cell { - background-color: #334155; -} - -[data-theme="dark"] .cell:hover { - background-color: #475569; -} - -[data-theme="dark"] .cell.x { - background: linear-gradient(135deg, rgba(255, 0, 0, 0.15), #334155); -} - -[data-theme="dark"] .cell.o { - background: linear-gradient(135deg, rgba(0, 0, 255, 0.15), #334155); -} - -[data-theme="dark"] .player-input input { - background-color: #334155; - border-color: #475569; - color: #F8FAFC; -} - -[data-theme="dark"] .player-input input:hover, -[data-theme="dark"] .player-input input:focus { - background-color: #475569; - border-color: var(--button-color); -} - -[data-theme="dark"] .history-container { - background-color: #334155; - color: #F8FAFC; -} - -[data-theme="dark"] h1 { - background: linear-gradient(135deg, - var(--x-color) 0%, - #A3B8D9 50%, - var(--o-color) 100% - ); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; -} - -[data-theme="dark"] .game-info { - background-color: #1E293B; -} - -[data-theme="dark"] #current-player, -[data-theme="dark"] #timer { - background-color: #334155; - border-color: #475569; - color: #F8FAFC; -} - -[data-theme="dark"] .score { - background-color: #334155; -} - -[data-theme="dark"] .score span { - background: linear-gradient(135deg, #475569, #334155); - color: #F8FAFC; -} - -.game-status { - display: flex; - flex-direction: column; - gap: var(--spacing-md); - padding: var(--spacing-md); - background: var(--frame-color); - border-radius: var(--border-radius); - box-shadow: var(--shadow-main); -} - -.game-info { - display: flex; - flex-direction: column; - gap: var(--spacing-md); - padding: var(--spacing-md); - background: var(--bg-color); - border-radius: var(--border-radius-sm); - box-shadow: var(--shadow-inner); -} - -.game-mode { - background: var(--frame-color); - border-radius: var(--border-radius); - padding: var(--spacing-md); - box-shadow: var(--shadow-main); - display: flex; - flex-direction: column; - gap: var(--spacing-md); -} - -.mode-switch { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--spacing-md); - background: var(--bg-color); - border-radius: var(--border-radius-sm); - box-shadow: var(--shadow-inset); - color: var(--text-color); - font-weight: 600; - font-size: 0.95em; - position: relative; - overflow: hidden; - border: 2px solid var(--border-color); - height: 48px; -} - -[data-theme="dark"] .mode-switch { - background: var(--bg-color); - border-color: var(--button-color); - color: var(--text-color); -} - -#current-player, #timer { - padding: var(--spacing-md); - border-radius: var(--border-radius-sm); - background: var(--frame-color); - box-shadow: var(--shadow-main); - font-weight: 600; - color: var(--text-color); - transition: var(--transition-main); - font-size: 0.95em; - text-align: center; - height: 38px; - display: flex; - align-items: center; - justify-content: center; - border: 2px solid var(--border-color); -} - -#current-player.x-turn { - border-color: var(--x-color); - color: var(--x-color); -} - -#current-player.o-turn { - border-color: var(--o-color); - color: var(--o-color); -} - -#timer { - border-color: var(--accent-color); - color: var(--text-color); -} - -/* Dark mode adjustments */ -[data-theme="dark"] .mode-switch { - background: var(--bg-color); - border-color: var(--button-color); -} - -[data-theme="dark"] #current-player, -[data-theme="dark"] #timer { - background: var(--bg-color); - color: var(--text-color); -} - -[data-theme="dark"] #current-player.x-turn { - color: var(--x-color); -} - -[data-theme="dark"] #current-player.o-turn { - color: var(--o-color); -} - -/* Dark mode adjustments */ -[data-theme="dark"] .mode-switch, -[data-theme="dark"] #current-player, -[data-theme="dark"] #timer, -[data-theme="dark"] .history-container { - background: var(--bg-color); - border-color: var(--border-color); -} - -[data-theme="dark"] .slider { - background: var(--bg-color); - border-color: var(--border-color); -} - -#current-player::before, #timer::before, #moves::before { - background-color: var(--text-color); -} - -[data-theme="dark"] .subtitle { - background: linear-gradient(135deg, #6366F1, #475569); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; -} - -/* Dark mode title adjustments */ -[data-theme="dark"] h1 { - background: linear-gradient(135deg, - var(--x-color) 0%, - #A3B8D9 50%, - var(--o-color) 100% - ); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; -} - -[data-theme="dark"] .subtitle { - background: linear-gradient(135deg, #A3B8D9, var(--button-color)); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; -} - -/* Main containers - raised effect */ -.title-container, -.game-status, -.move-history, -.player-names, -.game-mode, -.score-board { - box-shadow: var(--shadow-raised); -} - -/* Inner elements - inset effect */ -.game-board, -.game-controls, -.game-info, -.history-container, -.score-container, -.player-input, -.mode-switch { - box-shadow: var(--shadow-inset); -} - -/* Interactive elements - raised with hover effect */ -.cell, -.control-btn, -.score { - box-shadow: var(--shadow-raised); - transition: var(--transition-main); -} - -.cell:hover, -.control-btn:hover:not(:disabled), -.score:hover { - box-shadow: var(--shadow-hover); -} - -/* Input fields - subtle raised effect */ -.player-input input { - box-shadow: var(--shadow-raised); -} - -/* Game info elements - raised effect */ -#current-player, -#timer, -#moves { - box-shadow: var(--shadow-raised); -} \ No newline at end of file