From 564a06ddfaca3065a43c954044bce6333273911e Mon Sep 17 00:00:00 2001 From: Maik Date: Thu, 11 Jun 2026 20:10:52 +0200 Subject: [PATCH] web deployment changes --- .dockerignore | 9 +++ Dockerfile | 12 +++ api.py | 115 ++++++++++++++++++++++++++++ docker-compose.yml | 7 ++ requirements-web.txt | 2 + static/index.html | 177 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 322 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 api.py create mode 100644 docker-compose.yml create mode 100644 requirements-web.txt create mode 100644 static/index.html diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cdd5cfd --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.venv +__pycache__ +.git +.gitignore +.venv/ +**/__pycache__ +*.pyc +README.md +web-deployment-plan.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..73fce6d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements-web.txt . +RUN pip install --no-cache-dir -r requirements-web.txt + +COPY . . + +EXPOSE 8000 + +CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/api.py b/api.py new file mode 100644 index 0000000..27c15c2 --- /dev/null +++ b/api.py @@ -0,0 +1,115 @@ +import uuid +from typing import Optional + +from fastapi import FastAPI, Cookie, HTTPException +from fastapi.responses import FileResponse, JSONResponse +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel + +from blackjack_game import BlackjackGame +from game_logic import can_split, get_correct_decision, get_split_decision + +app = FastAPI(title="Blackjack Theory Trainer") + +sessions: dict[str, BlackjackGame] = {} + + +def get_or_create_session(session_id: Optional[str] = None) -> tuple[str, BlackjackGame, bool]: + if session_id and session_id in sessions: + return session_id, sessions[session_id], False + new_id = str(uuid.uuid4()) + game = BlackjackGame() + sessions[new_id] = game + return new_id, game, True + + +class DecisionRequest(BaseModel): + action: str + + +@app.post("/api/start-round") +async def start_round(session_id: Optional[str] = Cookie(None)): + sid, game, is_new = get_or_create_session(session_id) + game.start_new_round() + data = { + "dealer_card": game.dealer_card, + "player_hand": game.player_hand, + "player_total": game.player_total, + "has_blackjack": game.has_blackjack, + "can_split": can_split(game.player_hand), + "current_streak": game.stats["current_streak"], + } + response = JSONResponse(content=data) + if is_new: + response.set_cookie(key="session_id", value=sid, path="/") + return response + + +@app.post("/api/check-decision") +async def check_decision(req: DecisionRequest, session_id: Optional[str] = Cookie(None)): + if not session_id or session_id not in sessions: + raise HTTPException(status_code=400, detail="No active session") + game = sessions[session_id] + + scenario_type = "hard_totals" + if can_split(game.player_hand): + scenario_type = "pairs" + elif "A" in [c.split("_")[-1] for c in game.player_hand]: + scenario_type = "soft_totals" + + if can_split(game.player_hand): + decision, feedback = get_split_decision(game.player_hand, game.dealer_card) + correct = req.action == decision + else: + decision, feedback = get_correct_decision( + game.player_hand, game.player_total, game.dealer_card + ) + correct = req.action == decision + + had_streak = game.stats["current_streak"] + game.update_stats(correct, scenario_type) + stats = game.get_stats() + + streak_ended = None + if not correct and had_streak >= 3: + streak_ended = f"Streak ended at {had_streak}!" + + return { + "correct": correct, + "decision": decision, + "feedback": feedback, + "scenario_type": scenario_type, + "stats": stats, + "streak_ended": streak_ended, + } + + +@app.get("/api/stats") +async def get_stats(session_id: Optional[str] = Cookie(None)): + if not session_id or session_id not in sessions: + raise HTTPException(status_code=400, detail="No active session") + return sessions[session_id].get_stats() + + +@app.get("/api/hint") +async def get_hint(session_id: Optional[str] = Cookie(None)): + if not session_id or session_id not in sessions: + raise HTTPException(status_code=400, detail="No active session") + game = sessions[session_id] + + if can_split(game.player_hand): + decision, feedback = get_split_decision(game.player_hand, game.dealer_card) + else: + decision, feedback = get_correct_decision( + game.player_hand, game.player_total, game.dealer_card + ) + + return {"decision": decision, "feedback": feedback} + + +@app.get("/") +async def read_index(): + return FileResponse("static/index.html") + + +app.mount("/images", StaticFiles(directory="images"), name="images") diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bcc61e3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,7 @@ +services: + blackjack: + build: . + container_name: blackjack-theory + restart: unless-stopped + ports: + - "127.0.0.1:8000:8000" diff --git a/requirements-web.txt b/requirements-web.txt new file mode 100644 index 0000000..50b0ab1 --- /dev/null +++ b/requirements-web.txt @@ -0,0 +1,2 @@ +fastapi>=0.115.0 +uvicorn[standard]>=0.34.0 diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..6f6ee95 --- /dev/null +++ b/static/index.html @@ -0,0 +1,177 @@ + + + + + +Blackjack Theory Trainer + + + +
+ +
+
+ +
+
+
+ +
+
+
+ + + + +
+ +
+ +
+
+
+ + + + + -- 2.47.3