From 2e65f0011f54b0808772fa9f731890f9d4edc747 Mon Sep 17 00:00:00 2001 From: tototomate123 Date: Tue, 24 Feb 2026 20:12:13 +0100 Subject: [PATCH 1/4] add WS types and basic runtime validation --- app/sockets/types.py | 137 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 app/sockets/types.py diff --git a/app/sockets/types.py b/app/sockets/types.py new file mode 100644 index 0000000..a1f2d49 --- /dev/null +++ b/app/sockets/types.py @@ -0,0 +1,137 @@ +from typing import Any, Literal, NotRequired, Optional, TypedDict, get_args, get_origin + +class CreateCodeGame(TypedDict): # Client + play_as: Literal["b", "w", "r"] + time_mode: str + +class JoinCodeGame(TypedDict): # Client + code: str + +class CodeGameCreated(TypedDict): # Server + code: str + +class JoinCodeGameSuccess(TypedDict): # Server + p1_name: str + play_as: NotRequired[Literal["b", "w", "r"]] + +class JoinCodeGameFailure(TypedDict): # Server + reason: str + +class P2Connected(TypedDict): # Server + p2_name: str + ready: bool + +class UserReady(TypedDict): # Client or Server + ready: bool + +class GameStart(TypedDict): # Server + play_as: Literal["b", "w", "r"] + time_left_ms: int + +class Move_Request(TypedDict): # Client + from_square: str + to_square: str + promotion: NotRequired[str] + +class Move_Accepted(TypedDict): # Server + from_square: str + to_square: str + promotion: NotRequired[str] + time_left_ms: int + +class Move_Refused(TypedDict): # Server + reason: str + +class RequestResign(TypedDict): # Client + accepted: NotRequired[bool] # missing = offer, true = accept, false = refuse + +class RequestDraw(TypedDict): # Client + accepted: NotRequired[bool] # missing = offer, true = accept, false = refuse + +class GameEnd(TypedDict): # Server + result: Literal["win", "loss", "draw"] + reason: str + + +#todo: implement later +class _RematchRequest(TypedDict): # Client + accepted: NotRequired[bool] # missing = offer, true = accept, false = refuse + + +ClientEventSchema = { + "create_code_game": CreateCodeGame, + "join_code_game": JoinCodeGame, + "user_ready": UserReady, + "move_request": Move_Request, + "request_resign": RequestResign, + "request_draw": RequestDraw, +} + + +def _matches_annotation(value: Any, annotation: Any) -> bool: + """Minimal runtime checker for the annotations used in WS payloads.""" + origin = get_origin(annotation) + args = get_args(annotation) + + if annotation is Any: + return True + if origin is None: + return isinstance(value, annotation) + if origin is Literal: + return value in args + if origin is Optional: + inner_type = args[0] + return value is None or _matches_annotation(value, inner_type) + if origin is list: + return isinstance(value, list) and all(_matches_annotation(v, args[0]) for v in value) + if origin is dict: + key_type, val_type = args + return isinstance(value, dict) and all( + _matches_annotation(k, key_type) and _matches_annotation(v, val_type) + for k, v in value.items() + ) + if origin is tuple: + if not isinstance(value, tuple): + return False + if len(args) == 2 and args[1] is Ellipsis: + return all(_matches_annotation(v, args[0]) for v in value) + return len(value) == len(args) and all(_matches_annotation(v, t) for v, t in zip(value, args)) + if origin is set: + return isinstance(value, set) and all(_matches_annotation(v, args[0]) for v in value) + + # covers Union and "|" types + if args: + return any(_matches_annotation(value, arg) for arg in args) + + return False + + +def validate_typed_dict_payload(payload: Any, schema: type[Any]) -> tuple[bool, str | None]: + if not isinstance(payload, dict): + return False, "payload must be an object" + + required_keys = getattr(schema, "__required_keys__", set()) + optional_keys = getattr(schema, "__optional_keys__", set()) + allowed_keys = required_keys | optional_keys + + missing = required_keys - payload.keys() + if missing: + return False, f"missing required keys: {', '.join(sorted(missing))}" + + unexpected = payload.keys() - allowed_keys + if unexpected: + return False, f"unexpected keys: {', '.join(sorted(unexpected))}" + + annotations = schema.__annotations__ + for key, expected_type in annotations.items(): + if key in payload and not _matches_annotation(payload[key], expected_type): + return False, f"invalid type for '{key}'" + + return True, None + + +def validate_client_event(event: str, payload: Any) -> tuple[bool, str | None]: + schema = ClientEventSchema.get(event) + if schema is None: + return False, f"unknown event '{event}'" + return validate_typed_dict_payload(payload, schema) -- 2.52.0 From 3f4ea57ee1340c774626cfdae1aaa611b796df4a Mon Sep 17 00:00:00 2001 From: tototomate123 Date: Wed, 25 Feb 2026 19:57:11 +0100 Subject: [PATCH 2/4] add WS client in frontend, start work on play website --- app/sockets/socket.py | 5 ----- app/static/js/main.js | 16 ++++++++++++++++ app/static/js/ws.js | 35 +++++++++++++++++++++++++++++++++++ app/static/js/ws_handlers.js | 11 +++++++++++ app/static/play.css | 0 app/templates/home.html | 2 ++ app/templates/play.html | 23 +++++++++++++++++++++++ run.py | 2 +- 8 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 app/static/js/main.js create mode 100644 app/static/js/ws.js create mode 100644 app/static/js/ws_handlers.js create mode 100644 app/static/play.css create mode 100644 app/templates/play.html diff --git a/app/sockets/socket.py b/app/sockets/socket.py index 0df3bfa..e69de29 100644 --- a/app/sockets/socket.py +++ b/app/sockets/socket.py @@ -1,5 +0,0 @@ -from app import sIO - -@sIO.on('auth') -def handle_auth(msg): - print(msg) \ No newline at end of file diff --git a/app/static/js/main.js b/app/static/js/main.js new file mode 100644 index 0000000..2e09dfd --- /dev/null +++ b/app/static/js/main.js @@ -0,0 +1,16 @@ +import { createWSClient } from "./ws.js"; +import { + handleP2Connected, + handleGameStarted, + handleCodeGameCreated, +} from "./ws_handlers.js"; + +function main() { + createWSClient({ + onGameStarted: handleGameStarted, + onP2Connected: handleP2Connected, + onGameCreated: handleCodeGameCreated, + }); +} + +main(); diff --git a/app/static/js/ws.js b/app/static/js/ws.js new file mode 100644 index 0000000..c809e9a --- /dev/null +++ b/app/static/js/ws.js @@ -0,0 +1,35 @@ +// using socketio for websocket communication +export function createWSClient(handlers = {}) { + const socket = io(); + + socket.on("connect", () => { + console.log("Connected to server"); + handlers.onConnect?.(); + }); + + socket.on("disconnect", () => { + console.log("Disconnected from server"); + handlers.onDisconnect?.(); + }); + + const serverEvents = { + code_game_created: handlers.onGameCreated, + code_game_joined: handlers.onGameJoined, + game_started: handlers.onGameStarted, + p2_connected: handlers.onP2Connected, + user_move: handlers.onUserMove, + move_accept: handlers.onMoveAccept, + move_reject: handlers.onMoveReject, + game_over: handlers.onGameOver, + //todo: draw, resign, rematch + }; + + let i = 0; + for (const [event, handler] of Object.entries(serverEvents)) { + if (handler) { + socket.on(event, handler); + i++; + } + } + console.log("registered " + i + " server event handlers"); +} diff --git a/app/static/js/ws_handlers.js b/app/static/js/ws_handlers.js new file mode 100644 index 0000000..4db7c48 --- /dev/null +++ b/app/static/js/ws_handlers.js @@ -0,0 +1,11 @@ +export function handleGameStarted(data) { + throw new Error("todo"); +} + +export function handleCodeGameCreated(data) { + throw new Error("todo"); +} + +export function handleP2Connected(data) { + throw new Error("todo"); +} diff --git a/app/static/play.css b/app/static/play.css new file mode 100644 index 0000000..e69de29 diff --git a/app/templates/home.html b/app/templates/home.html index 5159827..cc31329 100644 --- a/app/templates/home.html +++ b/app/templates/home.html @@ -12,4 +12,6 @@

logged in

hi, {{ current_user.username }}!

+ Play Chess + diff --git a/app/templates/play.html b/app/templates/play.html new file mode 100644 index 0000000..c5377f1 --- /dev/null +++ b/app/templates/play.html @@ -0,0 +1,23 @@ + + + + + + Chess Arena + + + + + +
+ + diff --git a/run.py b/run.py index 4f3b0b6..5cdc59a 100644 --- a/run.py +++ b/run.py @@ -4,4 +4,4 @@ app = create_app() if __name__ == "__main__": print("Starting server...") - sIO.run(app, debug=True) \ No newline at end of file + sIO.run(app, debug=True, use_reloader=False) \ No newline at end of file -- 2.52.0 From d9c2a8cd2a740e236163458bc2468baa6c857bc0 Mon Sep 17 00:00:00 2001 From: tototomate123 Date: Wed, 25 Feb 2026 20:48:19 +0100 Subject: [PATCH 3/4] fix #1 --- app/routes/auth.py | 12 +++++++++--- app/routes/main.py | 12 ++++++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/app/routes/auth.py b/app/routes/auth.py index 5c02d60..bfb8f26 100644 --- a/app/routes/auth.py +++ b/app/routes/auth.py @@ -1,5 +1,5 @@ -from flask import Blueprint, render_template, request, redirect, url_for, session, flash -from flask_login import login_user +from flask import Blueprint, render_template, request, redirect, url_for, flash +from flask_login import login_user, current_user from app.db import get_db from werkzeug.security import generate_password_hash, check_password_hash @@ -9,6 +9,9 @@ auth_bp = Blueprint("auth", __name__) @auth_bp.route("/login", methods=["GET", "POST"]) def login(): + if current_user.is_authenticated: + return redirect(url_for("main.home")) + if request.method == "POST": username = request.form["username"] password = request.form["password"] @@ -32,6 +35,9 @@ def login(): @auth_bp.route("/register", methods=["GET", "POST"]) def register(): + if current_user.is_authenticated: + return redirect(url_for("main.home")) + if request.method == "POST": username = request.form.get("username") password = request.form.get("password") @@ -62,4 +68,4 @@ def register(): flash("Account created! Please log in.") return redirect(url_for("auth.login")) - return render_template("register.html") \ No newline at end of file + return render_template("register.html") diff --git a/app/routes/main.py b/app/routes/main.py index 53f503d..2675480 100644 --- a/app/routes/main.py +++ b/app/routes/main.py @@ -1,4 +1,4 @@ -from flask import Blueprint, render_template +from flask import Blueprint, render_template, redirect, url_for from flask_login import login_required, current_user main_bp = Blueprint("main", __name__) @@ -14,6 +14,8 @@ main_bp = Blueprint("main", __name__) @main_bp.route("/", methods=["GET", "POST"]) def index(): + if current_user.is_authenticated: + return redirect(url_for("main.home")) return render_template("index.html") @main_bp.route("/home", methods=["GET", "POST"]) @@ -26,4 +28,10 @@ def home(): @main_bp.route("/play", methods=["GET"]) @login_required def play(): - return render_template("play.html") \ No newline at end of file + return render_template("play.html") + + +@main_bp.route("/friends", methods=["GET"]) +@login_required +def friends(): + return render_template("friends.html") -- 2.52.0 From 7ee2e1ad086fbdb75414e038453502750d0d0cb0 Mon Sep 17 00:00:00 2001 From: tototomate123 Date: Wed, 25 Feb 2026 20:48:43 +0100 Subject: [PATCH 4/4] create basic homepage layout --- app/static/app.css | 219 +++++++++++++++++++++++++++++++++++++ app/static/play.css | 0 app/templates/friends.html | 48 ++++++++ app/templates/home.html | 33 +++++- app/templates/play.html | 55 ++++++++-- run.py | 2 +- 6 files changed, 340 insertions(+), 17 deletions(-) create mode 100644 app/static/app.css delete mode 100644 app/static/play.css create mode 100644 app/templates/friends.html diff --git a/app/static/app.css b/app/static/app.css new file mode 100644 index 0000000..5544f45 --- /dev/null +++ b/app/static/app.css @@ -0,0 +1,219 @@ +:root { + --bg: #f4f6f8; + --surface: #ffffff; + --surface-soft: #f8fafc; + --border: #dde2e8; + --text: #111827; + --muted: #5f6b7a; + --primary: #1f2937; + --primary-strong: #0f172a; + --success: #16a34a; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: "Manrope", "Segoe UI", sans-serif; + background: var(--bg); + color: var(--text); +} + +.site-shell { + min-height: 100vh; +} + +.topbar { + height: 64px; + border-bottom: 1px solid var(--border); + background: var(--surface); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 20px; + position: sticky; + top: 0; +} + +.brand { + color: var(--text); + text-decoration: none; + font-weight: 700; + font-size: 1.1rem; +} + +.topnav { + display: flex; + gap: 8px; +} + +.topnav a { + text-decoration: none; + color: var(--muted); + padding: 8px 12px; + border: 1px solid transparent; + border-radius: 4px; + font-weight: 600; +} + +.topnav a:hover { + color: var(--text); + background: var(--surface-soft); + border-color: var(--border); +} + +.topnav a.active { + color: var(--text); + background: var(--surface-soft); + border-color: var(--border); +} + +.profile-pill { + border: 1px solid var(--border); + background: var(--surface-soft); + padding: 7px 10px; + font-size: 0.9rem; + color: var(--muted); +} + +.page-wrap { + max-width: 1080px; + margin: 0 auto; + padding: 24px 20px 30px; +} + +h1 { + margin: 8px 0 10px; + font-size: clamp(1.6rem, 3vw, 2.25rem); + line-height: 1.15; +} + +.cta-row { + margin-top: 18px; + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.btn { + font: inherit; + border: 1px solid var(--border); + background: var(--surface); + color: var(--text); + padding: 9px 14px; + border-radius: 4px; + text-decoration: none; + cursor: pointer; +} + +.btn-primary { + background: var(--primary); + border-color: var(--primary); + color: #ffffff; +} + +.btn-primary:hover { + background: var(--primary-strong); + border-color: var(--primary-strong); +} + +.btn-secondary:hover { + background: var(--surface-soft); +} + +.queue-layout { + margin-top: 14px; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +.chip-row { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 10px; +} + +.chip { + border: 1px solid var(--border); + background: var(--surface-soft); + color: var(--text); + border-radius: 4px; + padding: 7px 11px; + font: inherit; + cursor: pointer; +} + +.chip.active, +.chip:hover { + border-color: #b7c0cb; + background: #eef2f6; +} + +.friend-list { + margin: 0; + padding: 0; + list-style: none; +} + +.friend-list li { + display: flex; + align-items: center; + gap: 8px; + padding: 7px 0; + border-top: 1px solid var(--border); + color: var(--muted); +} + +.friend-list li:first-child { + border-top: 0; +} + +.dot { + width: 8px; + height: 8px; + border-radius: 4px; + background: var(--success); +} + +@media (max-width: 900px) { + .panel-grid { + grid-template-columns: 1fr; + } + + .queue-layout { + grid-template-columns: 1fr; + } +} + +@media (max-width: 700px) { + .topbar { + height: auto; + flex-wrap: wrap; + gap: 8px; + padding: 10px 14px; + } + + .topnav { + width: 100%; + order: 3; + } + + .topnav a { + flex: 1; + text-align: center; + } + + .page-wrap { + padding: 16px 14px 24px; + } + + .hero, + .stack-head, + .panel { + padding: 14px; + } +} diff --git a/app/static/play.css b/app/static/play.css deleted file mode 100644 index e69de29..0000000 diff --git a/app/templates/friends.html b/app/templates/friends.html new file mode 100644 index 0000000..15d359b --- /dev/null +++ b/app/templates/friends.html @@ -0,0 +1,48 @@ + + + + + + Friends + + + + + + +
+
+ Chess + +
{{ current_user.username }}
+
+ +

Friends

+ +
+
+

Online now

+
    +
  • name1
  • +
  • friend2
  • +
  • bro has a lot of friends
  • +
+
+ +
+

Incoming invites

+

No pending invitations +

+
+
+ +
+ + diff --git a/app/templates/home.html b/app/templates/home.html index cc31329..4648daa 100644 --- a/app/templates/home.html +++ b/app/templates/home.html @@ -3,15 +3,38 @@ - Home Page + Home + + + -

logged in

-

hi, {{ current_user.username }}!

- Play Chess +
+
+ Chess + +
{{ current_user.username }}
+
+ +
+

Welcome back, {{current_user.username}}

+ +
+
diff --git a/app/templates/play.html b/app/templates/play.html index c5377f1..3e608b0 100644 --- a/app/templates/play.html +++ b/app/templates/play.html @@ -3,21 +3,54 @@ - Chess Arena + Play + + - - + -
+
+
+ Chess + +
{{ current_user.username }}
+
+ + +
+
+
+

Game settings

+

Choose a time setup

+
+ + + + +
+

You play as

+
+ + + +
+
+ + +
+
+
+
+
diff --git a/run.py b/run.py index 5cdc59a..7c6bf2e 100644 --- a/run.py +++ b/run.py @@ -4,4 +4,4 @@ app = create_app() if __name__ == "__main__": print("Starting server...") - sIO.run(app, debug=True, use_reloader=False) \ No newline at end of file + sIO.run(app, debug=True, use_reloader=True) -- 2.52.0