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 @@
Classic snake game!
+
+
+ Classic X and O strategy game!
+
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 `
-
-
- Player X's Turn
+// ============================================ +// Tic-Tac-Toe +// ============================================ - -Two players or vs AI — classic strategy game!
+ +Player X's turn
- - -Two players or vs AI — classic strategy game!
+ +