From db8642080cb383f511b76d002b532e6fbadacee6 Mon Sep 17 00:00:00 2001 From: Midas Wollinger Date: Wed, 29 Jul 2026 12:37:38 +0200 Subject: [PATCH] first commit --- app.py | 436 +++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + static/css/style.css | 249 ++++++++++++++++++++++++ static/js/game.js | 232 +++++++++++++++++++++++ templates/index.html | 55 ++++++ 5 files changed, 973 insertions(+) create mode 100644 app.py create mode 100644 requirements.txt create mode 100644 static/css/style.css create mode 100644 static/js/game.js create mode 100644 templates/index.html diff --git a/app.py b/app.py new file mode 100644 index 0000000..adf31c9 --- /dev/null +++ b/app.py @@ -0,0 +1,436 @@ +import json +import random +import sqlite3 +import uuid +from pathlib import Path + +from flask import Flask, g, jsonify, render_template, request, session + +BASE_DIR = Path(__file__).resolve().parent +DB_PATH = BASE_DIR / "schiffe_versenken.db" +BOARD_SIZE = 10 + +FLEET = [ + {"id": "koenig", "name": "Zwergenkönig", "size": 5, "hat": "#c0392b"}, + {"id": "hauptmann", "name": "Zwergenhauptmann", "size": 4, "hat": "#2980b9"}, + {"id": "krieger1", "name": "Zwergenkrieger I", "size": 3, "hat": "#27ae60"}, + {"id": "krieger2", "name": "Zwergenkrieger II", "size": 3, "hat": "#8e44ad"}, + {"id": "kundschafter", "name": "Zwergenkundschafter", "size": 2, "hat": "#d35400"}, +] +FLEET_BY_ID = {s["id"]: s for s in FLEET} + +app = Flask(__name__) +app.secret_key = "zwerge-schiffe-versenken-" + uuid.uuid4().hex + + +def get_db(): + if "db" not in g: + g.db = sqlite3.connect(DB_PATH) + g.db.row_factory = sqlite3.Row + return g.db + + +@app.teardown_appcontext +def close_db(exception=None): + db = g.pop("db", None) + if db is not None: + db.close() + + +def init_db(): + conn = sqlite3.connect(DB_PATH) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS games ( + id TEXT PRIMARY KEY, + player_board TEXT NOT NULL, + computer_board TEXT NOT NULL, + player_shots TEXT NOT NULL, + computer_shots TEXT NOT NULL, + computer_ai_state TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'placing', + message TEXT DEFAULT '', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """ + ) + conn.commit() + conn.close() + + +def empty_grid(): + return [[None for _ in range(BOARD_SIZE)] for _ in range(BOARD_SIZE)] + + +def empty_shots(): + return [[None for _ in range(BOARD_SIZE)] for _ in range(BOARD_SIZE)] + + +def can_place(grid, row, col, size, orientation): + cells = ship_cells(row, col, size, orientation) + for r, c in cells: + if not (0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE): + return False + if grid[r][c] is not None: + return False + return True + + +def ship_cells(row, col, size, orientation): + if orientation == "h": + return [(row, col + i) for i in range(size)] + return [(row + i, col) for i in range(size)] + + +def place_ship(grid, ship_id, row, col, size, orientation): + for r, c in ship_cells(row, col, size, orientation): + grid[r][c] = ship_id + + +def random_place_all_ships(): + grid = empty_grid() + for ship in FLEET: + placed = False + attempts = 0 + while not placed and attempts < 500: + attempts += 1 + orientation = random.choice(["h", "v"]) + row = random.randint(0, BOARD_SIZE - 1) + col = random.randint(0, BOARD_SIZE - 1) + if can_place(grid, row, col, ship["size"], orientation): + place_ship(grid, ship["id"], row, col, ship["size"], orientation) + placed = True + if not placed: + return random_place_all_ships() + return grid + + +def new_game_row(): + game_id = str(uuid.uuid4()) + computer_board = random_place_all_ships() + row = { + "id": game_id, + "player_board": empty_grid(), + "computer_board": computer_board, + "player_shots": empty_shots(), + "computer_shots": empty_shots(), + "computer_ai_state": {"mode": "random", "targets": [], "tried": []}, + "status": "placing", + "message": "Platziere deine Zwerge auf dem Spielfeld.", + } + db = get_db() + db.execute( + "INSERT INTO games (id, player_board, computer_board, player_shots, computer_shots, " + "computer_ai_state, status, message) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + ( + row["id"], + json.dumps(row["player_board"]), + json.dumps(row["computer_board"]), + json.dumps(row["player_shots"]), + json.dumps(row["computer_shots"]), + json.dumps(row["computer_ai_state"]), + row["status"], + row["message"], + ), + ) + db.commit() + session["game_id"] = game_id + return load_game(game_id) + + +def load_game(game_id): + db = get_db() + r = db.execute("SELECT * FROM games WHERE id = ?", (game_id,)).fetchone() + if r is None: + return None + return { + "id": r["id"], + "player_board": json.loads(r["player_board"]), + "computer_board": json.loads(r["computer_board"]), + "player_shots": json.loads(r["player_shots"]), + "computer_shots": json.loads(r["computer_shots"]), + "computer_ai_state": json.loads(r["computer_ai_state"]), + "status": r["status"], + "message": r["message"], + } + + +def save_game(game): + db = get_db() + db.execute( + "UPDATE games SET player_board=?, computer_board=?, player_shots=?, computer_shots=?, " + "computer_ai_state=?, status=?, message=? WHERE id=?", + ( + json.dumps(game["player_board"]), + json.dumps(game["computer_board"]), + json.dumps(game["player_shots"]), + json.dumps(game["computer_shots"]), + json.dumps(game["computer_ai_state"]), + game["status"], + game["message"], + game["id"], + ), + ) + db.commit() + + +def get_current_game(): + game_id = session.get("game_id") + if not game_id: + return None + return load_game(game_id) + + +def ship_is_sunk(board, shots, ship_id): + for r in range(BOARD_SIZE): + for c in range(BOARD_SIZE): + if board[r][c] == ship_id and shots[r][c] != "hit": + return False + return True + + +def all_ships_sunk(board, shots): + ship_ids = {cell for row in board for cell in row if cell is not None} + return all(ship_is_sunk(board, shots, sid) for sid in ship_ids) + + +def fleet_status(board, shots): + result = [] + for ship in FLEET: + result.append( + { + "id": ship["id"], + "name": ship["name"], + "size": ship["size"], + "hat": ship["hat"], + "sunk": ship_is_sunk(board, shots, ship["id"]), + } + ) + return result + + +def neighbors(r, c): + for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)): + nr, nc = r + dr, c + dc + if 0 <= nr < BOARD_SIZE and 0 <= nc < BOARD_SIZE: + yield nr, nc + + +def computer_take_shot(game): + board = game["player_board"] + shots = game["computer_shots"] + ai = game["computer_ai_state"] + + target = None + if ai["mode"] == "target" and ai["targets"]: + while ai["targets"]: + candidate = tuple(ai["targets"].pop(0)) + if shots[candidate[0]][candidate[1]] is None: + target = candidate + break + if not ai["targets"] and target is None: + ai["mode"] = "random" + + if target is None: + ai["mode"] = "random" + choices = [ + (r, c) + for r in range(BOARD_SIZE) + for c in range(BOARD_SIZE) + if shots[r][c] is None + ] + target = random.choice(choices) + + r, c = target + hit = board[r][c] is not None + shots[r][c] = "hit" if hit else "miss" + + sunk_ship = None + if hit: + ship_id = board[r][c] + if ship_is_sunk(board, shots, ship_id): + sunk_ship = FLEET_BY_ID[ship_id]["name"] + ai["mode"] = "random" + ai["targets"] = [] + else: + ai["mode"] = "target" + for nr, nc in neighbors(r, c): + if shots[nr][nc] is None and [nr, nc] not in ai["targets"]: + ai["targets"].append([nr, nc]) + game["computer_ai_state"] = ai + return {"row": r, "col": c, "hit": hit, "sunk_ship": sunk_ship} + + +@app.route("/") +def index(): + game = get_current_game() + if game is None: + game = new_game_row() + return render_template("index.html", fleet=FLEET, board_size=BOARD_SIZE) + + +@app.route("/api/state") +def api_state(): + game = get_current_game() + if game is None: + game = new_game_row() + return jsonify(build_state_payload(game)) + + +def build_state_payload(game): + return { + "status": game["status"], + "message": game["message"], + "player_board": game["player_board"], + "player_shots_received": game["computer_shots"], + "player_shots_made": game["player_shots"], + "player_fleet": fleet_status(game["player_board"], game["computer_shots"]), + "computer_fleet": fleet_status(game["computer_board"], game["player_shots"]), + "fleet_def": FLEET, + "board_size": BOARD_SIZE, + } + + +@app.route("/api/new_game", methods=["POST"]) +def api_new_game(): + game = new_game_row() + return jsonify(build_state_payload(game)) + + +@app.route("/api/place_ship", methods=["POST"]) +def api_place_ship(): + game = get_current_game() + if game is None or game["status"] != "placing": + return jsonify({"error": "Kein aktives Platzierungsspiel."}), 400 + + data = request.get_json(force=True) + ship_id = data.get("ship_id") + row = data.get("row") + col = data.get("col") + orientation = data.get("orientation", "h") + + ship = FLEET_BY_ID.get(ship_id) + if ship is None: + return jsonify({"error": "Unbekannter Zwerg."}), 400 + + board = game["player_board"] + already_placed = any(ship_id in row_ for row_ in board) + if already_placed: + return jsonify({"error": "Dieser Zwerg steht schon auf dem Feld."}), 400 + + if not can_place(board, row, col, ship["size"], orientation): + return jsonify({"error": "Hier passt der Zwerg nicht hin."}), 400 + + place_ship(board, ship_id, row, col, ship["size"], orientation) + game["player_board"] = board + + placed_ids = {cell for row_ in board for cell in row_ if cell is not None} + if len(placed_ids) == len(FLEET): + game["message"] = "Alle Zwerge stehen bereit! Klicke auf 'Kampf beginnen'." + else: + game["message"] = f"{ship['name']} platziert." + + save_game(game) + return jsonify(build_state_payload(game)) + + +@app.route("/api/random_place", methods=["POST"]) +def api_random_place(): + game = get_current_game() + if game is None or game["status"] != "placing": + return jsonify({"error": "Kein aktives Platzierungsspiel."}), 400 + game["player_board"] = random_place_all_ships() + game["message"] = "Alle Zwerge wurden zufällig aufgestellt." + save_game(game) + return jsonify(build_state_payload(game)) + + +@app.route("/api/reset_placement", methods=["POST"]) +def api_reset_placement(): + game = get_current_game() + if game is None or game["status"] != "placing": + return jsonify({"error": "Kein aktives Platzierungsspiel."}), 400 + game["player_board"] = empty_grid() + game["message"] = "Aufstellung zurückgesetzt." + save_game(game) + return jsonify(build_state_payload(game)) + + +@app.route("/api/start", methods=["POST"]) +def api_start(): + game = get_current_game() + if game is None or game["status"] != "placing": + return jsonify({"error": "Kein aktives Platzierungsspiel."}), 400 + + placed_ids = {cell for row_ in game["player_board"] for cell in row_ if cell is not None} + if len(placed_ids) != len(FLEET): + return jsonify({"error": "Bitte alle Zwerge platzieren, bevor der Kampf beginnt."}), 400 + + game["status"] = "playing" + game["message"] = "Der Kampf hat begonnen! Du bist am Zug – ziehe in die feindlichen Berge." + save_game(game) + return jsonify(build_state_payload(game)) + + +@app.route("/api/fire", methods=["POST"]) +def api_fire(): + game = get_current_game() + if game is None or game["status"] != "playing": + return jsonify({"error": "Das Spiel läuft gerade nicht."}), 400 + + data = request.get_json(force=True) + row, col = data.get("row"), data.get("col") + if row is None or col is None or not (0 <= row < BOARD_SIZE and 0 <= col < BOARD_SIZE): + return jsonify({"error": "Ungültiges Feld."}), 400 + + if game["player_shots"][row][col] is not None: + return jsonify({"error": "Dorthin hast du schon geschossen."}), 400 + + board = game["computer_board"] + shots = game["player_shots"] + hit = board[row][col] is not None + shots[row][col] = "hit" if hit else "miss" + game["player_shots"] = shots + + player_result = {"row": row, "col": col, "hit": hit, "sunk_ship": None} + if hit: + ship_id = board[row][col] + if ship_is_sunk(board, shots, ship_id): + player_result["sunk_ship"] = FLEET_BY_ID[ship_id]["name"] + + computer_result = None + if all_ships_sunk(game["computer_board"], game["player_shots"]): + game["status"] = "player_win" + game["message"] = "Sieg! Du hast alle Zwergensiedlungen des Computers zerstört." + else: + computer_result = computer_take_shot(game) + if all_ships_sunk(game["player_board"], game["computer_shots"]): + game["status"] = "computer_win" + game["message"] = "Niederlage! Der Computer hat all deine Zwerge besiegt." + else: + msgs = [] + msgs.append( + f"Treffer bei {player_result['row']},{player_result['col']}!" + if player_result["hit"] + else "Daneben." + ) + if player_result["sunk_ship"]: + msgs.append(f"{player_result['sunk_ship']} wurde versenkt!") + msgs.append( + "Der Computer trifft dich!" if computer_result["hit"] else "Der Computer schießt daneben." + ) + if computer_result["sunk_ship"]: + msgs.append(f"Dein {computer_result['sunk_ship']} wurde versenkt!") + game["message"] = " ".join(msgs) + + save_game(game) + payload = build_state_payload(game) + payload["player_shot"] = player_result + payload["computer_shot"] = computer_result + return jsonify(payload) + + +init_db() + +if __name__ == "__main__": + app.run(debug=True, port=5000) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c37b670 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +Flask>=3.0 diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..a186507 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,249 @@ +:root { + --bg: #1b2735; + --panel: #24344a; + --water: #2f6f9e; + --water-dark: #24597f; + --grid-line: #16232f; + --fog: #3a536b; + --text: #eef3f8; + --accent: #e8b23a; +} + +* { box-sizing: border-box; } + +body { + margin: 0; + font-family: "Segoe UI", Verdana, sans-serif; + background: linear-gradient(180deg, var(--bg), #0e1620); + color: var(--text); + min-height: 100vh; +} + +.app { + max-width: 1100px; + margin: 0 auto; + padding: 24px 16px 60px; +} + +header { text-align: center; margin-bottom: 8px; } +header h1 { margin: 0 0 4px; font-size: 2.1rem; } +.subtitle { margin: 0; opacity: 0.75; } + +.message { + text-align: center; + min-height: 1.4em; + margin: 14px auto 20px; + padding: 10px 16px; + background: var(--panel); + border-radius: 8px; + max-width: 640px; + font-weight: 600; +} + +.screen.hidden { display: none; } + +h2, h3 { text-align: center; } + +.placement-controls { + display: flex; + flex-wrap: wrap; + gap: 10px; + justify-content: center; + align-items: center; + margin-bottom: 18px; +} + +button { + background: var(--accent); + color: #2a1c00; + border: none; + border-radius: 6px; + padding: 10px 16px; + font-size: 0.95rem; + font-weight: 700; + cursor: pointer; + transition: transform 0.08s ease, opacity 0.15s ease; +} +button:hover:not(:disabled) { transform: translateY(-1px); } +button:disabled { opacity: 0.4; cursor: not-allowed; } + +.fleet-picker { + display: flex; + flex-wrap: wrap; + gap: 8px; + justify-content: center; +} + +.fleet-picker button { + background: var(--panel); + color: var(--text); + border: 2px solid transparent; + display: flex; + align-items: center; + gap: 6px; +} +.fleet-picker button.selected { border-color: var(--accent); } +.fleet-picker button.placed { opacity: 0.35; cursor: default; } + +.boards { + display: flex; + flex-wrap: wrap; + gap: 32px; + justify-content: center; +} + +.board-wrap { margin-bottom: 20px; } + +.board { + display: grid; + grid-template-columns: repeat(10, 34px); + grid-template-rows: repeat(10, 34px); + gap: 2px; + background: var(--grid-line); + padding: 4px; + border-radius: 8px; + width: max-content; + margin: 0 auto; +} + +.cell { + width: 34px; + height: 34px; + background: var(--water); + position: relative; + border-radius: 3px; + cursor: default; + display: flex; + align-items: center; + justify-content: center; + overflow: visible; +} + +.cell.own { background: var(--water-dark); } + +#enemy-board .cell { + cursor: crosshair; + background: var(--fog); +} +#enemy-board .cell:hover:not(.hit):not(.miss) { + background: #4a6a87; +} + +.cell.preview-ok { outline: 2px solid #2ecc71; } +.cell.preview-bad { outline: 2px solid #e74c3c; } + +.cell.miss::after { + content: ""; + width: 8px; + height: 8px; + border-radius: 50%; + background: rgba(255,255,255,0.6); +} + +.cell.hit { + background: #7a2323 !important; +} +.cell.hit::after { + content: "💥"; + font-size: 18px; +} + +/* --- Dwarf ship sprite, built purely from CSS shapes --- */ +.dwarf { + position: relative; + width: 24px; + height: 26px; +} +.dwarf .hat { + position: absolute; + top: -2px; + left: 2px; + width: 0; + height: 0; + border-left: 10px solid transparent; + border-right: 10px solid transparent; + border-bottom: 12px solid var(--hat, #c0392b); + filter: drop-shadow(0 1px 0 rgba(0,0,0,0.4)); +} +.dwarf .hat::after { + content: ""; + position: absolute; + top: 11px; + left: -6px; + width: 12px; + height: 4px; + background: var(--hat, #c0392b); + border-radius: 2px; +} +.dwarf .head { + position: absolute; + top: 9px; + left: 4px; + width: 16px; + height: 14px; + background: #f0c090; + border-radius: 50% 50% 45% 45%; +} +.dwarf .beard { + position: absolute; + top: 15px; + left: 3px; + width: 18px; + height: 12px; + background: #d9d9d9; + border-radius: 0 0 9px 9px; +} +.dwarf .eyes { + position: absolute; + top: 13px; + left: 7px; + width: 10px; + height: 2px; + background: transparent; + box-shadow: 0 0 0 1px #2a1c00, 8px 0 0 -1px #2a1c00; +} + +.cell.hit .dwarf { filter: grayscale(1) brightness(0.6); transform: rotate(90deg); } +.cell.hit .dwarf .eyes { box-shadow: none; } +.cell.hit .dwarf::before { + content: "✕"; + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-weight: bold; + font-size: 14px; + transform: rotate(-90deg); +} + +.fleet-status { + display: flex; + flex-wrap: wrap; + gap: 6px; + justify-content: center; + margin-top: 10px; + max-width: 340px; +} +.fleet-status .badge { + font-size: 0.78rem; + padding: 4px 8px; + border-radius: 12px; + background: var(--panel); +} +.fleet-status .badge.sunk { + text-decoration: line-through; + opacity: 0.5; + background: #5a1a1a; +} + +#new-game-btn { display: block; margin: 24px auto 0; } + +@media (max-width: 560px) { + .board { + grid-template-columns: repeat(10, 28px); + grid-template-rows: repeat(10, 28px); + } + .cell { width: 28px; height: 28px; } +} diff --git a/static/js/game.js b/static/js/game.js new file mode 100644 index 0000000..fe241c0 --- /dev/null +++ b/static/js/game.js @@ -0,0 +1,232 @@ +const BOARD_SIZE = 10; + +const state = { + fleetDef: [], + selectedShip: null, + orientation: "h", + status: "placing", +}; + +const el = (id) => document.getElementById(id); + +async function api(path, options) { + const res = await fetch(path, { + method: options?.method || "GET", + headers: { "Content-Type": "application/json" }, + body: options?.body ? JSON.stringify(options.body) : undefined, + }); + const data = await res.json(); + if (!res.ok) { + setMessage(data.error || "Fehler", true); + throw new Error(data.error || "Request failed"); + } + return data; +} + +function setMessage(text, isError = false) { + const m = el("message"); + m.textContent = text; + m.style.color = isError ? "#ff8b8b" : ""; +} + +function dwarfSprite(hat) { + const wrap = document.createElement("div"); + wrap.className = "dwarf"; + wrap.style.setProperty("--hat", hat); + wrap.innerHTML = '
'; + return wrap; +} + +function buildBoard(container, size, onClick) { + container.innerHTML = ""; + const cells = []; + for (let r = 0; r < size; r++) { + const rowCells = []; + for (let c = 0; c < size; c++) { + const cell = document.createElement("div"); + cell.className = "cell"; + cell.dataset.row = r; + cell.dataset.col = c; + if (onClick) cell.addEventListener("click", () => onClick(r, c, cell)); + container.appendChild(cell); + rowCells.push(cell); + } + cells.push(rowCells); + } + return cells; +} + +// ----- Placement screen ----- +let placementCells = null; + +function renderFleetPicker(data) { + const picker = el("fleet-picker"); + picker.innerHTML = ""; + const placedIds = new Set(); + for (const row of data.player_board) { + for (const cell of row) if (cell) placedIds.add(cell); + } + for (const ship of data.fleet_def) { + const btn = document.createElement("button"); + btn.type = "button"; + const isPlaced = placedIds.has(ship.id); + btn.textContent = `${ship.name} (${ship.size})`; + btn.style.setProperty("--hat", ship.hat); + btn.classList.toggle("placed", isPlaced); + btn.classList.toggle("selected", state.selectedShip === ship.id); + btn.disabled = isPlaced; + btn.addEventListener("click", () => { + state.selectedShip = ship.id; + renderFleetPicker(data); + }); + picker.appendChild(btn); + } +} + +function renderPlacementBoard(data) { + if (!placementCells) { + placementCells = buildBoard(el("placement-board"), BOARD_SIZE, handlePlacementClick); + } + for (let r = 0; r < BOARD_SIZE; r++) { + for (let c = 0; c < BOARD_SIZE; c++) { + const cell = placementCells[r][c]; + cell.className = "cell own"; + cell.innerHTML = ""; + const shipId = data.player_board[r][c]; + if (shipId) { + const ship = data.fleet_def.find((s) => s.id === shipId); + cell.appendChild(dwarfSprite(ship.hat)); + } + } + } + const placedCount = new Set(data.player_board.flat().filter(Boolean)).size; + el("start-btn").disabled = placedCount !== data.fleet_def.length; +} + +async function handlePlacementClick(r, c) { + if (!state.selectedShip) { + setMessage("Wähle zuerst einen Zwergenclan aus.", true); + return; + } + try { + const data = await api("/api/place_ship", { + method: "POST", + body: { ship_id: state.selectedShip, row: r, col: c, orientation: state.orientation }, + }); + applyState(data); + state.selectedShip = null; + } catch (e) { + /* message already shown */ + } +} + +// ----- Battle screen ----- +let ownCells = null; +let enemyCells = null; + +function renderBattle(data) { + if (!ownCells) ownCells = buildBoard(el("own-board"), BOARD_SIZE, null); + if (!enemyCells) enemyCells = buildBoard(el("enemy-board"), BOARD_SIZE, handleFireClick); + + for (let r = 0; r < BOARD_SIZE; r++) { + for (let c = 0; c < BOARD_SIZE; c++) { + const own = ownCells[r][c]; + own.className = "cell own"; + own.innerHTML = ""; + const shipId = data.player_board[r][c]; + const receivedShot = data.player_shots_received[r][c]; + if (shipId) own.appendChild(dwarfSprite(data.fleet_def.find((s) => s.id === shipId).hat)); + if (receivedShot) own.classList.add(receivedShot); + + const enemy = enemyCells[r][c]; + enemy.className = "cell"; + enemy.innerHTML = ""; + const madeShot = data.player_shots_made[r][c]; + if (madeShot) enemy.classList.add(madeShot); + } + } + + renderFleetStatus(el("own-fleet"), data.player_fleet); + renderFleetStatus(el("enemy-fleet"), data.computer_fleet); +} + +function renderFleetStatus(container, fleet) { + container.innerHTML = ""; + for (const ship of fleet) { + const badge = document.createElement("span"); + badge.className = "badge" + (ship.sunk ? " sunk" : ""); + badge.textContent = `${ship.name}`; + container.appendChild(badge); + } +} + +async function handleFireClick(r, c) { + if (state.status !== "playing") return; + try { + const data = await api("/api/fire", { method: "POST", body: { row: r, col: c } }); + applyState(data); + } catch (e) { + /* message already shown */ + } +} + +// ----- Shared ----- +function applyState(data) { + state.fleetDef = data.fleet_def; + state.status = data.status; + setMessage(data.message); + + const placementScreen = el("placement-screen"); + const battleScreen = el("battle-screen"); + + if (data.status === "placing") { + placementScreen.classList.remove("hidden"); + battleScreen.classList.add("hidden"); + renderFleetPicker(data); + renderPlacementBoard(data); + } else { + placementScreen.classList.add("hidden"); + battleScreen.classList.remove("hidden"); + renderBattle(data); + el("new-game-btn").classList.toggle( + "hidden", + !(data.status === "player_win" || data.status === "computer_win") + ); + } +} + +function init() { + el("rotate-btn").addEventListener("click", () => { + state.orientation = state.orientation === "h" ? "v" : "h"; + el("rotate-btn").textContent = + "🔄 Ausrichtung: " + (state.orientation === "h" ? "horizontal" : "vertikal"); + }); + + el("random-btn").addEventListener("click", async () => { + const data = await api("/api/random_place", { method: "POST" }); + applyState(data); + }); + + el("reset-btn").addEventListener("click", async () => { + const data = await api("/api/reset_placement", { method: "POST" }); + applyState(data); + }); + + el("start-btn").addEventListener("click", async () => { + try { + const data = await api("/api/start", { method: "POST" }); + applyState(data); + } catch (e) { + /* message already shown */ + } + }); + + el("new-game-btn").addEventListener("click", async () => { + const data = await api("/api/new_game", { method: "POST" }); + applyState(data); + }); + + api("/api/state").then(applyState); +} + +init(); diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..0479d25 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,55 @@ + + + + + +Zwerge versenken + + + +
+
+

⛏️ Zwerge versenken

+

Schiffe versenken mit Zwergenclans – du gegen den Computer

+
+ +
+ +
+

Aufstellung

+

Wähle einen Zwergenclan und klicke auf dein Feld, um ihn zu platzieren.

+ +
+
+ + + + +
+ +
+

Dein Lager

+
+
+
+ + +
+ + + +