From d278add605ede4fd33a092029d0f622ca0d18621 Mon Sep 17 00:00:00 2001 From: Nency Patel Date: Sat, 23 May 2026 15:52:47 +0530 Subject: [PATCH 1/4] Add initial Tic Tac Toe web app integration --- games/Tic-Tac-Toe/Tic-Tac-Toe.py | 603 ++++++++++++++++--------------- web-app/index.html | 8 + 2 files changed, 320 insertions(+), 291 deletions(-) diff --git a/games/Tic-Tac-Toe/Tic-Tac-Toe.py b/games/Tic-Tac-Toe/Tic-Tac-Toe.py index 9834f58..7398774 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,216 +191,235 @@ 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 diff --git a/web-app/index.html b/web-app/index.html index 7fb4c5b..e62f373 100644 --- a/web-app/index.html +++ b/web-app/index.html @@ -1286,6 +1286,14 @@

Rock Paper Scissors

+
+
+ +
+

Tic Tac Toe

+

Classic X and O strategy game!

+ +

Dice Rolling

Roll the dice with 3D animation!

From d02f9371e401657cb71c9eeb7c1bf6a471f61e15 Mon Sep 17 00:00:00 2001 From: Nency Patel Date: Sat, 23 May 2026 19:20:58 +0530 Subject: [PATCH 2/4] Implement advanced Tic Tac Toe game module --- games/Tic-Tac-Toe/Tic-Tac-Toe.py | 2 +- web-app/assets/banners/tic-tac-toe.jpg | Bin 0 -> 11039 bytes web-app/css/styles.css | 482 ++++++++++++++++++ web-app/index.html | 13 +- web-app/js/projects.js | 674 +++++++++++++++++++------ web-app/js/projects/tic-tac-toe.js | 661 ++++++++++++++++++------ 6 files changed, 1520 insertions(+), 312 deletions(-) create mode 100644 web-app/assets/banners/tic-tac-toe.jpg diff --git a/games/Tic-Tac-Toe/Tic-Tac-Toe.py b/games/Tic-Tac-Toe/Tic-Tac-Toe.py index 7398774..4dd4603 100644 --- a/games/Tic-Tac-Toe/Tic-Tac-Toe.py +++ b/games/Tic-Tac-Toe/Tic-Tac-Toe.py @@ -424,4 +424,4 @@ def main(): 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 0000000000000000000000000000000000000000..781d9afa412a3c46a6946b7509c89ab2561e300c GIT binary patch literal 11039 zcmXweV{j!*xO8kg+1R#id!vncf{kr!<78vow(VqNo!Iut#<=_5@4G!!)7{TJRnt{d zHS=S@z`zIru3nBN?$!XXf3UL#u>Gg)tWEy&D|QyHCjZs{fy~O>*7^TQFfd1JGuQvq z|Hmkf)~y5d?+w-x`*+i=)MV0t^h|UoHsYe*(?X+SB^K z3M3pH+&{vxcC`Dy{Qp8g20=iAfzdPBnVYy`fFU5@Q(NLa1w#-Z!?7o8NUDN~L7}NW zbk2(l#!>pqV*-fy%d2blqD!X)>y=XD*8uXa_j|WNhTVN&5YJKEboCMb)CgVOc(Z4! zx-#Mix-me+RR3mH=!6Uu1A;fPGZ45kSXW)w%CON%bQ>hy%bjI2*At}@R_|sz>BT#B z20}J;$VQyiRD0fYtWMja<)8%k`eE)Dr_LXk;=d(PH3Ewe5U& zx%>Swa_spgtDk3&pX(QPr}$utq1TFeyc}6b#bwKW3CgWhlP1K#^v);bHKT4)9xkUJ?yx-IIWp=<43(cwh1iT&;$+u$8IwbqA)MLHS8(LThm627 z7{OL#5WJwrbiF$q^^DEpZbX@i&GCMwiWa9GBTU&v(Yb}eVGXQcAjoL^e?yc``a zYnB4-&w5EH*Y%y#x{r6~1j7322V|d=<1z9%1p*Ge%c#9I8a)=H33mr-K^B&{2CY~M z$!+$LtRR*F-Rv{opTl(_!Si?h^_SZKrQ-eFiHhoi^)b+9`0NwJ{b>Rb-WpwmI9Zlu_bJ!SjqOBCxTs9Ex6E`(Q0Z6#Ap_aOlVE z51g2+#^ss{vM8&tXN-q^T{FPji}PTt&+huNHED_&uNa%R?UAPDm$k;!@zs-6eg%pF zH3(==+Df#b+*NgFDJnb=SUXbN@jESuMbIxgB(Tz`?r{_zda={FxfFWe<6a3HPhCtS zww${UOFcS;%>dhafr`jm%ithM#GY_Qacw7MDJn+fT|KYR)4jDsG2 z-*C={Hnsf8kt6oF#321XhEONx{Pm+2C2p93F1%lF>AKG*>wagqtE5@WpPz+TqeA#z ziEm7Yuh~&w1LTVj-V@~>hpIuSd8B#a7j&W#oz(xf(3#9ns+9~Y<}X5qA#$B9-z_3_ zv%T3{O47IO*_e}6OafmI%NjOv-Q8k=R)7@ukoiI(T+C-D^fRTt^;it^Hg_8!FSz-Z zCG)fK-SJ!}a_3{G-}It&s+~kGQs9+7N;hVe`+2b?b@7Sxx^jCVDT`X+fUB6jh>kr_ z(LDsd<*Q0@l?_FGomsBWc6q-y;~L~zG3QG2qa+#(oAlFKhojeW3Q-pWvg8M9Sl2+t z>cYOL>W7FIy1^&HmmoSU4|cZ#7_GwJ-B{vLzVsShjWZQ%?-E~Mo&E-3Sf9dKjA zB2N^VexT}!wR!KifNTv>KwdYHZai1T0M5=QnoP6lsb$P?<`Gzby&bR9qt}U~@Ga39 zX-4I#d%Y2J2v8v?_GBP$Etz=@+BwVJ-)srxk9y;_i4I}Gn5?qn(D0M@;FX1Q^XWhv zfX5pOdG+H+*yUA_V>aRCJBknV1JiRGoLHtVV1;8Vxv{k-fxQlzs{ymH!jYyhJe(d; zj4vdh{LgwAep#M@_gX<)l74u%Qb+e2i=sSO71s1^5$|yL4|PS?dI*oGrz-$CasKV@ zOxts)*_?~y4M2If(#B|BbygF_W`0s9wh$1N-#1Z*oB{m1oKini!R;Q!}9IiTA?(N>~TM6BUVF$B&=D@s?a5F>zUPWyzLyz z85}o%7VXT}^_yY{^1;iY<(q6Y9t=ix`b>mo^G%!J&9#pcfnq1vV=2=Nrche6Wy0Ar znVOq&D%8VDeWejM+)U(%WEg9^r)jd+mkgrUTlaB{fhzdC@bb5_wKy#%0h3>8N#Pbm z{k_In^KxLYV|PJegG5!O!ksf)<%F2U37&6^pXm=BS31T>`kek2ap7D6o@|5Hr3wzh zTrn+0oX-(M7}ieFz@7R%<%L)}EAR0&ie6yw0IjRB1hgFVm+$b``wP}dZIOSG`5TJH7=5Ve(%aS+l z@@cDk|AsjB9)p#A#0>>!dW`iNwxB+uEPgyLDpMa;MI<;}4_^ z-~{H84xNoMi=EHCx!9=Ppnp%(duRK`C=kucg>r@C7somdEcIu$4w^Vgs+ z0CxR9f^#3Xw2I^jkJ(Tz>P~#eADyntwyBMlmG8~k(Q&2=-Rf87L!%{DQ+{6f{Y=%) zyY26UFS)OU*@;42Y{pa zbEF87<2#52PwAdI(TePT&tO|PDiEp9kP?*VatO=KTgY+}bsievWkO!(pW8tpT_n4A zDNd+Zum&<@Gfzu>L_eM5nKcvhv_m7-Iof<5l;Rs3*!em9g&k+OwI@e6PXrvmF*ACe z>%OAy;R2jY`S13~E~k!e{DCY744*@k4(+XFe#=_SJoo!J;*1}iV6n-@CX@~koL#hK zM_1ztCI|0!1hh3LYmZjLZb2A;^3SnkYErw8bz4NxU8H+n=k1O#M_WVyK^?!VtZg|KPojpCC^1&tVuM&p9!%~2 zEKIX8w3kwz{wd@0tumELWo94XTBQoi$>>){lQ31fl&Z>0it7rd3`=j(EMoL5@k`w5Q)Y| z^BWfkde?o44+4+hz#Dct6C|$vH^n}*!VO(N0km2l56{j{_x>4F+he^Shg&&e31QEC z;5?%{hH(4^8cW&tZMzl84GFKD$^!dSYfhuQG5=T^W$TZqf-s%q9z{_-I<;IML{v+Z zS9-M<*}+ezjJy_sV5rNsuR6u9U`Q4KB2aQYx-aMg+ldgs@^ zPu&yNH1{luEpbi)B-pRYi|=oBvBv9(mE#u+&I?fg%ETK9{^dBGj#<&FK)sRy=SajZ z7px1py=Mc*m-5z4&4FGWBDGM?VTvbELVBUpA5dv}{NRQs6sg+f4l4Kp+Quv>i=#DL zCZq^^M>;}`{Zw$E-l(rzKSeHO`f zhx%YG+S4Ls#JL+>ukVv%HH+fs^#%m(-Uh(;;A3A_c48Vr2tHYl%YmyQ_9j3lQnGv{ zGamNCK3ljiRS}f+?{3@}@q_*P3`vthlfw{yLAsB@c4|?eu-SG7%F=MAn83m9guf4S zHl<$ji@j-!?LCm*%(s<#=9oY#a~;J$AxCCgT8Ch}y)&&5=`~mfXb3~q8$RkC1RQqx zu!yOuZR@;g_<-h0NsM57)ayp>k5JPO>bZ=hYW|i;up48orv7M+700!HEDGqP{9@pg zOQXRhIXlfaqn@Mo-H14xqSKi~n-b>AtRG(^dAp>)a2uVRls5K|!6x8OcI`8{r&{Lx?g+J~sgeY=%pT@*5yYw@eM_h(n;p(vJ|>3$P4Obp5_;D~ z_p^>s(!9nf{nwbU3a&afi__iO8J9B#Y@&5#7^0TOhNUh8dWJs4p4^BQ z1V>yi>gP(Kwr`!C4>falIeY;IyGkK67naXCdnjj`6NX+j62x;c!_Et{TCZ4Y37oWD z5Xn7~mcn25IHjA8=I?J%glw_F74``;-<6(b5Q+($$Hp$O0K;YL=d&6{WeJ+yBL#{M z`f8w!oDxAiYB~-xJgBKW2Cl{=sVMc_Gt$~uoI&n>-nm@HUl0(6B!7JikLt}xyBW-6 zTKCB^MD#b(8Ei8B#A#OP)q35ffgvC3bg$_mD<|Qd>U8koMIAtgK!2<-PT-!L@_w-V zZxZB2Q5R{l%`G{H8y2mdlqls6Mp@Um`IkJS#6z+~1Nkir{h{t&QaaVz@evm5jvE-O zhP(X&{y-C$ z8}co~*F3X+=epnL5CNtetT6DzMqRCEH$59QTFz>9#kUiZ?%FRaw7p_NP~Dl zrSnFN+b>vznP4x`5`P;X#arF}`z#of_F_PcxZw9~A}fu&*ua`$%i6PZVd2XySEIHd zce*1SENv*lMm8)`DosYJ*!!`9MYUzz#RXPZ_60}HdRSX4QE+%D;FA>u!8E}*LKNes z`(mUEs-pUkhv-jXUz0S#YeSk#zS#z;Bi7<%fvj4e z>?ffYh%w<4CefX3^hMkH^0O1;@2To_Cqg*n-f+Z*V-14fNtDsE!T!={F=C)M?d;Id zu83h|H5NI2x`rr%t>#_`xDJskZhEC7eCo4qrJ`i%>lFZFSRn}<>NC;fi?ErjQ^{h9 z{SP0+`z0U@*G_2xLmjVoxf4e1vy*x%y6>g=#}xKAv;B+H&^Q5u;v8VNG(K96R2Y;S6W@qKVkrxMb&S}d+&Fq zW+U6n6Jgq`sEyRU0M^*gn(_Ds^<}HN?lqb%CD@ zH&>4-SJWRdJXZt?mwIW{+{*RZio7?gf6)4V(__sjLBE#9gQwQH*3PweRnC~Zf^mm(x zbILQA#xfK#GAhtcThz<$4!XAu&S!UmQ7D}g@n(lz5{h5+ju^MuR@p#VxV$_k$wX1x zXaH?WLq}=lGNvR{8#a>>>|F%LT#>;n<)J8?J!7c-qGrhD1WtGo&m_I0EMK+}O3YH8 zR!cA9r|ck~BQ%?wgRqgz%{(LlkyJfakt3WW2Z`?2y z(}cU}NHAu8wZsnCYxC;@f}o3kG~rGcogaj^_zwdU)*-W&_PC=A?XStdMO3mpA3UXu zg-8c`i)Y5XYuj`4n~M6#`r-nJg04`if;d=v_%uw_%giLq4AnZiqMtfFu^MSue>B>( z@=sQAz_~R0z}A1n=B801N$Zy$(dl41$ja96aO^s;zaKHF{fY|-<4EsUL3@uXcS`x? z6j?eYE1*7Z*BXxKoPl=LOu1*(=U=v~P0EwmYE0FilB-=;@ydTT{h&`GJf77ObQRDc z;T#?*9kE5fz#~=n3Gh-PEr0qH9$E`&Y#2_bAN@cZB$1JB)x3Z{hgXdqqpx@O$&Z7j z#?}0b-G8M7-%@8Di;MYpFK*+YF6y2i23FTgE~3K(I#)63&sq!)_@|Q%+(awWCJ|1D z)#=7~9qlf$;_Em+-x1gx%D2jbzn5RbqH1a3sLvXm_EF&-6WAd!d3cOti!)I4l(+_G z62PjduJ0fLOQ*Oh1>l>bTqR3l{e87qVXry2R7Q9K@RzQtVRyswG=gPy^zxYs=NH}9 z2?8zP`{~8+U|vYDM6e-YE$*I}#$TOfL%;oJXD&*bL3ty|Te|5Cn^=jjmjkZgv>lQ< zp5(;%2gMP!9lo-&CHBere1FVy5E1*Sdj2SjLHlGsQxSY?d6iMuQ5M?jhV4+4Cza%% zPI0X&mZX<&$eB_8N z8oUJCwvJIxT;lpgO@B%2o5Z0%QV&gyU50kf_Y0$tXU1;3$m1F3xP^fY;`h%2Y7Z;! zvg5Vjpjb)#2%+uB$J8$6>qOl6ng;os)C-%0m#Zy|!eMko!AXQwvt$_b-8e1x`NM#= zc2#DLmz(dL*6ZH0@LPPQ&`D}Bg22~Vu1Pc}7n z?Ju6?TB}!Uo>CB50~I(PXSV3?>c6zZX;k^(Bb$021{gzP9n~pUTUAmPqAn|CC@)-UXnwRQ&xw8hpr2==rmc>BXQv7 z(hsno>}}99OeKLB-f%~@H+pnFDuim4>FuQc zU9sE~+_h>jg^rBxYH2wH9Pbu)-NLY|X)R|fI=AC&=OlOg*;%Xh^t`75glMA`*}}(KK$C1l1d4KChhmhVxor1WFyi}CQeWu(=TVh;XfJ{Jz-`3u=qSHKl5qhgWS6dl<% zRjhrr{i-=0-+dBj0a&5wR2F&twhCmSP0al}^Xw`<;$2X*>h?y}=jjtBRBqcDEyVHf zF_zABziOF{QZ}V-sD=0Jb6U=Z0!3^s3}$v$q>sIlHi(`@msK7>a%TUiUnD7jbZYp}Ld@lT!NW^}AeKp02K)Mx|?VB)xANnW~NS9ve74j$9Znz#L!=4q#+EpA88o9nN?KJpN2| zk&g>1-*79K`f(lgb3_iRC?}hNTY+JhA1Qy`>MDrHQ#RM9x94diDAAWaW3pK>t5a%7 zsVYp4*Xn*%JJ7>3Vk|QcIPJtTl}|Jmz$|QHw6$j?-U6bjWk-zHd~EmHzUywW9abnItz{n|7>xJbx-=S<$F$;_V-Qy7tf zWt123W=Pj2wK9Op!?(Cd-hUq@UxYla4*u&)VY~7z&K9dG&v{PNgED!zdxgLZt016y zpAMOh<={Pih`Z%vU-X(w=##9B4z|m)LxRd7BDMn)&jk)tw;kPJH3dcRFe-1E+u%UL zA*;ub8d5Y!hoa$|??PyY?fs6bUuK^Qu!dI$kSZ!;^VEAaPrbB9ftNTO~X1-zbQ+e}k-L(8Hz=;5?%r12MALn3@M=Sq8;i?R)u zaGzF2_p#{1AhFb_FR-FbnSiX8HQZSvlnTu@?jDFB!MGTaeF_&%{B@3`B9Xk`HkKnzoJ$e( zlHRe^qIIIdXi(LGx9h~2T*&P9YrEu=<0Tn5qq(}ZaEl6+oR*znVbw+3nY7c0J?Uwb8 zy+~Qv{>xFc4}P|$cU(YXM9v;_YNzPvIR6=BZ?1-e;RPC1w5)G(J-tS< zTGDBtA$5+PUoS9;53!PsWp$l|A05DKg626&Qq~yWZTwx4fKh=4jZR3(IXBYWrjfSs zoWdwSyBMn>bTfZh!{=~fqjM>od**f`FPkAeX^z$Z#3OEyYMZY+5Lz6i6PM(fK1D7X z|0J>XCYTR=6}`{J1fjt3ITk?jAv-wi<>8sbR_yyZ4`Ds1yYfY&h0(H`qFotEf57tM z=`i*=^lgj?sg>5XFT&~=33|<8esz>lhn`>%mtUzP_U)7PD96AZ4@gQ|D_AsX`tNQe z{N2W%ZwibDOL|jX_~!kKH$GJxZ%dBothD%o_qY?Tt;6KEF6Phl?-mhLj~%P;!70j& zzdf0@krori^%p0;aK^GYo^!vD&zcQX5XoY+-Uj&!K_n&xzxr7Vo<{;-GSbBjz9J(c z^C*D`vS6D40IVk@iBaI56Xn4iCqo$&p!jF>d7Ze(;4KdyP<~t%os`I2OC(MgwSHtz z@mFi8nsQs&_SxZsyA2S!fN45Y-i#_b=DrTdDR%#J9X|8KTAHn_`bG=fCfA?X)P$R^ ze;*CLo{CT_d$osSg05c1esn49R{fwUOA>F@oA*>A@K^tH1xLMqi%T$pw`teJt9mb- z+e_?rL=Ui%Y%-l4%nq|mz+W79nW@WX~hmtig{G9sX$JgfXVTu|1g)@%=bxJSSe(+-?xj|MIVVtoBj!cM( zV4yq~Gm3Z{b9P|iY`F12X0~I`8*A_rv0U)NwUagKxrPxRGPa^py+i24PEUcWd8$#x zNglZFJE?j2thrMAowBf#nSkMetDODX8u$q)P0|8B7p`R5)8bYapD{x7{<1*=)dgSp zh9wWd^yPJ<1NcdpDzjQJ^Qle?UN?vI;o4SQK8a+pmbNE#eYgTedEv?ccKvFlfOJkj7uSh>DEbHo85{ zuJuAKaG)-sY^-~j^O9wfYJOHs?$x#L@O(J~+LjZIfS@^l~|W zEvuT*MdT7c7HD1k?GD>_+UE}2z2a3S5sNS@Zo;n;GKTxzE9nEp<2dnL))ZBk6j)fPHo1-pI6fa%>>IbKX2npHFHbDK95C0b(b> z_0SL%HFUoA`96XSOD^hsiQZZmOD*lS^+wU}(9Ygel~7NWY8sOrr?Mp|7dCWJSEazC zAR{-}o^yijU3bQ>wgT8#CCv$rB&Kq47lAz4n(n8^?+7!X??Pk>S5HKYn@#X%C8DZd z-pt<5GyMC`RZgV4wrqnKE$&NusA^zmHGt4*bm<){=cZ~IZQa+VccmG&-brw1xIxwN zyBy4DJd1B_^iO__jv_qLpPAydswB%fX2QO$8!H+4xGE)ndw|}=b?A=GWd*_koX{uu zNGotk$E!q64a$bdq_?Cv zus_@8$dJMF)Bx&Sc*&*+O|6mq`}sGQpWw+2b=LAA${YH^T^PUlIn9R|@(;L%Nb^dR zUg}M|@SoZ}xOX)b#1XO8CPZGSrQaT3toVS1A1OFB@b*yEQIM{tt_78DLah5loqpsu z2m)Q9%eS2?>)N;gbW8nG4HnT_9!Fp2XKP}PvGz!}{?L{LRuT{%v&2)k7K~sxjU=u{ zvJ#w&l@o|{1D{insf=$k0IagB4a`i=Y|FCOqg6s?aSj7lZnTVZN0h8sN6Bm~hr=Oa zFLr>?eM$4qe3m+TOFDN?UjSsXB*XUn#%l zn#h<1k5_=^*uvhD397GOIr({*yg@VnnWLNxh&&5X;2BQqN_tu1s}x2MUsOv7C}IaQI_QihO4OdoHkjE(<}VRmZ3>Geg~4+ zO>w~9EyHS*__uONqtkPm8du5CNA9y{?fFMu=ZUo zuYoP1iXvH8R(_ytU7 zWQ4_VGP0J{8}wr^Z>S1VIgPZ(3Xo&-CwIpvu0-GD**R4SR_jYb~E3Vm2M#+ru=*Ubg1SLijsT z;KqX#_Da2}!oJirt=i88H9kZi8DDsRZi6)q0jK;ow}&a8Pi+xct-%WlbY%~>?setF zY4%LY6Fq8Z+WL`jxuXT}vXw2P<8=7F2)y<#n728~qi^NyHr^pa(m^C6r6YqS&vc)M z*X4QZ7iDzX5xQ)mPX~8S%HL;Ojib^vibr}R@VP~vU0+7(bE{q=_Y7MF96uZhZh&*H zp@I*K!KYIBb}+{nM%mylab}Oi*zt}PI|U>yShRaNVx5+Qu%ng}Htkd&t%JjHM^xqK zwtwX-J?-dPYH3$S-Jw?{`Yda)28}Zg+w?qsOC&0(?sSxX5xGLugd7ve37qNUgrzVF zvlHasg*>c>KcZcau3EbVtmF0C+40ekwRjAm;h$5zi>Hxu?jMI&%%;o%V%o&=LbqM@ zM~;3m<@9%-dh6V)U23qD4qJ=wfhSK>eE*$ABeG>>TqFOQ0g;${kHKfmx8`t%wEmNt z70U_U%a*#I{9X(JNyZl}(|6CD1LN@eq0sG%`CN6IH&gD}Sq^yxKb1oJct(AQH8Dxz-by|L`A-{JXy*?X$P*p|5^*CT#CF1PbJ87R+4^O5;n*P_&}(|E z-!JycS|ZalYl^B28oaI?@tSWxx2=`we;Tq}~Y4sk$jP&$-}TOD>d znyqDwK3x3MqpxKu3Mp~FPOI(qV~n9fnyd2Kbo5i9LOU(+xR Ja<4h<{ts~FPvrmr literal 0 HcmV?d00001 diff --git a/web-app/css/styles.css b/web-app/css/styles.css index f0dae01..0bb39b4 100644 --- a/web-app/css/styles.css +++ b/web-app/css/styles.css @@ -4353,4 +4353,486 @@ html[data-theme="dark"] body { padding: 1rem; } } +} + +/* ============================== + 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; +} +.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; +} +.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: 100%; + height: 100%; + pointer-events: none; + overflow: visible; + opacity: 0; + transition: opacity .25s; + z-index: 2; +} +.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; } } \ No newline at end of file diff --git a/web-app/index.html b/web-app/index.html index bed0d93..2064ec2 100644 --- a/web-app/index.html +++ b/web-app/index.html @@ -1185,8 +1185,17 @@

Rock Paper Scissors

-

Tic Tac Toe

-

Classic X and O strategy game!

+ +
+ Tic Tac Toe + +
+ +
+ +

Tic Tac Toe

+

Classic X and O strategy game!

+
Dice Rolling
diff --git a/web-app/js/projects.js b/web-app/js/projects.js index 517c72e..31ec44b 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/ @@ -2876,187 +2876,539 @@ 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()) { + refreshTurnBanner(); - document.getElementById('status').innerText = - `Player ${currentPlayer} Wins!`; - - 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" ? "❌" : "⭕"; + 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 resetGame() { + }); + + function placeMove(i, sym) { + board[i] = sym; + const cell = document.querySelector(`.ttt-cell[data-i="${i}"]`); + if (!cell) return; + cell.textContent = sym === "X" ? "❌" : "⭕"; + 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); + } + } - board = [ - '', '', '', - '', '', '', - '', '', '' - ]; + 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!"); + } - currentPlayer = 'X'; + // 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; + } + } - 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..4167aa3 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" ? "❌" : "⭕"; + 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 === "X" ? "❌" : "⭕"; + 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 From a035c8e793f2ab0973175fd09e21c00a30c70c4f Mon Sep 17 00:00:00 2001 From: Nency Patel Date: Sun, 24 May 2026 11:22:42 +0530 Subject: [PATCH 3/4] Fix Tic Tac Toe strike alignment and improve marker styling --- web-app/css/styles.css | 8 +++++-- web-app/js/projects.js | 34 +++++++++++++++------------- web-app/js/projects/tic-tac-toe.js | 36 +++++++++++++++--------------- 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/web-app/css/styles.css b/web-app/css/styles.css index 0bb39b4..28d4130 100644 --- a/web-app/css/styles.css +++ b/web-app/css/styles.css @@ -4714,6 +4714,8 @@ html[data-theme="dark"] body { 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; @@ -4721,6 +4723,7 @@ html[data-theme="dark"] body { 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; @@ -4743,13 +4746,14 @@ html[data-theme="dark"] body { .ttt-win-svg { position: absolute; inset: 0; - width: 100%; - height: 100%; + 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 { diff --git a/web-app/js/projects.js b/web-app/js/projects.js index 31ec44b..d27dcfa 100644 --- a/web-app/js/projects.js +++ b/web-app/js/projects.js @@ -2962,7 +2962,9 @@ function getTicTacToeHTML() {
-
+
+
+ ${[0,1,2,3,4,5,6,7,8].map(i => `` ).join('')} @@ -2974,7 +2976,7 @@ function getTicTacToeHTML() { stroke="var(--ttt-accent)" stroke-width="0.18" stroke-linecap="round" opacity="0"/> - +