diff --git a/games/Tic-Tac-Toe/Tic-Tac-Toe.py b/games/Tic-Tac-Toe/Tic-Tac-Toe.py index 9834f58..4dd4603 100644 --- a/games/Tic-Tac-Toe/Tic-Tac-Toe.py +++ b/games/Tic-Tac-Toe/Tic-Tac-Toe.py @@ -1,187 +1,189 @@ import random import math import time +import os -# ───────────────────────────────────────── -# 🎮 Tic Tac Toe — Smart AI Edition -# ───────────────────────────────────────── +# ═══════════════════════════════════════════════ +# 🎮 TIC TAC TOE — Full Featured Edition +# ═══════════════════════════════════════════════ -EMPTY = "⬜" -X = "❌" -O = "⭕" +EMPTY = " " +X = "X" +O = "O" +WINS = [ + (0, 1, 2), (3, 4, 5), (6, 7, 8), # rows + (0, 3, 6), (1, 4, 7), (2, 5, 8), # cols + (0, 4, 8), (2, 4, 6) # diagonals +] -# ───────────────────────────────────────── -# BOARD FUNCTIONS -# ───────────────────────────────────────── -def create_board(): - return [EMPTY] * 9 +# ─────────────────────────────────────────────── +# DISPLAY +# ─────────────────────────────────────────────── + +def clear(): + os.system("cls" if os.name == "nt" else "clear") + + +def color(text, code): + """ANSI color wrapper.""" + return f"\033[{code}m{text}\033[0m" + + +def RED(t): return color(t, "91") +def BLUE(t): return color(t, "94") +def GREEN(t): return color(t, "92") +def YELLOW(t): return color(t, "93") +def BOLD(t): return color(t, "1") +def DIM(t): return color(t, "2") +def CYAN(t): return color(t, "96") + + +def banner(): + print(BOLD(CYAN(""" +╔══════════════════════════════════════╗ +║ 🎮 TIC TAC TOE 🎮 ║ +╚══════════════════════════════════════╝"""))) + + +def fmt_cell(val, pos): + """Color X red, O blue, empty show position number.""" + if val == X: + return BOLD(RED(f" {X} ")) + elif val == O: + return BOLD(BLUE(f" {O} ")) + else: + return DIM(f" {pos} ") def display_board(board): + b = board + sep = "───┼───┼───" print() - for i in range(0, 9, 3): - print(f" {board[i]} {board[i+1]} {board[i+2]}") + print(f" {fmt_cell(b[0],1)}│{fmt_cell(b[1],2)}│{fmt_cell(b[2],3)}") + print(f" {sep}") + print(f" {fmt_cell(b[3],4)}│{fmt_cell(b[4],5)}│{fmt_cell(b[5],6)}") + print(f" {sep}") + print(f" {fmt_cell(b[6],7)}│{fmt_cell(b[7],8)}│{fmt_cell(b[8],9)}") print() -def display_position_guide(): - print("\n 📌 Position guide:") - for i in range(1, 10, 3): - print(f" {i} {i+1} {i+2}") +def display_scoreboard(scores, names): + p1, p2 = names + s1, s2, draws = scores["p1"], scores["p2"], scores["draws"] + print(BOLD(" ┌─── SCOREBOARD ───────────────┐")) + print(f" │ {RED(p1):<20} {BOLD(str(s1)):>3} │") + print(f" │ {BLUE(p2):<20} {BOLD(str(s2)):>3} │") + print(f" │ {'Draws':<20} {BOLD(str(draws)):>3} │") + print(BOLD(" └───────────────────────────────┘")) print() -def available_moves(board): - return [i for i, cell in enumerate(board) if cell == EMPTY] - - -# ───────────────────────────────────────── +# ─────────────────────────────────────────────── # GAME LOGIC -# ───────────────────────────────────────── +# ─────────────────────────────────────────────── + +def create_board(): + return [EMPTY] * 9 -def check_winner(board, symbol): - wins = [ - [0, 1, 2], - [3, 4, 5], - [6, 7, 8], - [0, 3, 6], - [1, 4, 7], - [2, 5, 8], +def available_moves(board): + return [i for i, c in enumerate(board) if c == EMPTY] - [0, 4, 8], - [2, 4, 6] - ] +def check_winner(board, symbol): return any( board[a] == board[b] == board[c] == symbol - for a, b, c in wins + for a, b, c in WINS ) -def check_draw(board): - return all(cell != EMPTY for cell in board) - - -# ───────────────────────────────────────── -# PLAYER INPUT -# ───────────────────────────────────────── - -def get_player_move(board, symbol, name): - while True: - try: - pos = int(input( - f" {symbol} {name}'s turn!\n" - f" ➡️ Enter position (1-9): " - )) - - if pos < 1 or pos > 9: - print(" ⚠️ Enter a number between 1 and 9.\n") +def get_winning_cells(board, symbol): + for a, b, c in WINS: + if board[a] == board[b] == board[c] == symbol: + return (a, b, c) + return None - elif board[pos - 1] != EMPTY: - print(" ⚠️ Position already taken.\n") - - else: - return pos - 1 - except ValueError: - print(" ⚠️ Invalid input.\n") +def check_draw(board): + return EMPTY not in board -# ───────────────────────────────────────── -# EASY AI -# ───────────────────────────────────────── +# ─────────────────────────────────────────────── +# AI ENGINES +# ─────────────────────────────────────────────── -def easy_ai(board): +def easy_ai(board, _symbol): return random.choice(available_moves(board)) -# ───────────────────────────────────────── -# MEDIUM AI -# ───────────────────────────────────────── +def medium_ai(board, symbol): + opponent = X if symbol == O else O -def medium_ai(board): - # Try to win + # 1. Win if possible for move in available_moves(board): - board[move] = O - - if check_winner(board, O): + board[move] = symbol + if check_winner(board, symbol): board[move] = EMPTY return move - board[move] = EMPTY - # Block player win + # 2. Block opponent win for move in available_moves(board): - board[move] = X - - if check_winner(board, X): + board[move] = opponent + if check_winner(board, opponent): board[move] = EMPTY return move - board[move] = EMPTY - # Otherwise random - return random.choice(available_moves(board)) - - -# ───────────────────────────────────────── -# HARD AI (MINIMAX) -# ───────────────────────────────────────── + # 3. Prefer center, then corners, then sides + for preferred in [4, 0, 2, 6, 8, 1, 3, 5, 7]: + if board[preferred] == EMPTY: + return preferred -def minimax(board, depth, is_maximizing): - - if check_winner(board, O): - return 1 + return random.choice(available_moves(board)) - if check_winner(board, X): - return -1 +def minimax(board, depth, is_max, symbol, opponent, alpha, beta): + if check_winner(board, symbol): + return 10 - depth + if check_winner(board, opponent): + return depth - 10 if check_draw(board): return 0 - if is_maximizing: - best_score = -math.inf - + if is_max: + best = -math.inf for move in available_moves(board): - board[move] = O - - score = minimax(board, depth + 1, False) - + board[move] = symbol + best = max(best, minimax(board, depth+1, False, symbol, opponent, alpha, beta)) board[move] = EMPTY - - best_score = max(score, best_score) - - return best_score - + alpha = max(alpha, best) + if beta <= alpha: + break + return best else: - best_score = math.inf - + best = math.inf for move in available_moves(board): - board[move] = X - - score = minimax(board, depth + 1, True) - + board[move] = opponent + best = min(best, minimax(board, depth+1, True, symbol, opponent, alpha, beta)) board[move] = EMPTY + beta = min(beta, best) + if beta <= alpha: + break + return best - best_score = min(score, best_score) - - return best_score - -def hard_ai(board): +def hard_ai(board, symbol): + opponent = X if symbol == O else O best_score = -math.inf best_move = None for move in available_moves(board): - board[move] = O - - score = minimax(board, 0, False) - + board[move] = symbol + score = minimax(board, 0, False, symbol, opponent, -math.inf, math.inf) board[move] = EMPTY - if score > best_score: best_score = score best_move = move @@ -189,218 +191,237 @@ def hard_ai(board): return best_move -# ───────────────────────────────────────── -# AI CONTROLLER -# ───────────────────────────────────────── +AI_ENGINES = { + "easy": easy_ai, + "medium": medium_ai, + "hard": hard_ai, +} -def get_computer_move(board, difficulty): - if difficulty == "easy": - return easy_ai(board) +# ─────────────────────────────────────────────── +# INPUT HELPERS +# ─────────────────────────────────────────────── - elif difficulty == "medium": - return medium_ai(board) - - else: - return hard_ai(board) - - -# ───────────────────────────────────────── -# GAME MODE -# ───────────────────────────────────────── - -def choose_difficulty(): +def get_player_move(board, symbol, name): + sym_colored = RED(symbol) if symbol == X else BLUE(symbol) while True: + try: + raw = input(f" {sym_colored} {BOLD(name)} → enter position (1-9): ").strip() + pos = int(raw) + if not 1 <= pos <= 9: + print(YELLOW(" ⚠ Enter a number from 1 to 9.\n")) + elif board[pos - 1] != EMPTY: + print(YELLOW(" ⚠ That cell is already taken.\n")) + else: + return pos - 1 + except ValueError: + print(YELLOW(" ⚠ Please enter a valid number.\n")) - print("\n 🎯 Choose Difficulty:") - print(" 1️⃣ Easy") - print(" 2️⃣ Medium") - print(" 3️⃣ Hard") - - choice = input(" ➡️ Enter choice (1/2/3): ").strip() - - if choice == "1": - return "easy" - - elif choice == "2": - return "medium" - - elif choice == "3": - return "hard" - - else: - print(" ⚠️ Invalid choice.\n") - - -def play_vs_computer(): - - board = create_board() - - difficulty = choose_difficulty() - - print(f"\n 👤 You = {X} | 🤖 Computer = {O}") - print(f" 🎯 Difficulty: {difficulty.upper()}") - - display_position_guide() +def prompt(msg, valid): while True: - - # PLAYER TURN - display_board(board) - - move = get_player_move(board, X, "You") - - board[move] = X - - if check_winner(board, X): - display_board(board) - print(" 🎉 You win! 🏆\n") - return - - if check_draw(board): - display_board(board) - print(" 🤝 It's a draw!\n") - return - - # COMPUTER TURN - print("\n 🤖 Computer is thinking...") - time.sleep(1) - - comp_move = get_computer_move(board, difficulty) - - board[comp_move] = O - - print(f" 🤖 Computer chose position {comp_move + 1}") - - if check_winner(board, O): - display_board(board) - print(" 😔 Computer wins!\n") - return - - if check_draw(board): - display_board(board) - print(" 🤝 It's a draw!\n") - return - - -# ───────────────────────────────────────── -# TWO PLAYER MODE -# ───────────────────────────────────────── - -def play_two_players(): - + ans = input(msg).strip().lower() + if ans in valid: + return ans + print(YELLOW(f" ⚠ Enter one of: {', '.join(valid)}\n")) + + +# ─────────────────────────────────────────────── +# MENUS +# ─────────────────────────────────────────────── + +def menu_mode(): + print(BOLD("\n Choose Game Mode:")) + print(" 1 → Two Players (local)") + print(" 2 → vs Computer (AI)") + return prompt("\n ➜ Your choice (1/2): ", {"1", "2"}) + + +def menu_difficulty(): + print(BOLD("\n Choose Difficulty:")) + print(" 1 → Easy (random moves)") + print(" 2 → Medium (smart blocking)") + print(" 3 → Hard (unbeatable AI)") + choice = prompt("\n ➜ Your choice (1/2/3): ", {"1", "2", "3"}) + return {"1": "easy", "2": "medium", "3": "hard"}[choice] + + +def menu_first_turn(p1_name, p2_name): + print(BOLD("\n Who goes first?")) + print(f" 1 → {p1_name}") + print(f" 2 → {p2_name}") + print(f" 3 → Random") + choice = prompt("\n ➜ Your choice (1/2/3): ", {"1", "2", "3"}) + if choice == "3": + return random.choice(["1", "2"]) + return choice + + +# ─────────────────────────────────────────────── +# CORE GAME ROUND +# ─────────────────────────────────────────────── + +def play_round(players, scores, mode, difficulty=None): + """ + players: list of dicts with keys: name, symbol, is_human + Returns: "p1", "p2", or "draw" + """ board = create_board() - - players = [ - {"name": "Player 1", "symbol": X}, - {"name": "Player 2", "symbol": O} - ] - - print(f"\n 👤 Player 1 = {X} | Player 2 = {O}") - - display_position_guide() - turn = 0 while True: - + clear() + banner() + display_scoreboard(scores, [p["name"] for p in players]) display_board(board) - player = players[turn % 2] - - move = get_player_move( - board, - player["symbol"], - player["name"] - ) + current = players[turn % 2] + other = players[(turn + 1) % 2] - board[move] = player["symbol"] + sym_label = RED(current["symbol"]) if current["symbol"] == X else BLUE(current["symbol"]) - if check_winner(board, player["symbol"]): + # ── Get move ── + if current["is_human"]: + move = get_player_move(board, current["symbol"], current["name"]) + else: + print(f" 🤖 {BOLD(current['name'])} is thinking", end="", flush=True) + for _ in range(3): + time.sleep(0.35) + print(".", end="", flush=True) + print() + time.sleep(0.2) + move = AI_ENGINES[difficulty](board, current["symbol"]) + print(f" 🤖 {current['name']} chose position {BOLD(str(move + 1))}\n") + time.sleep(0.5) + + board[move] = current["symbol"] + + # ── Check result ── + if check_winner(board, current["symbol"]): + clear() + banner() + display_scoreboard(scores, [p["name"] for p in players]) display_board(board) - - print( - f" 🎉 {player['name']} wins! 🏆\n" - ) - - return + win_sym = "🏆" + print(GREEN(BOLD(f" {win_sym} {current['name']} wins this round! {win_sym}\n"))) + time.sleep(1.5) + return "p1" if turn % 2 == 0 else "p2" if check_draw(board): + clear() + banner() + display_scoreboard(scores, [p["name"] for p in players]) display_board(board) - print(" 🤝 It's a draw!\n") - return + print(YELLOW(BOLD(" 🤝 It's a draw!\n"))) + time.sleep(1.5) + return "draw" turn += 1 -# ───────────────────────────────────────── -# MENU -# ───────────────────────────────────────── - -def choose_mode(): - - while True: +# ─────────────────────────────────────────────── +# FULL MATCH (BEST OF N) +# ─────────────────────────────────────────────── - print("\n Choose mode:") - print(" 1️⃣ 2 Players") - print(" 2️⃣ vs Computer") - - choice = input( - " ➡️ Enter choice (1/2): " - ).strip() - - if choice == "1": - return "two" - - elif choice == "2": - return "computer" +def play_match(mode, difficulty=None): + clear() + banner() + # Names + if mode == "1": + p1_name = input(BOLD(" Player 1 name: ")).strip() or "Player 1" + p2_name = input(BOLD(" Player 2 name: ")).strip() or "Player 2" + players = [ + {"name": p1_name, "symbol": X, "is_human": True}, + {"name": p2_name, "symbol": O, "is_human": True}, + ] + else: + p1_name = input(BOLD(" Your name: ")).strip() or "You" + p2_name = "Computer" + players = [ + {"name": p1_name, "symbol": X, "is_human": True}, + {"name": p2_name, "symbol": O, "is_human": False}, + ] + + # First turn + first = menu_first_turn(players[0]["name"], players[1]["name"]) + if first == "2": + players.reverse() + # Keep X always first to move; reassign symbols + players[0]["symbol"] = X + players[1]["symbol"] = O + + scores = {"p1": 0, "p2": 0, "draws": 0} + + # Rounds + print(BOLD("\n Best of how many rounds?")) + print(" 1 → 1 round") + print(" 2 → 3 rounds (best of 3)") + print(" 3 → 5 rounds (best of 5)") + rc = prompt("\n ➜ Your choice (1/2/3): ", {"1", "2", "3"}) + max_rounds = {"1": 1, "2": 3, "3": 5}[rc] + rounds_played = 0 + + while rounds_played < max_rounds: + remaining = max_rounds - rounds_played + winner = play_round(players, scores, mode, difficulty) + + if winner == "p1": + scores["p1"] += 1 + elif winner == "p2": + scores["p2"] += 1 else: - print(" ⚠️ Invalid choice.\n") - - -def play_again(): - - while True: + scores["draws"] += 1 - answer = input( - " 🔄 Play again? (y/n): " - ).strip().lower() + rounds_played += 1 - if answer in ("y", "yes"): - return True - - elif answer in ("n", "no"): - return False + # Early exit if someone has won majority + majority = (max_rounds // 2) + 1 + if scores["p1"] >= majority or scores["p2"] >= majority: + break - else: - print(" ⚠️ Enter y or n.\n") + if rounds_played < max_rounds: + cont = prompt(f" ➜ Next round? (y/n): ", {"y", "n"}) + if cont == "n": + break + + # Final result + clear() + banner() + p1_name_display = players[0]["name"] if first == "1" else players[1]["name"] + p2_name_display = players[1]["name"] if first == "1" else players[0]["name"] + display_scoreboard(scores, [players[0]["name"], players[1]["name"]]) + + if scores["p1"] > scores["p2"]: + print(GREEN(BOLD(f" 🏆 {players[0]['name']} wins the match!\n"))) + elif scores["p2"] > scores["p1"]: + print(GREEN(BOLD(f" 🏆 {players[1]['name']} wins the match!\n"))) + else: + print(YELLOW(BOLD(" 🤝 The match is a tie!\n"))) -# ───────────────────────────────────────── +# ─────────────────────────────────────────────── # MAIN -# ───────────────────────────────────────── +# ─────────────────────────────────────────────── def main(): - - print("\n" + "=" * 42) - print(" 🎮 TIC TAC TOE — SMART AI") - print("=" * 42) - while True: + clear() + banner() - mode = choose_mode() + mode = menu_mode() + difficulty = None + if mode == "2": + difficulty = menu_difficulty() - if mode == "two": - play_two_players() - - else: - play_vs_computer() + play_match(mode, difficulty) - if not play_again(): - print("\n 👋 Thanks for playing!\n") + again = prompt(" ➜ Play again? (y/n): ", {"y", "n"}) + if again == "n": + clear() + print(CYAN(BOLD("\n 👋 Thanks for playing! See you next time.\n"))) break if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/web-app/assets/banners/tic-tac-toe.jpg b/web-app/assets/banners/tic-tac-toe.jpg new file mode 100644 index 0000000..781d9af Binary files /dev/null and b/web-app/assets/banners/tic-tac-toe.jpg differ diff --git a/web-app/css/styles.css b/web-app/css/styles.css index bb8aedf..0d4bdac 100644 --- a/web-app/css/styles.css +++ b/web-app/css/styles.css @@ -4619,3 +4619,488 @@ html[data-theme="dark"] body { min-height: auto !important; } } +/* ============================== + Tic-Tac-Toe + =============================== */ +/* ═══════════════════════════════════════════════════════════ + TTT — Tic Tac Toe (scoped under .ttt-wrap) + Safe to append to the global styles.css. + Every selector starts with .ttt- so nothing leaks out. + Uses repo CSS vars: --accent-color, --surface-color, + --panel-color, --text-color, --text-secondary, --border-color +═══════════════════════════════════════════════════════════ */ + +/* ── Root tokens (scoped inside .ttt-wrap) ── */ +.ttt-wrap { + --ttt-x: #e05c6f; /* red — player X */ + --ttt-o: #3bb8d6; /* cyan — player O */ + --ttt-accent: var(--accent-color, #6abf8d); + --ttt-surface: var(--surface-color, #fff); + --ttt-panel: var(--panel-color, #f5f5f5); + --ttt-border: var(--border-color, rgba(0,0,0,.1)); + --ttt-text: var(--text-color, #1f1f1f); + --ttt-muted: var(--text-secondary, #666); + --ttt-radius: 14px; + --ttt-cell-bg: var(--panel-color, #f5f5f5); + + display: block; + max-width: 400px; + margin: 0 auto; + padding: 4px 0 24px; + font-family: inherit; + color: var(--ttt-text); +} + +/* ── Screens ── */ +.ttt-screen { display: none; } +.ttt-screen--active { display: block; } + +/* ── Logo ── */ +.ttt-logo { + text-align: center; + font-size: 2.2rem; + font-weight: 800; + letter-spacing: .06em; + line-height: 1; + margin-bottom: 2px; +} +.ttt-logo-x { color: var(--ttt-x); text-shadow: 0 0 16px rgba(224,92,111,.35); } +.ttt-logo-o { color: var(--ttt-o); text-shadow: 0 0 16px rgba(59,184,214,.35); } +.ttt-logo-dot { color: var(--ttt-muted); margin: 0 4px; } + +/* ── Titles ── */ +.ttt-title { + text-align: center; + font-size: 1rem; + font-weight: 700; + letter-spacing: .18em; + text-transform: uppercase; + color: var(--ttt-muted); + margin: 0 0 2px; +} +.ttt-sub { + text-align: center; + font-size: .84rem; + color: var(--ttt-muted); + margin: 0 0 20px; +} + +/* ── Field groups ── */ +.ttt-field-group { + margin-bottom: 16px; +} +.ttt-label { + display: block; + font-size: .7rem; + font-weight: 700; + letter-spacing: .15em; + text-transform: uppercase; + color: var(--ttt-muted); + margin-bottom: 7px; + font-style: normal; +} +.ttt-x-tag { color: var(--ttt-x); font-style: normal; } +.ttt-o-tag { color: var(--ttt-o); font-style: normal; } + +/* ── Pill toggle buttons ── + Fully isolated: overrides .game-btn, .mode-btn, .btn from global CSS */ +.ttt-pill-group { + display: flex !important; + gap: 6px !important; + background: var(--ttt-panel) !important; + border: 1px solid var(--ttt-border) !important; + border-radius: 10px !important; + padding: 4px !important; + box-shadow: none !important; +} + +/* Reset every possible global override then re-style */ +.ttt-wrap .ttt-pill, +.ttt-wrap .ttt-pill:hover, +.ttt-wrap .ttt-pill:focus, +.ttt-wrap .ttt-pill:active { + /* layout */ + flex: 1 1 0 !important; + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + padding: 9px 6px !important; + min-width: 0 !important; + width: auto !important; + + /* look */ + background: transparent !important; + border: none !important; + border-radius: 7px !important; + box-shadow: none !important; + outline: none !important; + + /* type */ + color: var(--ttt-muted) !important; + font-family: inherit !important; + font-size: .82rem !important; + font-weight: 700 !important; + text-transform: none !important; + letter-spacing: normal !important; + white-space: nowrap !important; + + cursor: pointer !important; + transition: background .18s, color .18s, box-shadow .18s !important; + + /* undo global transform/translate tricks */ + transform: none !important; + top: 0 !important; +} + +.ttt-wrap .ttt-pill:hover:not(.ttt-pill--on) { + background: rgba(0,0,0,.06) !important; + color: var(--ttt-text) !important; +} + +[data-theme="dark"] .ttt-wrap .ttt-pill:hover:not(.ttt-pill--on) { + background: rgba(255,255,255,.08) !important; +} + +.ttt-wrap .ttt-pill.ttt-pill--on { + background: var(--ttt-accent) !important; + color: #fff !important; + box-shadow: 0 2px 10px rgba(106,191,141,.4) !important; + transform: none !important; +} + +/* ── Name inputs ── */ +.ttt-names-row { display: flex; gap: 12px; margin-bottom: 18px; } +.ttt-name-box { flex: 1; display: flex; flex-direction: column; gap: 6px; } +.ttt-name-box.ttt-dimmed { opacity: .45; pointer-events: none; } + +.ttt-wrap .ttt-input { + display: block !important; + width: 100% !important; + padding: 9px 11px !important; + background: var(--ttt-panel) !important; + border: 1.5px solid var(--ttt-border) !important; + border-radius: 9px !important; + color: var(--ttt-text) !important; + font-family: inherit !important; + font-size: .9rem !important; + outline: none !important; + box-shadow: none !important; + transition: border-color .2s !important; +} +.ttt-wrap .ttt-input:focus { + border-color: var(--ttt-accent) !important; + box-shadow: 0 0 0 3px rgba(106,191,141,.15) !important; +} + +/* ── CTA / primary button ── */ +.ttt-wrap .ttt-cta { + display: block !important; + width: 100% !important; + padding: 13px 16px !important; + background: var(--ttt-accent) !important; + color: #fff !important; + border: none !important; + border-radius: var(--ttt-radius) !important; + font-family: inherit !important; + font-size: 1rem !important; + font-weight: 800 !important; + letter-spacing: .04em !important; + cursor: pointer !important; + text-align: center !important; + box-shadow: 0 4px 0 rgba(0,0,0,.1), 0 8px 20px rgba(106,191,141,.3) !important; + transition: transform .18s cubic-bezier(.34,1.56,.64,1), box-shadow .18s !important; + transform: none !important; + top: 0 !important; + position: relative !important; +} +.ttt-wrap .ttt-cta:hover { + transform: translateY(-3px) !important; + box-shadow: 0 7px 0 rgba(0,0,0,.08), 0 14px 28px rgba(106,191,141,.4) !important; +} +.ttt-wrap .ttt-cta:active { + transform: translateY(2px) !important; + box-shadow: 0 2px 0 rgba(0,0,0,.1), 0 4px 10px rgba(106,191,141,.2) !important; +} +.ttt-wrap .ttt-cta.ttt-cta--sm { + width: auto !important; + display: inline-block !important; + padding: 10px 28px !important; + font-size: .9rem !important; +} + +/* ── Ghost / back button ── */ +.ttt-wrap .ttt-ghost-btn { + display: block !important; + width: 100% !important; + margin-top: 10px !important; + padding: 10px !important; + background: transparent !important; + border: 1.5px solid var(--ttt-border) !important; + border-radius: var(--ttt-radius) !important; + color: var(--ttt-muted) !important; + font-family: inherit !important; + font-size: .85rem !important; + font-weight: 700 !important; + cursor: pointer !important; + transition: color .2s, border-color .2s !important; + text-align: center !important; + transform: none !important; + box-shadow: none !important; +} +.ttt-wrap .ttt-ghost-btn:hover { + color: var(--ttt-text) !important; + border-color: var(--ttt-text) !important; + box-shadow: none !important; + transform: none !important; +} + +/* ══════════════════════════════════════════ + GAME SCREEN +══════════════════════════════════════════ */ + +/* ── Scoreboard ── */ +.ttt-scores { + display: flex; + gap: 4px; + background: var(--ttt-panel); + border: 1px solid var(--ttt-border); + border-radius: 12px; + padding: 10px 8px; + margin-bottom: 8px; +} +.ttt-score { flex: 1; text-align: center; } +.ttt-score-name { + font-size: .64rem; + font-weight: 700; + letter-spacing: .1em; + text-transform: uppercase; + color: var(--ttt-muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-bottom: 2px; +} +.ttt-score-val { font-size: 1.55rem; font-weight: 800; } +.ttt-score--x .ttt-score-val { color: var(--ttt-x); } +.ttt-score--o .ttt-score-val { color: var(--ttt-o); } +.ttt-score--d .ttt-score-val { color: var(--ttt-muted); font-size: 1.1rem; } + +/* ── Round tag ── */ +.ttt-round-tag { + text-align: center; + font-size: .7rem; + font-weight: 700; + letter-spacing: .14em; + text-transform: uppercase; + color: var(--ttt-muted); + margin-bottom: 10px; +} + +/* ── Turn banner ── */ +.ttt-turn-banner { + text-align: center; + font-size: .95rem; + font-weight: 700; + color: var(--ttt-muted); + margin-bottom: 14px; + min-height: 22px; + transition: color .2s; +} +.ttt-turn-sym { + font-size: 1.05rem; + margin-right: 4px; + display: inline-block; +} +.ttt-turn-banner--x .ttt-turn-sym { text-shadow: 0 0 10px rgba(224,92,111,.5); } +.ttt-turn-banner--o .ttt-turn-sym { text-shadow: 0 0 10px rgba(59,184,214,.5); } + +/* ── Board wrapper (position context for SVG overlay) ── */ +#ttt-board { + position: relative; + display: grid !important; + grid-template-columns: repeat(3, 1fr) !important; + gap: 8px !important; + margin-bottom: 0 !important; +} + +/* ── Cells ── */ +.ttt-wrap .ttt-cell { + /* layout */ + aspect-ratio: 1 !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + + /* look */ + background: var(--ttt-cell-bg) !important; + border: 2px solid var(--ttt-border) !important; + border-radius: var(--ttt-radius) !important; + box-shadow: none !important; + + /* type */ + font-size: 2rem !important; + font-family: inherit !important; + color: var(--ttt-text) !important; + text-align: center !important; + line-height: 1 !important; + + cursor: pointer !important; + user-select: none !important; + transition: background .15s, border-color .15s, transform .12s !important; + transform: none !important; + top: 0 !important; + position: relative !important; + padding: 0 !important; + margin: 0 !important; + width: 100% !important; +} + +.ttt-wrap .ttt-cell:hover:not(:disabled):not(.ttt-cell--x):not(.ttt-cell--o) { + background: var(--ttt-accent) !important; + opacity: .15 !important; + border-color: var(--ttt-accent) !important; + transform: scale(1.04) !important; +} +/* Reset opacity on non-hover so the above doesn't tint filled cells */ +.ttt-wrap .ttt-cell { opacity: 1 !important; } +.ttt-wrap .ttt-cell:hover:not(:disabled):not(.ttt-cell--x):not(.ttt-cell--o) { + opacity: 1 !important; + background: rgba(106,191,141,.12) !important; +} + +.ttt-wrap .ttt-cell:disabled:not(.ttt-cell--x):not(.ttt-cell--o) { + cursor: not-allowed !important; +} + +.ttt-wrap .ttt-cell--x { + color: var(--ttt-x) !important; + border-color: rgba(224,92,111,.3) !important; + background: rgba(224,92,111,.07) !important; + animation: tttPop .22s cubic-bezier(.34,1.56,.64,1) !important; + cursor: default !important; + font-weight: 800 !important; + +} +.ttt-wrap .ttt-cell--o { + color: var(--ttt-o) !important; + border-color: rgba(59,184,214,.3) !important; + background: rgba(59,184,214,.07) !important; + animation: tttPop .22s cubic-bezier(.34,1.56,.64,1) !important; + cursor: default !important; + font-weight: 800 !important; +} +.ttt-wrap .ttt-cell--win { + animation: tttWin .45s ease forwards !important; + border-color: var(--ttt-accent) !important; + background: rgba(106,191,141,.15) !important; +} + +@keyframes tttPop { + from { transform: scale(.35); opacity: 0; } + to { transform: scale(1); opacity: 1; } +} +@keyframes tttWin { + 0% { transform: scale(1); } + 40% { transform: scale(1.14); } + 70% { transform: scale(.96); } + 100% { transform: scale(1.07); } +} + +/* ── Win-line SVG (sits over the board) ── */ +.ttt-win-svg { + position: absolute; + inset: 0; + width: calc(100% + 8px); + height: calc(100% + 8px); + pointer-events: none; + overflow: visible; + opacity: 0; + transition: opacity .25s; + z-index: 2; + display: block; +} +.ttt-win-svg--visible { opacity: 1; } +#ttt-win-line { + stroke: var(--ttt-accent); + stroke-width: .18; + stroke-linecap: round; + filter: drop-shadow(0 0 4px rgba(106,191,141,.6)); + transition: opacity .3s; +} + +/* ── Result overlay (sits on top of board area) ── */ +.ttt-result-overlay { + display: none; + position: absolute; + inset: 0; + background: rgba(0,0,0,.45); + border-radius: var(--ttt-radius); + align-items: center; + justify-content: center; + z-index: 10; + animation: tttFadeIn .3s ease; +} +#ttt-game { position: relative; } + +@keyframes tttFadeIn { + from { opacity:0; } + to { opacity:1; } +} + +.ttt-result-card { + background: var(--ttt-surface); + border: 1px solid var(--ttt-border); + border-radius: var(--ttt-radius); + padding: 24px 28px; + text-align: center; + animation: tttSlideUp .32s cubic-bezier(.34,1.56,.64,1); +} +@keyframes tttSlideUp { + from { transform: translateY(24px) scale(.92); opacity:0; } + to { transform: translateY(0) scale(1); opacity:1; } +} + +.ttt-result-emoji { font-size: 2.4rem; margin-bottom: 6px; } +.ttt-result-text { font-size: 1.15rem; font-weight: 800; margin-bottom: 16px; color: var(--ttt-text); } + +/* ══════════════════════════════════════════ + FINAL SCREEN +══════════════════════════════════════════ */ +#ttt-final { text-align: center; padding: 8px 0; } + +.ttt-final-trophy { font-size: 3rem; margin-bottom: 8px; } +.ttt-final-title { + font-size: 1.3rem; + font-weight: 800; + margin-bottom: 18px; + color: var(--ttt-text); +} +.ttt-final-scoreline { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + margin-bottom: 6px; +} +.ttt-fs-name { font-size: .82rem; font-weight: 700; } +.ttt-fs-x { color: var(--ttt-x); } +.ttt-fs-o { color: var(--ttt-o); } +.ttt-fs-score { font-size: 2.2rem; font-weight: 800; color: var(--ttt-text); } +.ttt-fs-sep { font-size: 1.3rem; color: var(--ttt-muted); } +.ttt-final-draws { + font-size: .8rem; + color: var(--ttt-muted); + margin-bottom: 20px; +} +.ttt-final-actions { + display: flex; + flex-direction: column; + gap: 8px; +} + +/* ── Mobile ── */ +@media (max-width: 420px) { + .ttt-wrap { padding: 4px 0 16px; } + .ttt-wrap .ttt-cell { font-size: 1.6rem !important; } + .ttt-result-card { padding: 18px 16px; } +} diff --git a/web-app/index.html b/web-app/index.html index 5c12c92..c6f0183 100644 --- a/web-app/index.html +++ b/web-app/index.html @@ -1341,6 +1341,12 @@

Simon Says

Snake Game

Classic snake game!

+
+ Tic Tac Toe +
+

Tic Tac Toe

+

Classic X and O strategy game!

+
Tower of Hanoi
diff --git a/web-app/js/projects.js b/web-app/js/projects.js index 04ba8fd..05bdd56 100644 --- a/web-app/js/projects.js +++ b/web-app/js/projects.js @@ -1,4 +1,4 @@ -// Project Registry +// Project Registry // Each project's HTML and logic lives in its own file under js/projects/ @@ -3022,187 +3022,541 @@ function initTowerOfHanoi() { initializeGame(); } -function getTicTacToeHTML() { - return ` - - -
-

Tic Tac Toe

- -
- - - - - - - - - - - -
- -

Player X's Turn

+// ============================================ +// Tic-Tac-Toe +// ============================================ - -
- `; +function getTicTacToeHTML() { + return ` +
+ + +
+ +

Tic Tac Toe

+

Two players or vs AI — classic strategy game!

+ +
+ Game Mode +
+ + +
+
+ + + +
+ Rounds +
+ + + +
+
+ +
+
+ Player 1 + +
+
+ Player 2 + +
+
+ + +
+ + +
+ + +
+
+
P1
+
0
+
+
+
Draws
+
0
+
+
+
P2
+
0
+
+
+ +
Round 1 of 3
+ + +
+ + Player 1's turn +
+ + +
+
+ + ${[0,1,2,3,4,5,6,7,8].map(i => + `` + ).join('')} +
+ + + + + +
+ + + + +
+ + +
+
🏆
+
Player 1 Wins!
+
+ P1 + 0 + + 0 + P2 +
+
0 draws
+
+ + +
+
+ +
+`; } - -let currentPlayer = 'X'; - -let board = [ - '', '', '', - '', '', '', - '', '', '' -]; - -let gameActive = true; - -const winningPatterns = [ - [0,1,2], - [3,4,5], - [6,7,8], - - [0,3,6], - [1,4,7], - [2,5,8], - - [0,4,8], - [2,4,6] -]; - -function makeMove(index) { - - if (!gameActive || board[index] !== '') { - return; +function initTicTacToe() { + + // Win combos + const WINS = [ + [0,1,2],[3,4,5],[6,7,8], // rows + [0,3,6],[1,4,7],[2,5,8], // cols + [0,4,8],[2,4,6] // diagonals + ]; + + // Win-line centre coordinates (column, row) in 0-2 grid space + const WIN_COORDS = [ + [[0,0],[2,0]], [[0,1],[2,1]], [[0,2],[2,2]], // rows + [[0,0],[0,2]], [[1,0],[1,2]], [[2,0],[2,2]], // cols + [[0,0],[2,2]], [[2,0],[0,2]] // diagonals + ]; + + // ── State ── + let mode = "2p"; + let difficulty = "easy"; + let maxRounds = 3; + let p1 = "Player 1"; + let p2 = "Player 2"; + let scores = { p1:0, p2:0, draws:0 }; + let board = []; + let current = "X"; // "X" | "O" + let round = 1; + let gameOver = false; + + // ── Helpers ── + function qs(sel, ctx) { return (ctx||document).querySelector(sel); } + + // Show one of the three screens + function showScreen(id) { + ["ttt-setup","ttt-game","ttt-final"].forEach(s => { + const el = document.getElementById(s); + if (el) { + el.classList.toggle("ttt-screen--active", s === id); + } + }); + } + + // Pill-toggle group helper + function initPillGroup(groupId, onChange) { + const grp = document.getElementById(groupId); + if (!grp) return; + grp.querySelectorAll(".ttt-pill").forEach(btn => { + btn.addEventListener("click", () => { + grp.querySelectorAll(".ttt-pill").forEach(b => b.classList.remove("ttt-pill--on")); + btn.classList.add("ttt-pill--on"); + onChange(btn.dataset.val); + }); + }); + } + + // ── Wire up Setup ── + initPillGroup("ttt-mode-group", val => { + mode = val; + const diffGroup = document.getElementById("ttt-diff-group"); + const p2box = document.getElementById("ttt-p2-box"); + const p2inp = document.getElementById("ttt-p2"); + if (val === "ai") { + diffGroup.style.display = "block"; + p2box.classList.add("ttt-dimmed"); + p2inp.disabled = true; + p2inp.placeholder = "Computer 🤖"; + } else { + diffGroup.style.display = "none"; + p2box.classList.remove("ttt-dimmed"); + p2inp.disabled = false; + p2inp.placeholder = "Player 2"; } + }); + + initPillGroup("ttt-diff-pills", val => { difficulty = val; }); + initPillGroup("ttt-rounds-group", val => { maxRounds = parseInt(val); }); + + // Start button + const startBtn = document.getElementById("ttt-start"); + if (startBtn) { + startBtn.addEventListener("click", () => { + p1 = (document.getElementById("ttt-p1").value.trim()) || "Player 1"; + p2 = mode === "ai" + ? "Computer 🤖" + : ((document.getElementById("ttt-p2").value.trim()) || "Player 2"); + scores = { p1:0, p2:0, draws:0 }; + round = 1; + newRound(); + showScreen("ttt-game"); + }); + } + + // Back / Menu buttons + const backBtn = document.getElementById("ttt-back"); + if (backBtn) backBtn.addEventListener("click", () => showScreen("ttt-setup")); + + const menuBtn = document.getElementById("ttt-menu"); + if (menuBtn) menuBtn.addEventListener("click", () => showScreen("ttt-setup")); + + // Rematch button + const rematchBtn = document.getElementById("ttt-rematch"); + if (rematchBtn) { + rematchBtn.addEventListener("click", () => { + scores = { p1:0, p2:0, draws:0 }; + round = 1; + newRound(); + showScreen("ttt-game"); + }); + } + + // Next-round button + const nextBtn = document.getElementById("ttt-next"); + if (nextBtn) { + nextBtn.addEventListener("click", () => { + const majority = Math.ceil(maxRounds / 2); + const matchDone = round >= maxRounds + || scores.p1 >= majority + || scores.p2 >= majority; + if (matchDone) { + renderFinal(); + showScreen("ttt-final"); + } else { + round++; + newRound(); + } + }); + } + + // ── Round management ── + function newRound() { + board = Array(9).fill(null); + current = "X"; + gameOver = false; + + // Reset cells + document.querySelectorAll(".ttt-cell").forEach(c => { + c.textContent = ""; + c.className = "ttt-cell"; + c.disabled = false; + }); - const cells = document.querySelectorAll('.cell'); + // Hide result overlay + const overlay = document.getElementById("ttt-result-overlay"); + if (overlay) overlay.style.display = "none"; - board[index] = currentPlayer; + // Clear win line + clearWinLine(); - cells[index].innerText = currentPlayer; + // Update scoreboard + syncScoreboard(); - if (currentPlayer === 'X') { - cells[index].style.color = '#ff4d4d'; - } else { - cells[index].style.color = '#4d79ff'; + // Round label + const tag = document.getElementById("ttt-round-tag"); + if (tag) { + tag.textContent = maxRounds === 1 + ? "Single Round" + : `Round ${round} of ${maxRounds}`; } - // CHECK WINNER FIRST - if (checkWinner()) { - - document.getElementById('status').innerText = - `Player ${currentPlayer} Wins!`; + refreshTurnBanner(); - gameActive = false; - return; + // If AI goes first (not default, but safe to handle) + if (mode === "ai" && current === "O") { + lockBoard(true); + setTimeout(aiTurn, 600); } - - // DRAW CONDITION - const isDraw = board.every(cell => cell !== ''); - - if (isDraw) { - - document.getElementById('status').innerText = - "It's a Draw!"; - - gameActive = false; - return; + } + + function syncScoreboard() { + const set = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; + set("ttt-sn1", p1); + set("ttt-sn2", p2); + set("ttt-sv1", scores.p1); + set("ttt-sv2", scores.p2); + set("ttt-svd", scores.draws); + } + + function refreshTurnBanner() { + const name = current === "X" ? p1 : p2; + const sym = current === "X" ? "X" : "O"; + const symEl = document.getElementById("ttt-turn-sym"); + const nameEl = document.getElementById("ttt-turn-name"); + const banner = document.getElementById("ttt-turn-banner"); + if (symEl) symEl.textContent = sym; + if (nameEl) nameEl.textContent = name; + if (banner) { + banner.classList.toggle("ttt-turn-banner--x", current === "X"); + banner.classList.toggle("ttt-turn-banner--o", current === "O"); } + } - // SWITCH PLAYER - currentPlayer = currentPlayer === 'X' ? 'O' : 'X'; - - document.getElementById('status').innerText = - `Player ${currentPlayer}'s Turn`; -} - - -function checkWinner() { - - return winningPatterns.some(pattern => { - - return pattern.every(index => { - return board[index] === currentPlayer; - }); + // ── Cell clicks ── + document.querySelectorAll(".ttt-cell").forEach(cell => { + cell.addEventListener("click", () => { + const i = parseInt(cell.dataset.i); + if (gameOver || board[i]) return; + if (mode === "ai" && current === "O") return; // AI's turn + placeMove(i, current); + afterMove(); }); -} + }); + + function placeMove(i, sym) { + board[i] = sym; + const cell = document.querySelector(`.ttt-cell[data-i="${i}"]`); + if (!cell) return; + cell.textContent = sym; + cell.classList.add(sym === "X" ? "ttt-cell--x" : "ttt-cell--o"); + cell.disabled = true; + } + + function afterMove() { + const win = getWinner(board); + if (win) { endRound(win); return; } + if (board.every(c=>c)){ endRound(null); return; } + + current = current === "X" ? "O" : "X"; + refreshTurnBanner(); + + if (mode === "ai" && current === "O") { + lockBoard(true); + setTimeout(aiTurn, 480 + Math.random()*200); + } + } -function resetGame() { + function lockBoard(on) { + document.querySelectorAll(".ttt-cell").forEach(c => { + if (!board[parseInt(c.dataset.i)]) c.disabled = on; + }); + } + + // ── Win detection ── + function getWinner(b) { + for (let idx=0; idx { + const cell = document.querySelector(`.ttt-cell[data-i="${i}"]`); + if (cell) cell.classList.add("ttt-cell--win"); + }); + // Draw SVG win line + drawWinLine(win.coordIdx); + + const winnerName = win.sym === "X" ? p1 : p2; + if (win.sym === "X") scores.p1++; else scores.p2++; + syncScoreboard(); + + setResult("🏆", `${winnerName} wins this round!`); + } else { + scores.draws++; + syncScoreboard(); + setResult("🤝", "It's a draw!"); + } - board = [ - '', '', '', - '', '', '', - '', '', '' - ]; + // Update Next button label + setTimeout(() => { + const majority = Math.ceil(maxRounds / 2); + const matchDone = round >= maxRounds + || scores.p1 >= majority + || scores.p2 >= majority; + const nextBtn = document.getElementById("ttt-next"); + if (nextBtn) nextBtn.textContent = matchDone ? "See Results →" : "Next Round →"; + + const overlay = document.getElementById("ttt-result-overlay"); + if (overlay) overlay.style.display = "flex"; + }, 600); + } + + function setResult(emoji, text) { + const e = document.getElementById("ttt-res-emoji"); + const t = document.getElementById("ttt-res-text"); + if (e) e.textContent = emoji; + if (t) t.textContent = text; + } + + // ── Win-line SVG ── + // Grid cells are (col, row) 0-indexed; centre of cell = col+0.5, row+0.5 + function drawWinLine(comboIdx) { + const line = document.getElementById("ttt-win-line"); + const svg = document.getElementById("ttt-win-svg"); + if (!line || !svg) return; + + const [[c1,r1],[c2,r2]] = WIN_COORDS[comboIdx]; + line.setAttribute("x1", c1 + 0.5); + line.setAttribute("y1", r1 + 0.5); + line.setAttribute("x2", c2 + 0.5); + line.setAttribute("y2", r2 + 0.5); + line.setAttribute("opacity", "1"); + svg.classList.add("ttt-win-svg--visible"); +} - currentPlayer = 'X'; + function clearWinLine() { + const line = document.getElementById("ttt-win-line"); + const svg = document.getElementById("ttt-win-svg"); + if (line) line.setAttribute("opacity","0"); + if (svg) svg.classList.remove("ttt-win-svg--visible"); + } + + // ── Final screen ── + function renderFinal() { + const set = (id,v) => { const el=document.getElementById(id); if(el) el.textContent=v; }; + set("ttt-fp1", p1); + set("ttt-fp2", p2); + set("ttt-fp1s", scores.p1); + set("ttt-fp2s", scores.p2); + set("ttt-final-draws", `${scores.draws} draw${scores.draws!==1?"s":""}`); + + let title; + if (scores.p1 > scores.p2) title = `🏆 ${p1} wins the match!`; + else if (scores.p2 > scores.p1) title = `🏆 ${p2} wins the match!`; + else title = "🤝 The match is tied!"; + set("ttt-final-title", title); + } + + // ── AI engines ── + function freeCells(b) { + return b.reduce((acc,v,i) => { if(!v) acc.push(i); return acc; }, []); + } + + function checkWinFor(b, sym) { + return WINS.some(([a,x,c]) => b[a]===sym && b[x]===sym && b[c]===sym); + } + + function minimax(b, isMax, alpha, beta, depth) { + if (checkWinFor(b,"O")) return 10 - depth; + if (checkWinFor(b,"X")) return depth - 10; + if (b.every(c=>c)) return 0; + + const moves = freeCells(b); + if (isMax) { + let best = -Infinity; + for (const m of moves) { + b[m] = "O"; + best = Math.max(best, minimax(b, false, alpha, beta, depth+1)); + b[m] = null; + alpha = Math.max(alpha, best); + if (beta <= alpha) break; + } + return best; + } else { + let best = Infinity; + for (const m of moves) { + b[m] = "X"; + best = Math.min(best, minimax(b, true, alpha, beta, depth+1)); + b[m] = null; + beta = Math.min(beta, best); + if (beta <= alpha) break; + } + return best; + } + } - gameActive = true; + function chooseMove(b, diff) { + const moves = freeCells(b); + if (!moves.length) return null; - const cells = document.querySelectorAll('.cell'); + // Easy — random + if (diff === "easy") return moves[Math.floor(Math.random()*moves.length)]; - cells.forEach(cell => { - cell.textContent = ''; - }); + // Medium — win → block → center/corners + if (diff === "medium") { + for (const m of moves) { b[m]="O"; if(checkWinFor(b,"O")){b[m]=null;return m;} b[m]=null; } + for (const m of moves) { b[m]="X"; if(checkWinFor(b,"X")){b[m]=null;return m;} b[m]=null; } + for (const p of [4,0,2,6,8,1,3,5,7]) { if(!b[p]) return p; } + return moves[0]; + } - document.getElementById('status').textContent = - "Player X's Turn"; -} + // Hard — minimax + let bestScore=-Infinity, bestMove=moves[0]; + for (const m of moves) { + b[m]="O"; + const s = minimax(b, false, -Infinity, Infinity, 0); + b[m]=null; + if (s > bestScore) { bestScore=s; bestMove=m; } + } + return bestMove; + } + + function aiTurn() { + if (gameOver) return; + const move = chooseMove([...board], difficulty); // pass copy so minimax doesn't corrupt state + lockBoard(false); + if (move !== null) placeMove(move, "O"); + afterMove(); + } + +} // end initTicTacToe +// ================================ function getProductivePetHTML() { return `
diff --git a/web-app/js/projects/tic-tac-toe.js b/web-app/js/projects/tic-tac-toe.js index c86f237..1f26994 100644 --- a/web-app/js/projects/tic-tac-toe.js +++ b/web-app/js/projects/tic-tac-toe.js @@ -1,173 +1,538 @@ +// ═══════════════════════════════════════════════════════════ +// 🎮 Tic Tac Toe — web-app/js/projects/tic-tac-toe.js +// Category : games +// Features : 2-Player | vs AI (Easy/Medium/Hard) | +// Score tracking | Best-of-1/3/5 rounds +// ═══════════════════════════════════════════════════════════ + +// ── 1. HTML Template ────────────────────────────────────── function getTicTacToeHTML() { - return ` -
-

🧩 Tic-Tac-Toe

-
- - - -
-
-
- - - - - - - - - -
- -

Player X's turn

- - -
-
- `; + return ` +
+ + +
+ +

Tic Tac Toe

+

Two players or vs AI — classic strategy game!

+ +
+ Game Mode +
+ + +
+
+ + + +
+ Rounds +
+ + + +
+
+ +
+
+ Player 1 + +
+
+ Player 2 + +
+
+ + +
+ + +
+ + +
+
+
P1
+
0
+
+
+
Draws
+
0
+
+
+
P2
+
0
+
+
+ +
Round 1 of 3
+ + +
+ + Player 1's turn +
+ + +
+
+ ${[0,1,2,3,4,5,6,7,8].map(i => + `` + ).join('')} +
+ + + + + +
+ + + + +
+ + +
+
🏆
+
Player 1 Wins!
+
+ P1 + 0 + + 0 + P2 +
+
0 draws
+
+ + +
+
+ +
+`; } +// ── 2. Init Function ────────────────────────────────────── function initTicTacToe() { - const cells = document.querySelectorAll('.cell'); - const statusText = document.getElementById('ticTacToeStatus'); - const restartBtn = document.getElementById('restartTicTacToe'); - const twoPlayerBtn = document.getElementById('twoPlayerMode'); - const computerBtn = document.getElementById('computerMode'); - let vsComputer = false; - let currentPlayer = 'X'; - let board = ['', '', '', '', '', '', '', '', '']; - let gameActive = true; - - - - const pvpBtn = document.getElementById('pvpMode'); - const aiBtn = document.getElementById('aiMode'); - - pvpBtn.addEventListener('click', () => { - vsComputer = false; - - pvpBtn.classList.add('active-mode'); - aiBtn.classList.remove('active-mode'); - - resetGame(); - statusText.textContent = "2 Player Mode"; + // Win combos + const WINS = [ + [0,1,2],[3,4,5],[6,7,8], // rows + [0,3,6],[1,4,7],[2,5,8], // cols + [0,4,8],[2,4,6] // diagonals + ]; + + // Win-line centre coordinates (column, row) in 0-2 grid space + const WIN_COORDS = [ + [[0,0],[2,0]], [[0,1],[2,1]], [[0,2],[2,2]], // rows + [[0,0],[0,2]], [[1,0],[1,2]], [[2,0],[2,2]], // cols + [[0,0],[2,2]], [[2,0],[0,2]] // diagonals + ]; + + // ── State ── + let mode = "2p"; + let difficulty = "easy"; + let maxRounds = 3; + let p1 = "Player 1"; + let p2 = "Player 2"; + let scores = { p1:0, p2:0, draws:0 }; + let board = []; + let current = "X"; // "X" | "O" + let round = 1; + let gameOver = false; + + // ── Helpers ── + function qs(sel, ctx) { return (ctx||document).querySelector(sel); } + + // Show one of the three screens + function showScreen(id) { + ["ttt-setup","ttt-game","ttt-final"].forEach(s => { + const el = document.getElementById(s); + if (el) { + el.classList.toggle("ttt-screen--active", s === id); + } }); - - aiBtn.addEventListener('click', () => { - vsComputer = true; - - aiBtn.classList.add('active-mode'); - pvpBtn.classList.remove('active-mode'); - - resetGame(); - statusText.textContent = "Playing vs Computer"; + } + + // Pill-toggle group helper + function initPillGroup(groupId, onChange) { + const grp = document.getElementById(groupId); + if (!grp) return; + grp.querySelectorAll(".ttt-pill").forEach(btn => { + btn.addEventListener("click", () => { + grp.querySelectorAll(".ttt-pill").forEach(b => b.classList.remove("ttt-pill--on")); + btn.classList.add("ttt-pill--on"); + onChange(btn.dataset.val); + }); }); - - 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] - ]; - - function checkWinner() { - for (let combo of winningCombinations) { - const [a, b, c] = combo; - - if ( - board[a] && - board[a] === board[b] && - board[a] === board[c] - ) { - statusText.textContent = `🎉 Player ${board[a]} wins!`; - gameActive = false; - return; - } - } - - if (!board.includes('')) { - statusText.textContent = "🤝 It's a draw!"; - gameActive = false; - } + } + + // ── Wire up Setup ── + initPillGroup("ttt-mode-group", val => { + mode = val; + const diffGroup = document.getElementById("ttt-diff-group"); + const p2box = document.getElementById("ttt-p2-box"); + const p2inp = document.getElementById("ttt-p2"); + if (val === "ai") { + diffGroup.style.display = "block"; + p2box.classList.add("ttt-dimmed"); + p2inp.disabled = true; + p2inp.placeholder = "Computer 🤖"; + } else { + diffGroup.style.display = "none"; + p2box.classList.remove("ttt-dimmed"); + p2inp.disabled = false; + p2inp.placeholder = "Player 2"; } + }); + + initPillGroup("ttt-diff-pills", val => { difficulty = val; }); + initPillGroup("ttt-rounds-group", val => { maxRounds = parseInt(val); }); + + // Start button + const startBtn = document.getElementById("ttt-start"); + if (startBtn) { + startBtn.addEventListener("click", () => { + p1 = (document.getElementById("ttt-p1").value.trim()) || "Player 1"; + p2 = mode === "ai" + ? "Computer 🤖" + : ((document.getElementById("ttt-p2").value.trim()) || "Player 2"); + scores = { p1:0, p2:0, draws:0 }; + round = 1; + newRound(); + showScreen("ttt-game"); + }); + } + + // Back / Menu buttons + const backBtn = document.getElementById("ttt-back"); + if (backBtn) backBtn.addEventListener("click", () => showScreen("ttt-setup")); + + const menuBtn = document.getElementById("ttt-menu"); + if (menuBtn) menuBtn.addEventListener("click", () => showScreen("ttt-setup")); + + // Rematch button + const rematchBtn = document.getElementById("ttt-rematch"); + if (rematchBtn) { + rematchBtn.addEventListener("click", () => { + scores = { p1:0, p2:0, draws:0 }; + round = 1; + newRound(); + showScreen("ttt-game"); + }); + } + + // Next-round button + const nextBtn = document.getElementById("ttt-next"); + if (nextBtn) { + nextBtn.addEventListener("click", () => { + const majority = Math.ceil(maxRounds / 2); + const matchDone = round >= maxRounds + || scores.p1 >= majority + || scores.p2 >= majority; + if (matchDone) { + renderFinal(); + showScreen("ttt-final"); + } else { + round++; + newRound(); + } + }); + } + + // ── Round management ── + function newRound() { + board = Array(9).fill(null); + current = "X"; + gameOver = false; + + // Reset cells + document.querySelectorAll(".ttt-cell").forEach(c => { + c.textContent = ""; + c.className = "ttt-cell"; + c.disabled = false; + }); - cells.forEach(cell => { - cell.addEventListener('click', () => { - const index = cell.dataset.cell; + // Hide result overlay + const overlay = document.getElementById("ttt-result-overlay"); + if (overlay) overlay.style.display = "none"; - if (board[index] || !gameActive) return; + // Clear win line + clearWinLine(); - board[index] = currentPlayer; - cell.textContent = currentPlayer; + // Update scoreboard + syncScoreboard(); - checkWinner(); + // Round label + const tag = document.getElementById("ttt-round-tag"); + if (tag) { + tag.textContent = maxRounds === 1 + ? "Single Round" + : `Round ${round} of ${maxRounds}`; + } - if (vsComputer && gameActive && currentPlayer === 'X') { - currentPlayer = 'O'; - statusText.textContent = "Computer's turn"; + refreshTurnBanner(); - setTimeout(() => { - computerMove(); - }, 500); + // If AI goes first (not default, but safe to handle) + if (mode === "ai" && current === "O") { + lockBoard(true); + setTimeout(aiTurn, 600); + } + } + + function syncScoreboard() { + const set = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; + set("ttt-sn1", p1); + set("ttt-sn2", p2); + set("ttt-sv1", scores.p1); + set("ttt-sv2", scores.p2); + set("ttt-svd", scores.draws); + } + + function refreshTurnBanner() { + const name = current === "X" ? p1 : p2; + const sym = current === "X" ? "X" : "O"; + const symEl = document.getElementById("ttt-turn-sym"); + const nameEl = document.getElementById("ttt-turn-name"); + const banner = document.getElementById("ttt-turn-banner"); + if (symEl) symEl.textContent = sym; + if (nameEl) nameEl.textContent = name; + if (banner) { + banner.classList.toggle("ttt-turn-banner--x", current === "X"); + banner.classList.toggle("ttt-turn-banner--o", current === "O"); + } + } - return; - } + // ── Cell clicks ── + document.querySelectorAll(".ttt-cell").forEach(cell => { + cell.addEventListener("click", () => { + const i = parseInt(cell.dataset.i); + if (gameOver || board[i]) return; + if (mode === "ai" && current === "O") return; // AI's turn - if (gameActive) { - currentPlayer = currentPlayer === 'X' ? 'O' : 'X'; - statusText.textContent = `Player ${currentPlayer}'s turn`; - } - }); + placeMove(i, current); + afterMove(); }); - - function resetGame() { - board = ['', '', '', '', '', '', '', '', '']; - gameActive = true; - currentPlayer = 'X'; - - cells.forEach(cell => { - cell.textContent = ''; - }); - - statusText.textContent = "Player X's turn"; + }); + + function placeMove(i, sym) { + board[i] = sym; + const cell = document.querySelector(`.ttt-cell[data-i="${i}"]`); + if (!cell) return; + cell.textContent = sym; + cell.classList.add(sym === "X" ? "ttt-cell--x" : "ttt-cell--o"); + cell.disabled = true; + } + + function afterMove() { + const win = getWinner(board); + if (win) { endRound(win); return; } + if (board.every(c=>c)){ endRound(null); return; } + + current = current === "X" ? "O" : "X"; + refreshTurnBanner(); + + if (mode === "ai" && current === "O") { + lockBoard(true); + setTimeout(aiTurn, 480 + Math.random()*200); } + } - function computerMove() { - let emptyCells = []; - - board.forEach((cell, index) => { - if (cell === '') { - emptyCells.push(index); - } - }); - - if (emptyCells.length === 0) return; + function lockBoard(on) { + document.querySelectorAll(".ttt-cell").forEach(c => { + if (!board[parseInt(c.dataset.i)]) c.disabled = on; + }); + } + + // ── Win detection ── + function getWinner(b) { + for (let idx=0; idx { + const cell = document.querySelector(`.ttt-cell[data-i="${i}"]`); + if (cell) cell.classList.add("ttt-cell--win"); + }); + // Draw SVG win line + drawWinLine(win.coordIdx); + + const winnerName = win.sym === "X" ? p1 : p2; + if (win.sym === "X") scores.p1++; else scores.p2++; + syncScoreboard(); + + setResult("🏆", `${winnerName} wins this round!`); + } else { + scores.draws++; + syncScoreboard(); + setResult("🤝", "It's a draw!"); + } - const randomIndex = - emptyCells[Math.floor(Math.random() * emptyCells.length)]; + // Update Next button label + setTimeout(() => { + const majority = Math.ceil(maxRounds / 2); + const matchDone = round >= maxRounds + || scores.p1 >= majority + || scores.p2 >= majority; + const nextBtn = document.getElementById("ttt-next"); + if (nextBtn) nextBtn.textContent = matchDone ? "See Results →" : "Next Round →"; + + const overlay = document.getElementById("ttt-result-overlay"); + if (overlay) overlay.style.display = "flex"; + }, 600); + } + + function setResult(emoji, text) { + const e = document.getElementById("ttt-res-emoji"); + const t = document.getElementById("ttt-res-text"); + if (e) e.textContent = emoji; + if (t) t.textContent = text; + } + + // ── Win-line SVG ── + // Grid cells are (col, row) 0-indexed; centre of cell = col+0.5, row+0.5 + function drawWinLine(comboIdx) { + const line = document.getElementById("ttt-win-line"); + const svg = document.getElementById("ttt-win-svg"); + if (!line || !svg) return; + + const [[c1,r1],[c2,r2]] = WIN_COORDS[comboIdx]; + line.setAttribute("x1", c1 + 0.5); + line.setAttribute("y1", r1 + 0.5); + line.setAttribute("x2", c2 + 0.5); + line.setAttribute("y2", r2 + 0.5); + line.setAttribute("opacity", "1"); + svg.classList.add("ttt-win-svg--visible"); +} + function clearWinLine() { + const line = document.getElementById("ttt-win-line"); + const svg = document.getElementById("ttt-win-svg"); + if (line) line.setAttribute("opacity","0"); + if (svg) svg.classList.remove("ttt-win-svg--visible"); + } + + // ── Final screen ── + function renderFinal() { + const set = (id,v) => { const el=document.getElementById(id); if(el) el.textContent=v; }; + set("ttt-fp1", p1); + set("ttt-fp2", p2); + set("ttt-fp1s", scores.p1); + set("ttt-fp2s", scores.p2); + set("ttt-final-draws", `${scores.draws} draw${scores.draws!==1?"s":""}`); + + let title; + if (scores.p1 > scores.p2) title = `🏆 ${p1} wins the match!`; + else if (scores.p2 > scores.p1) title = `🏆 ${p2} wins the match!`; + else title = "🤝 The match is tied!"; + set("ttt-final-title", title); + } + + // ── AI engines ── + function freeCells(b) { + return b.reduce((acc,v,i) => { if(!v) acc.push(i); return acc; }, []); + } + + function checkWinFor(b, sym) { + return WINS.some(([a,x,c]) => b[a]===sym && b[x]===sym && b[c]===sym); + } + + function minimax(b, isMax, alpha, beta, depth) { + if (checkWinFor(b,"O")) return 10 - depth; + if (checkWinFor(b,"X")) return depth - 10; + if (b.every(c=>c)) return 0; + + const moves = freeCells(b); + if (isMax) { + let best = -Infinity; + for (const m of moves) { + b[m] = "O"; + best = Math.max(best, minimax(b, false, alpha, beta, depth+1)); + b[m] = null; + alpha = Math.max(alpha, best); + if (beta <= alpha) break; + } + return best; + } else { + let best = Infinity; + for (const m of moves) { + b[m] = "X"; + best = Math.min(best, minimax(b, true, alpha, beta, depth+1)); + b[m] = null; + beta = Math.min(beta, best); + if (beta <= alpha) break; + } + return best; + } + } - board[randomIndex] = 'O'; - cells[randomIndex].textContent = 'O'; + function chooseMove(b, diff) { + const moves = freeCells(b); + if (!moves.length) return null; - checkWinner(); + // Easy — random + if (diff === "easy") return moves[Math.floor(Math.random()*moves.length)]; - if (gameActive) { - currentPlayer = 'X'; - statusText.textContent = "Player X's turn"; - } + // Medium — win → block → center/corners + if (diff === "medium") { + for (const m of moves) { b[m]="O"; if(checkWinFor(b,"O")){b[m]=null;return m;} b[m]=null; } + for (const m of moves) { b[m]="X"; if(checkWinFor(b,"X")){b[m]=null;return m;} b[m]=null; } + for (const p of [4,0,2,6,8,1,3,5,7]) { if(!b[p]) return p; } + return moves[0]; } - restartBtn.addEventListener('click', resetGame); -} \ No newline at end of file + // Hard — minimax + let bestScore=-Infinity, bestMove=moves[0]; + for (const m of moves) { + b[m]="O"; + const s = minimax(b, false, -Infinity, Infinity, 0); + b[m]=null; + if (s > bestScore) { bestScore=s; bestMove=m; } + } + return bestMove; + } + + function aiTurn() { + if (gameOver) return; + const move = chooseMove([...board], difficulty); // pass copy so minimax doesn't corrupt state + lockBoard(false); + if (move !== null) placeMove(move, "O"); + afterMove(); + } + +} // end initTicTacToe \ No newline at end of file