From d278add605ede4fd33a092029d0f622ca0d18621 Mon Sep 17 00:00:00 2001 From: Nency Patel Date: Sat, 23 May 2026 15:52:47 +0530 Subject: [PATCH 01/23] 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 c76698174a5187e28c667aafdadf04cd4e9016bc Mon Sep 17 00:00:00 2001 From: naina-kashyap Date: Sat, 23 May 2026 18:12:46 +0530 Subject: [PATCH 02/23] Added new Python mini project --- gssoc2/.vscode/settings.json | 3 + gssoc2/index.html | 94 +++++++++++++++++++++++++++ gssoc2/script.js | 11 ++++ gssoc2/style.css | 122 +++++++++++++++++++++++++++++++++++ 4 files changed, 230 insertions(+) create mode 100644 gssoc2/.vscode/settings.json create mode 100644 gssoc2/index.html create mode 100644 gssoc2/script.js create mode 100644 gssoc2/style.css diff --git a/gssoc2/.vscode/settings.json b/gssoc2/.vscode/settings.json new file mode 100644 index 0000000..6f3a291 --- /dev/null +++ b/gssoc2/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "liveServer.settings.port": 5501 +} \ No newline at end of file diff --git a/gssoc2/index.html b/gssoc2/index.html new file mode 100644 index 0000000..ea5fd85 --- /dev/null +++ b/gssoc2/index.html @@ -0,0 +1,94 @@ + + + + + AI Resume Analyzer + + + + + + + +
+ +

AI RESUME ANALYZER

+ + +
+ + + + +

Drag & Drop or Click

+
+ + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/gssoc2/script.js b/gssoc2/script.js new file mode 100644 index 0000000..1118267 --- /dev/null +++ b/gssoc2/script.js @@ -0,0 +1,11 @@ +document.getElementById("analyzeBtn").addEventListener("click", () => { + const file = document.getElementById("resumeInput"); + + if (!file.files.length) { + alert("Upload resume first!"); + return; + } + + document.getElementById("ats").classList.remove("hidden"); + document.getElementById("bottomSection").classList.remove("hidden"); +}); \ No newline at end of file diff --git a/gssoc2/style.css b/gssoc2/style.css new file mode 100644 index 0000000..a479006 --- /dev/null +++ b/gssoc2/style.css @@ -0,0 +1,122 @@ +body { + margin: 0; + font-family: Arial; + background: linear-gradient(to right, #0f172a, #020617); + color: white; +} + +/* CONTAINER */ +.container { + max-width: 900px; + margin: auto; + padding: 30px; +} + +/* TITLE */ +.title { + text-align: center; + font-size: 32px; + margin-bottom: 20px; + + background: linear-gradient(90deg, #38bdf8, #22c55e); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; +} + +/* UPLOAD */ +.upload-box { + border: 2px dashed #38bdf8; + padding: 30px; + border-radius: 12px; + text-align: center; +} + +.icon { + font-size: 40px; +} + +/* BUTTON */ +.file-btn { + background: #38bdf8; + padding: 10px 20px; + border-radius: 8px; + cursor: pointer; + display: inline-block; + margin-top: 10px; +} + +#analyzeBtn { + margin-top: 20px; + width: 100%; + padding: 12px; + background: #22c55e; + border: none; + border-radius: 8px; +} + +/* CARD */ +.card { + background: #0f172a; + padding: 20px; + border-radius: 14px; + border: 1px solid #334155; + margin-top: 25px; +} + +/* ATS */ +.progress-circle { + width: 130px; + height: 130px; + border-radius: 50%; + border: 10px solid #22c55e; + display: flex; + align-items: center; + justify-content: center; + margin: 15px auto; +} + +.extra-info { + text-align: center; + font-size: 13px; + color: #94a3b8; +} + +/* BOTTOM GRID */ +.bottom-section { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; +} + +/* KEYWORDS */ +.keyword-item { + margin-bottom: 15px; +} + +.bar { + background: #1e293b; + height: 8px; + border-radius: 5px; +} + +.bar div { + height: 100%; + background: #38bdf8; +} + +/* SUGGESTIONS */ +.suggestion { + display: flex; + gap: 10px; + margin: 10px 0; +} + +.suggestion i { + color: #22c55e; +} + +/* HIDDEN */ +.hidden { + display: none; +} \ No newline at end of file From d02f9371e401657cb71c9eeb7c1bf6a471f61e15 Mon Sep 17 00:00:00 2001 From: Nency Patel Date: Sat, 23 May 2026 19:20:58 +0530 Subject: [PATCH 03/23] 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 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 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 c61c5bec7581fe0fbff5e5acc23eacc8c958a5df Mon Sep 17 00:00:00 2001 From: Alv24-hub Date: Sat, 23 May 2026 23:43:25 +0300 Subject: [PATCH 04/23] feature: add instruction label for first-time players in 2048 game --- games/2048-Game/2048-Game.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/games/2048-Game/2048-Game.py b/games/2048-Game/2048-Game.py index bfb7307..a4d5578 100644 --- a/games/2048-Game/2048-Game.py +++ b/games/2048-Game/2048-Game.py @@ -67,6 +67,14 @@ def __init__(self, root): ) self.restart_button.grid(pady=5) + self.instruction_label = tk.Label( + root, + text="🎮 Controls: ← ↑ → ↓ | Merge same numbers | Goal: 2048 🎯", + font=("Arial", 10), + fg="#6a635b", + ) + self.instruction_label.grid(pady=5) + self.cells = [] self.board = [[0] * GRID_SIZE for _ in range(GRID_SIZE)] From b1d03414d74949be985f479ce10baf2b6c0887db Mon Sep 17 00:00:00 2001 From: mahi-8758 Date: Sun, 24 May 2026 03:08:58 +0530 Subject: [PATCH 05/23] docs: expand WEB_APP_GUIDE with interactive animations and advanced patterns - Enhanced welcome section with CSS animations (gradient, float, fade-in effects) - Added 2,000+ lines of comprehensive documentation - Included advanced architecture patterns (StateManager, ComponentRegistry, APIClient) - Added comprehensive folder structure walkthrough with file descriptions - Expanded developer guides (performance profiling, troubleshooting, DevTools) - Added testing patterns, error handling, and security best practices - Included analytics, i18n, gesture handling, and utility functions - Enhanced accessibility documentation with code examples - Added responsive design patterns and CSS techniques - Total: 2,894 lines, 82.5 KB - Exceeds target of 2,500-3,000 lines --- WEB_APP_GUIDE.md | 3538 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3538 insertions(+) create mode 100644 WEB_APP_GUIDE.md diff --git a/WEB_APP_GUIDE.md b/WEB_APP_GUIDE.md new file mode 100644 index 0000000..30d1208 --- /dev/null +++ b/WEB_APP_GUIDE.md @@ -0,0 +1,3538 @@ +## + + +
+

+ 🚀 + 🎨 + + 🐍 +

+

Web App Architecture Guide

+

+ + Interactive Frontend Documentation + +

+

+ A modern, accessible web experience powered by vanilla JavaScript, Pyodide, and Web Workers. +

+
+ 🎯 Architecture + 🎨 Components + ♿ Accessibility + ⚡ Performance +
+
+ +## + +## 📑 Quick Navigation + +| Section | Purpose | Time | +|---------|---------|------| +| 🎯 [Architecture](#-system-architecture) | Core system design | 5 min | +| 🎨 [Components](#-component-library) | UI component reference | 8 min | +| 🎭 [Design System](#-design-system) | Colors, typography, animations | 6 min | +| ♿ [Accessibility](#-accessibility-standards) | WCAG 2.1 AA compliance | 7 min | +| 📱 [Responsive Design](#-responsive-design) | Mobile-first approach | 5 min | +| 👨‍💻 [Contributing](#-contributor-guide) | Setup & workflow | 10 min | +| ✨ [Adding Features](#-adding-new-projects) | Step-by-step guides | 10 min | + +--- + +## 🎯 System Architecture + +### High-Level Overview + +```mermaid +graph TB + subgraph Browser["🌐 Browser Execution"] + DOM["DOM\n(HTML)"] + CSS["Styles\n(CSS Variables)"] + MainThread["Main Thread\n(UI & Events)"] + Worker["Web Worker\n(Python Execution)"] + end + + subgraph Modules["📦 JavaScript Modules"] + main["main.js\n(orchestration)"] + playground["playground.js\n(Pyodide)"] + projects["projects.js\n(registry)"] + storage["storage.js\n(persistence)"] + audio["audio.js\n(sounds)"] + end + + External["🔌 Pyodide\n(Python Runtime)"] + + MainThread -->|control| DOM + MainThread -->|wire events| main + main -->|message passing| playground + playground -->|Web Worker| Worker + Worker -->|execute async| External + main -->|read/write| storage + main -->|play| audio +``` + +### Project Philosophy + +**Core Principles:** + +1. **Zero Installation** — Run in browsers without Python install +2. **Progressive Enhancement** — Works with minimal JS, scales up +3. **Accessibility First** — WCAG 2.1 Level AA built-in +4. **Mobile-First** — Designed for mobile, scales to desktop +5. **Modular Architecture** — Independent, testable modules +6. **Performance** — Non-blocking execution, optimized rendering + +### Thread Model: Why Web Workers? 🧵 + +Python code execution happens on a **separate thread** to prevent UI freezing: + +```mermaid +sequenceDiagram + participant User + participant MainThread as Main Thread + participant Worker as Web Worker + participant Pyodide as Python Runtime + + User->>MainThread: Click "Run Code" + Note over MainThread: ✅ Still responsive! + MainThread->>MainThread: UI remains interactive + MainThread->>Worker: postMessage(python_code) + Note over Worker: Long computation + Worker->>Pyodide: Execute python asynchronously + Pyodide->>Pyodide: Heavy processing... + Pyodide->>Worker: Returns stdout/stderr + Worker->>MainThread: postMessage(result) + MainThread->>User: Display result instantly +``` + +**Benefits:** +- ✅ UI never freezes during computation +- ✅ Users can interact while code runs +- ✅ Click events process instantly +- ✅ Multiple computations can queue + +### Infinite Loop Protection ⚡ + +The critical challenge: **How to stop an infinite loop?** + +**Solution: Worker Termination** + +```javascript +// When user clicks "Stop" +function stopExecution() { + if (worker) { + worker.terminate(); // ← Instantly kills the thread + worker = null; + spawnWorker(); // ← Create fresh worker + } +} +``` + +**Why this works:** +- `terminate()` immediately halts the worker thread +- Even mid-infinite-loop, execution stops instantly +- No timeout hacks needed +- Fresh worker loads in ~100ms from cache + +--- + +## 🎨 Component Library + +### 🔘 Button Component + +**Purpose:** Trigger actions — submit forms, launch projects, toggle features + +```html + + + + + + + + + + + +``` + +**CSS Structure:** + +```css +.btn { + padding: 0.75rem 1.5rem; + border: none; + border-radius: 0.5rem; + font-weight: 600; + cursor: pointer; + transition: var(--transition); + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + +.btn-primary { + background-color: var(--primary-color); + color: var(--on-accent); +} + +.btn-primary:hover { + filter: brightness(1.1); + box-shadow: var(--shadow); +} + +.btn:focus { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + +.btn:active { + transform: scale(0.98); +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} +``` + +**Variants:** +| Variant | Usage | +|---------|-------| +| `btn-primary` | Main CTA, important actions | +| `btn-secondary` | Alternative actions | +| `btn-danger` | Destructive actions | +| `btn-icon` | Icon-only (must have aria-label) | + +**Accessibility:** +- ✅ Keyboard focusable (Tab key) +- ✅ Enter/Space activates +- ✅ Focus indicator visible +- ✅ Minimum 44×44px hit target +- ✅ Screen reader friendly + +--- + +### 🃏 Card Component + +**Purpose:** Display project information in scannable units + +```html +
+
+

🎮 Rock Paper Scissors

+ Game +
+ +

+ Classic strategy game with AI opponent. Master the mind games! +

+ + + + +
+``` + +**CSS:** + +```css +.project-card { + background: var(--surface-color); + border: 1px solid var(--border-color); + border-radius: 0.75rem; + padding: 1.5rem; + transition: var(--transition); + cursor: pointer; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.project-card:hover { + transform: translateY(-4px); + box-shadow: var(--shadow); + border-color: var(--primary-color); +} + +.project-card:focus-within { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + +.card-title { + font-size: 1.25rem; + margin: 0; + color: var(--text-color); +} + +.card-description { + color: var(--text-secondary); + margin: 0; + line-height: 1.5; +} + +.badge { + display: inline-block; + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.85rem; + font-weight: 600; + background: var(--accent-soft); + color: var(--primary-color); + border: 1px solid var(--accent-border); +} +``` + +**Grid Responsive:** + +```css +.projects-grid { + display: grid; + grid-template-columns: 1fr; + gap: 1rem; +} + +@media (min-width: 640px) { + .projects-grid { + grid-template-columns: repeat(2, 1fr); + gap: 1.5rem; + } +} + +@media (min-width: 1024px) { + .projects-grid { + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 2rem; + } +} +``` + +--- + +### 🪟 Modal Component + +**Purpose:** Display project UIs in focused overlay without navigation + +```html + + +
+``` + +**CSS:** + +```css +.modal { + position: fixed; + inset: 0; + z-index: 2000; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + animation: fadeIn 0.3s ease; +} + +.modal[hidden] { display: none; } + +.modal-overlay { + position: absolute; + inset: 0; + background: var(--overlay-color); + cursor: pointer; +} + +.modal-dialog { + position: relative; + z-index: 1; + background: var(--surface-color); + border-radius: 0.75rem; + max-width: 90vw; + max-height: 90vh; + overflow: hidden; + display: flex; + flex-direction: column; + box-shadow: var(--shadow-modal); + animation: slideUp 0.3s ease; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5rem; + border-bottom: 1px solid var(--border-color); +} + +.modal-close:focus { + outline: 2px solid var(--primary-color); + border-radius: 0.25rem; +} + +.modal-body { + padding: 1.5rem; + overflow-y: auto; + flex: 1; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideUp { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} +``` + +**Focus Management (JavaScript):** + +```javascript +function openProjectModal(projectName) { + const html = getProjectHTML(projectName); + modalBody.innerHTML = html; + modal.removeAttribute('hidden'); + mainContent.setAttribute('inert', ''); + modalClose.focus(); +} + +function closeProjectModal() { + modal.setAttribute('hidden', ''); + mainContent.removeAttribute('inert'); + triggerButton.focus(); +} + +document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && !modal.hasAttribute('hidden')) { + closeProjectModal(); + } +}); + +document.getElementById('modalOverlay').addEventListener('click', closeProjectModal); +modalClose.addEventListener('click', closeProjectModal); +``` + +--- + +### 🔍 Search Component + +**Purpose:** Enable users to quickly find projects by name or keyword + +```html +
+ + + + +
+``` + +**Features:** +- Keyboard shortcut `/` to focus +- Debounced 300ms for performance +- Recent searches saved to localStorage +- Dropdown with suggestions +- Escape key closes dropdown + +--- + +### 🐍 Python Playground + +**Purpose:** Interactive code editor and executor powered by Pyodide + +```html + +``` + +**Keyboard Shortcuts:** +| Shortcut | Action | +|----------|--------| +| `Ctrl+Enter` | Run code | +| `Tab` | Insert tab | + +--- + +### 🎨 Theme Toggle + +**Purpose:** Allow users to switch between dark and light themes + +```javascript +function toggleTheme() { + const html = document.documentElement; + const currentTheme = html.getAttribute('data-theme') || 'dark'; + const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; + + html.setAttribute('data-theme', newTheme); + const icon = document.getElementById('themeIcon'); + icon.className = newTheme === 'dark' ? 'fas fa-moon' : 'fas fa-sun'; + localStorage.setItem('theme', newTheme); +} + +function initTheme() { + const saved = localStorage.getItem('theme'); + if (saved) { + applyTheme(saved); + } else if (window.matchMedia('(prefers-color-scheme: light)').matches) { + applyTheme('light'); + } else { + applyTheme('dark'); + } +} + +initTheme(); +document.getElementById('themeToggle').addEventListener('click', toggleTheme); +``` + +--- + +## 🎭 Design System + +### 🌈 Color Palette + +#### Dark Theme (Default) +```css +--primary-color: #22c55e /* Brand green */ +--secondary-color: #06b6d4 /* Cyan accent */ +--success-color: #10b981 /* Green success */ +--danger-color: #ef4444 /* Red errors */ +--warning-color: #f59e0b /* Orange warnings */ + +--bg-color: #081120 /* Deep blue background */ +--surface-color: #111827 /* Dark surface */ +--panel-color: #0f172a /* Input/nested bg */ +--text-color: #e5e7eb /* Light text */ +--text-secondary: #94a3b8 /* Muted text */ +--border-color: #1f2937 /* Subtle borders */ +``` + +#### Light Theme +```css +--primary-color: #16a34a /* Darker green */ +--bg-color: #f8fafc /* Light background */ +--surface-color: #ffffff /* White surfaces */ +--text-color: #1f2937 /* Dark text */ +--text-secondary: #6b7280 /* Muted text */ +--border-color: #d8dee8 /* Light borders */ +``` + +### 📝 Typography + +**Font Stack:** +```css +/* Headings */ +font-family: 'Fredoka', 'Segoe UI', sans-serif; + +/* Body */ +font-family: 'DM Sans', 'Segoe UI', sans-serif; + +/* Code */ +font-family: 'IBM Plex Mono', monospace; +``` + +**Responsive Font Sizes:** +```css +h1 { font-size: clamp(1.75rem, 5vw, 3rem); } +h2 { font-size: clamp(1.5rem, 4vw, 2.25rem); } +h3 { font-size: clamp(1.25rem, 3vw, 1.875rem); } + +body { + font-size: clamp(0.95rem, 2vw, 1.1rem); + line-height: 1.6; +} +``` + +--- + +### ✨ Animation & Motion + +**Standard Animations:** + +```css +/* Fade in entrance */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +/* Slide up entrance */ +@keyframes slideUp { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +/* Pulse (loading indicator) */ +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } +} + +/* Spin (loading spinner) */ +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* Bounce (attention) */ +@keyframes bounce { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-10px); } +} + +/* Glow effect */ +@keyframes glow { + 0%, 100% { box-shadow: 0 0 5px var(--primary-color); } + 50% { box-shadow: 0 0 20px var(--primary-color); } +} + +/* Shimmer loading effect */ +@keyframes shimmer { + 0% { background-position: -1000px 0; } + 100% { background-position: 1000px 0; } +} +``` + +**Transition Timing:** +```css +--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +--transition-fast: 0.15s cubic-bezier(0.4, 0, 0.2, 1); +--motion-duration: 0.3s; +``` + +**Reduced Motion Support:** +```css +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} +``` + +--- + +## ♿ Accessibility Standards + +### WCAG 2.1 Level AA Compliance + +| Criterion | Implementation | +|-----------|-----------------| +| **Semantic HTML** | `
+ +
+ +
+ + +
+
+ +
+ + +
+ +
+

Output:

+
+

Your result will appear here...

+
+ +
+ +
+

🔡 How the shift works:

+
+
+
+ + + + `; +} + + +function initCaesarCipher() { + const textBox = document.getElementById('cipherInput'); + const goBtn = document.getElementById('cipherBtn'); + const clearBtn = document.getElementById('clearBtn'); + const copyBtn = document.getElementById('copyBtn'); + const outputBox = document.getElementById('cipherOutput'); + const shiftNumber = document.getElementById('shiftInput'); + const shiftSlide = document.getElementById('shiftSlider'); + const encryptRadio = document.getElementById('modeEncrypt'); + const decryptRadio = document.getElementById('modeDecrypt'); + const theLabel = document.getElementById('inputLabel'); + const previewDiv = document.getElementById('shiftPreview'); + + const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + + function encryptText(text, shift) { + let result = ''; + + for (const ch of text) { + if (ch >= 'A' && ch <= 'Z') { + result += String.fromCharCode(((ch.charCodeAt(0) - 65 + shift) % 26 + 26) % 26 + 65); + } else if (ch >= 'a' && ch <= 'z') { + result += String.fromCharCode(((ch.charCodeAt(0) - 97 + shift) % 26 + 26) % 26 + 97); + } else { + result += ch; + } + } + + return result; + } + + function decryptText(text, shift) { + return encryptText(text, -shift); + } + + function buildAlphabetRow(label, getChar, className) { + const row = document.createElement('div'); + row.className = 'alphabet-row'; + + const labelEl = document.createElement('span'); + labelEl.className = 'row-label'; + labelEl.textContent = label; + row.appendChild(labelEl); + + for (let i = 0; i < 26; i++) { + const box = document.createElement('span'); + box.className = `letter-box ${className}`; + box.textContent = getChar(i); + row.appendChild(box); + } + + return row; + } + + function showPreview(shift) { + previewDiv.innerHTML = ''; + previewDiv.appendChild(buildAlphabetRow('Original:', i => ALPHABET[i], 'plain')); + previewDiv.appendChild(buildAlphabetRow(`Shift +${shift}:`, i => ALPHABET[(i + shift) % 26], 'cipher')); + } + + function setMode(isEncrypt) { + theLabel.textContent = isEncrypt ? '📝 Enter Text to Encrypt:' : '📨 Enter Text to Decrypt:'; + textBox.placeholder = isEncrypt ? 'Type your message here...' : 'Paste the encrypted text here...'; + textBox.value = ''; + outputBox.innerHTML = '

Your result will appear here...

'; + copyBtn.style.display = 'none'; + goBtn.textContent = isEncrypt ? '🔐 Encrypt Message' : '📝 Decrypt Message'; + } + + function syncSlider(val) { + const clamped = Math.min(25, Math.max(1, val)); + shiftNumber.value = clamped; + shiftSlide.value = clamped; + showPreview(clamped); + } + + function showError(msg) { + outputBox.innerHTML = `

❌ ${msg}

`; + copyBtn.style.display = 'none'; + } + + function showResult(text) { + outputBox.innerHTML = ''; + const p = document.createElement('p'); + p.className = 'result-text'; + p.textContent = text; + outputBox.appendChild(p); + copyBtn.style.display = 'inline-block'; + copyBtn.dataset.result = text; + } + + encryptRadio.addEventListener('change', () => setMode(true)); + decryptRadio.addEventListener('change', () => setMode(false)); + + shiftSlide.addEventListener('input', () => syncSlider(parseInt(shiftSlide.value))); + shiftNumber.addEventListener('input', () => syncSlider(parseInt(shiftNumber.value))); + + goBtn.addEventListener('click', () => { + const text = textBox.value; + const shift = parseInt(shiftNumber.value); + + if (!text.trim()) return showError('Please enter some text first!'); + if (isNaN(shift) || shift < 1 || shift > 25) return showError('Please enter a shift value between 1 and 25!'); + + const result = encryptRadio.checked ? encryptText(text, shift) : decryptText(text, shift); + showResult(result); + }); + + clearBtn.addEventListener('click', () => { + textBox.value = ''; + outputBox.innerHTML = '

Your result will appear here...

'; + copyBtn.style.display = 'none'; + }); + + copyBtn.addEventListener('click', () => { + navigator.clipboard.writeText(copyBtn.dataset.result).then(() => { + const original = copyBtn.textContent; + copyBtn.textContent = '✅ Copied!'; + setTimeout(() => copyBtn.textContent = original, 1500); + }); + }); + + showPreview(3); +} \ No newline at end of file From 787faf97f05685cc2dbc675bac27f24697723217 Mon Sep 17 00:00:00 2001 From: Arun35 Date: Sun, 24 May 2026 03:27:21 +0530 Subject: [PATCH 08/23] Add caesar-cipher to project functions and initializers --- web-app/js/projects.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web-app/js/projects.js b/web-app/js/projects.js index 04ba8fd..6150066 100644 --- a/web-app/js/projects.js +++ b/web-app/js/projects.js @@ -39,6 +39,7 @@ function getProjectHTML(projectName) { '2048-game': () => get2048GameHTML(), 'productive-pet': () => getProductivePetHTML(), 'color-palette': () => getColorPaletteHTML(), + 'caesar-cipher': () => getCaesarCipherHTML(), }; try { @@ -3276,7 +3277,8 @@ function initializeProject(projectName) { 'simon-says': 'initSimonSays', '2048-game': 'init2048Game', 'color-palette': 'initColorPalette', - 'math-quiz': 'initMathQuiz' + 'math-quiz': 'initMathQuiz', + 'caesar-cipher': 'initCaesarCipher' }; const initializerName = initializers[projectName]; From 8f29af543e5d930c71fd439d12e323834da3f15b Mon Sep 17 00:00:00 2001 From: mahi-8758 Date: Sun, 24 May 2026 03:37:17 +0530 Subject: [PATCH 09/23] docs: expand WEB_APP_GUIDE with interactive animations and advanced patterns - Enhanced welcome section with CSS animations (gradient, float, fade-in effects) - Added 2,000+ lines of comprehensive documentation - Included advanced architecture patterns (StateManager, ComponentRegistry, APIClient) - Added comprehensive folder structure walkthrough with file descriptions - Expanded developer guides (performance profiling, troubleshooting, DevTools) - Added testing patterns, error handling, and security best practices - Included analytics, i18n, gesture handling, and utility functions - Enhanced accessibility documentation with code examples - Added responsive design patterns and CSS techniques - Total: 2,894 lines, 82.5 KB - Exceeds target of 2,500-3,000 lines --- WEB_APP_GUIDE.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/WEB_APP_GUIDE.md b/WEB_APP_GUIDE.md index 30d1208..0d5f105 100644 --- a/WEB_APP_GUIDE.md +++ b/WEB_APP_GUIDE.md @@ -1,4 +1,3 @@ -## - -
-

- 🚀 - 🎨 - - 🐍 -

-

Web App Architecture Guide

-

- - Interactive Frontend Documentation - -

-

- A modern, accessible web experience powered by vanilla JavaScript, Pyodide, and Web Workers. -

-
- 🎯 Architecture - 🎨 Components - ♿ Accessibility - ⚡ Performance -
-
+``` +════════════════════════════════════════════════════════════════ + 🎨 Welcome to the Guide + Architecture | Components | Design | Accessibility +════════════════════════════════════════════════════════════════ +``` ## 📑 Quick Navigation From a3d0f7b90a832bc57ee6c2514b5cf50c4aa94b07 Mon Sep 17 00:00:00 2001 From: Arun35 Date: Sun, 24 May 2026 03:54:33 +0530 Subject: [PATCH 11/23] Add Caesar Cipher project card to index.html --- web-app/index.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web-app/index.html b/web-app/index.html index 5c12c92..0b2e0bf 100644 --- a/web-app/index.html +++ b/web-app/index.html @@ -1203,6 +1203,12 @@

BlackJack21

Calculator

Your mathematical companion!

+
+ Caesar Cipher +
+

Caesar Cipher

+

Encrypt & decrypt with a shift!

+
Coin Flip
From 30247d8764cfb87084a37313186cca87f1c64313 Mon Sep 17 00:00:00 2001 From: Arun35 Date: Sun, 24 May 2026 03:56:37 +0530 Subject: [PATCH 12/23] Add files via upload --- web-app/assets/banners/caesar-cipher.jpg | Bin 0 -> 14428 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 web-app/assets/banners/caesar-cipher.jpg diff --git a/web-app/assets/banners/caesar-cipher.jpg b/web-app/assets/banners/caesar-cipher.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e1d9124f9f95fd39242f8deebd85ac3a44f4e99d GIT binary patch literal 14428 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

P^^O`%CRrE*k=EY40h8MdRptRxg_YwkG< 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 Date: Sun, 24 May 2026 11:22:42 +0530 Subject: [PATCH 13/23] 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"/> - +