Projekt Schiffeversenken
Setzen der Schiffe
In diesem Kapitel werden wir das Setzen der Schiffe auf dem eigenen Spielfeld implementieren. Dazu gehört auch, dass der Spieler seine Schiffe so lange neu platzieren kann, bis er mit dem Ergebnis zufrieden ist.
und auch hier der obligatorische Hinweis:
auf dieser Seite ist der Endzustand der version_05 dokumentiert. Die einzelnen Schritte sind:
- Fünf Schiffe sind nach bestimmten Regeln zu verteilen
- Die Schiffe können horizontal oder vertikal gesetzt werden
- Rückgängig machen der Platzierung ist möglich
- Sind alle Schiffe platziert, muss der Spieler den Start per Button bestätigen
- Ab diesem Zeitpunkt sind keine Änderungen am eigenen Spielfeld mehr möglich
- Geändert werden die Sourcen der
game.htmlund der zugehörigenstyles.css, sowie diegame.jsund dieroute.pyUnd der Vollständigkeit halber: die Projektdoku gibt es hier, den Source-Code gibt es hier.
Hier ein Bild des fertigen Zustands:
Gehen wir zunächst die game.html an. Der Code sieht so aus:
{% extends 'base.html' %}
{% block content %}
<div class="first_row">
<h3>
Spieler 1: {{ p_1_name }} - Spieler 2: {{ p_2_name }} – Spielcode ist {{ game_code }}
</h3>
</div>
<div class="game-board">
<!-- Eigenes Feld -->
<div class="board-container">
<h3>Eigenes Feld ({{ p_actual }})</h3>
<div class="board-wrapper">
<div class="row-labels">
<!-- Wird später mit JS gefüllt: 1 bis 10 -->
</div>
<div class="col-wrapper">
<div class="col-labels">
<!-- Wird später mit JS gefüllt: A bis J -->
</div>
<div id="myBoard"
class="board">
</div>
</div>
</div>
<div class="button-grid">
<input type="radio" name="orientation"
id="radioHorizontal"
value="horizontal" checked hidden>
<label for="radioHorizontal">⟷ Horizontal</label>
<input type="radio" name="orientation"
id="radioVertical"
value="vertical" hidden>
<label for="radioVertical">↕ Vertikal</label>
<button id="resetBtn">Zurücksetzen</button>
<button id="readyBtn" disabled>Fertig</button>
</div>
</div>
<!-- Gegnerisches Feld -->
<div class="board-container">
<h3>Gegner Feld ({{ p_opponent }})</h3>
<div class="status" id="info">
Status: Warte auf deinen Zug...
</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/game.js') }}"></script>
<script>
initGame({
gameCode: "{{ game_code }}",
p_actual: "{{ p_actual }}",
p1: "{{ p_1_name }}",
p2: "{{ p_2_name }}",
p1Status: "{{ p_1_status }}",
p2Status: "{{ p_2_status }}",
myBoard: "{{ my_board }}",
oppBoard: "{{ opp_board }}",
info: "{{ info }}"
});
</script>
{% endblock %}
Neu sind die Zeilen 32 bis 46. Dann also hier der Code für die styles.css:
/* Grundlayout */
body {
font-family: 'Montserrat', Verdana, Geneva, Tahoma, sans-serif; /* Schriftart */
background-color: #f8f9fa;
margin: 0;
padding: 20px;
color: #333;
}
/* Button */
.button-index,
.button-setup {
padding: 10px 20px;
margin: 5px;
background-color: #007bff;
border: none;
color: white;
font-weight: bold;
border-radius: 5px;
cursor: pointer;
}
/* Input-Felder */
.fancy-input {
width: 250px;
margin-left: 10px;
margin-right: 10px;
padding: 10px 14px;
font-size: 16px;
border: 2px solid #ccc;
border-radius: 5px;
background-color: #f9f9f9;
color: #333;
outline: none;
transition: border-color 0.3s, box-shadow 0.3s;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
}
.fancy-input:focus {
border-color: #6cc06c; /* grüner Fokusrahmen */
box-shadow: 0 0 6px #a2e2a2;
background-color: #ffffff;
}
.error {
background-color: #ffebee;
border-left: 5px solid #f44336;
color: #d32f2f;
padding: 10px 15px;
margin: 15px 0;
border-radius: 4px;
font-weight: 500;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
list-style-type: none;
animation: fadeIn 0.3s ease-in-out;
position: relative;
}
.error::before {
content: '⚠️';
margin-right: 10px;
font-size: 1.2em;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Erste Zeile */
.first_row {
text-align: center;
margin-bottom: 20px;
}
/* Infoanzeige */
#info {
margin-top: 20px;
font-size: 16px;
font-weight: bold;
min-height: 1.5em;
}
/* Gameboard-Container */
.game-board {
display: flex;
justify-content: center;
gap: 40px;
flex-wrap: wrap;
margin-bottom: 20px;
}
/* Einzelnes Board */
.board-container {
text-align: center;
}
.board {
display: grid;
grid-template-columns: repeat(10, 40px);
grid-template-rows: repeat(10, 40px);
gap: 0px;
margin-top: 10px;
justify-content: center;
}
/* Zahlen und Buchstaben */
.board-wrapper {
display: flex;
align-items: start;
justify-content: center;
gap: 2px;
}
/* Zahlen */
.row-labels {
display: grid;
grid-template-rows: 40px repeat(10, 40px);
margin-top: 5px;
margin-right: 5px;
text-align: right;
gap: 0px;
}
.label-spacer {
height: 40px;
gap: 2px;
}
.col-board {
display: flex;
flex-direction: column;
align-items: center;
}
/* Buchstaben */
.col-labels {
display: grid;
grid-template-columns: repeat(10, 40px);
height: 40px;
margin-bottom: 2px;
padding-bottom: 0px;
gap: 0px;
}
.col-labels div,
.row-labels div {
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
font-size: 14px;
box-sizing: border-box;
padding-bottom: 1px;
}
.col-labels div{
width: 40px;
height: 60px;
}
.row-labels div {
width: 20px;
height: 40px;
}
/* Zellen */
.cell {
width: 40px;
height: 40px;
background-color: #cce5ff;
border: 1px solid #99c2ff;
box-sizing: border-box;
cursor: pointer;
}
.cell.ship {
background-color: #687b91;
}
.cell.hit {
background-color: red;
}
.cell.miss {
background-color: #5fc1ee;
border: 2px dashed #ccc;
}
.cell.opponent-hit {
background-color: #d6b3ff; /* hell violett */
}
.cell.opponent-miss {
background-color: var(--blue-200, #5fc1ee);
}
/* die 4 Button unterhalb des eigenen Spielfelds */
.button-grid {
display: grid;
grid-template-columns: repeat(2, 150px);
grid-gap: 10px;
justify-content: center;
margin-top: 20px;
}
.button-grid label,
.button-grid button {
display: flex;
justify-content: center;
align-items: center;
height: 44px;
font-size: 14px;
border: 1px solid #888;
border-radius: 6px;
background-color: #007bff;
color: white;
cursor: pointer;
transition: background-color 0.2s ease;
}
.button-grid label:hover,
.button-grid button:hover {
background-color: #ccc;
}
.button-grid button:disabled,
.button-grid label.disabled {
background-color: #ddd;
opacity: 0.6;
cursor: not-allowed;
}
/* Verstecke Radio-Inputs */
input[type="radio"][name="orientation"] {
display: none;
}
/* Aktives Label grün färben */
input[type="radio"][name="orientation"]:checked + label {
background-color: #c8f7c5; /* Hellgrün */
border-color: #6cc06c;
color: #888;
font-weight: bold;
}
.disabled-label {
opacity: 0.5;
pointer-events: none;
cursor: default;
}
/* Ende der 4 Button unterhalb des eigenen Spielfelds */
Ist nicht weniger geworden... Und dann auch hier gleich weiter mit der routes.py:
from flask import Blueprint, render_template, request, redirect, url_for, session, jsonify
import generate_code
from app_init import db
from models import Game
# Erstelle ein Blueprint für die Routen
main = Blueprint('main', __name__)
@main.route('/')
def index():
return render_template('index.html')
@main.route("/setup", methods=["GET", "POST"])
def setup():
error = None
board_init = "0000000000" * 10 # 10x10 Spielfeld, initial leer
# Wenn Methode "POST" ist, erwarten wir Formulardaten
if request.method == "POST":
if request.form.get('p1_pressed') == 'True':
p_1_name = request.form.get("p_1_name", "").strip()
# Generiere einen eindeutigen Code
game_code = generate_code.generate_unique_code(6)
new_game = Game(game_code=game_code,
p_1_name=p_1_name,
p_1_board=board_init,
p_1_status="Spiel generiert")
db.session.add(new_game)
db.session.commit()
session["p_actual"] = p_1_name
return redirect(url_for("main.game", game_code=game_code))
# hier könnten wir auch else: setzen, da wir nur bei Klick auf einen
# der beiden Button hier landen
if request.form.get('p2_pressed') == 'True':
p_2_name = request.form.get("p_2_name", "").strip()
game_code = request.form.get("game_code", "").strip()
#Versuch, den Datensatz mit dem angegebenen Code zu finden
existing_game = Game.query.filter_by(game_code=game_code).first()
if not existing_game:
error = f"Ups {p_2_name}, der Code {game_code} existiert nicht"
return render_template(
"setup.html",
error=error,
p_2_name=p_2_name,
game_code=game_code
)
else:
if existing_game.p_2_name != None:
error = f"Ups {p_2_name}, das Spiel {game_code} hat schon 2 Spieler"
return render_template(
"setup.html",
error=error,
p_2_name=p_2_name,
game_code=game_code
)
else:
p_1_name = existing_game.p_1_name
game_code = existing_game.game_code
existing_game.p_2_name = p_2_name
existing_game.p_2_board = board_init
existing_game.p_2_status = "Spieler 2 angemeldet"
db.session.commit()
session["p_actual"] = p_2_name
return redirect(url_for("main.game", game_code=game_code))
else:
return render_template(
"setup.html"
)
@main.route("/game/")
def game(game_code):
game = Game.query.filter_by(game_code=game_code).first_or_404()
p_actual = session.get("p_actual")
print(f"Spieler: {p_actual}, Spielcode: {game_code}")
# das eigene Spielfeld
my_board = game.p_1_board if p_actual == game.p_1_name else game.p_2_board
# das gengerische Spielfeld
opp_board = game.p_2_board if p_actual == game.p_1_name else game.p_1_board
p_opponent = ''
if p_actual == game.p_1_name:
p_opponent = game.p_2_name or "noch niemand..."
if p_actual == game.p_2_name:
p_opponent = game.p_1_name
return render_template(
"game.html",
p_1_name=game.p_1_name,
p_2_name=game.p_2_name or "noch niemand...",
game_code=game.game_code,
p_actual=p_actual,
p_opponent=p_opponent,
p1_status=game.p_1_status,
p2_status=game.p_2_status,
my_board=my_board or "",
opp_board=opp_board
)
@main.route("/save_board", methods=["POST"])
def save_board():
data = request.get_json(force=True)
gameCode = data.get("gameCode")
board = data.get("myBoard")
p_actual = data.get("p_actual")
print(f"in save_board: gameCode={gameCode}, player={p_actual}, board={board}")
if not (gameCode and board and p_actual) or len(board) != 100:
return jsonify({"error": "Ungültige Daten"}), 400
return jsonify({"status": "ok"}), 200
Auch kein Pappenstiel, richtig? Dann gleich weiter mit dem letzten Sourcecode, der game.js:
(function () {
'use strict';
/* === Initialisierung ================================================= */
const BOARD_SIZE = 10;
const SHIPS_TEMPLATE = [
{ size: 5, count: 1 },
{ size: 4, count: 1 },
{ size: 3, count: 2 },
{ size: 2, count: 1 },
];
const TOTAL_SHIPS = SHIPS_TEMPLATE.reduce((s, v) => s + v.count, 0);
const deepCopyShips = () => SHIPS_TEMPLATE.map((s) => ({ ...s }));
const setInfo = (t) => (cfg.el.info.textContent = `${t}`);
const lockBoard = (b) => Array.from(b.children).forEach((c) => c.replaceWith(c.cloneNode(true)));
const unlockBoard = (b, h) => Array.from(b.children).forEach((c) => {
const n = c.cloneNode(true);
n.className = c.className;
n.dataset.idx = c.dataset.idx;
if (h) n.addEventListener('click', h);
c.replaceWith(n);
});
const boardStr = () => boardState.join('');
let cfg = {};
let ships = [];
let boardState = [];
let placedShips = 0;
let placementsStack = [];
let orientation = 'horizontal';
/* === Hilfsfunktion für JSON ========================================== */
async function postJSON(url, body) {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`HTTP ${res.info}`);
return res.json();
}
/* === Boards erstellen ================================================ */
function createBoard(root, onClick) {
root.innerHTML = '';
for (let i = 0; i < BOARD_SIZE ** 2; i++) {
const d = document.createElement('div');
d.className = 'cell';
d.dataset.idx = i;
if (onClick) d.addEventListener('click', onClick);
root.appendChild(d);
}
}
/* === Labels für Boards =============================================== */
function generateBoardLabels() {
const rowLabels = document.querySelectorAll('.row-labels');
const colLabels = document.querySelectorAll('.col-labels');
for (const el of rowLabels) {
el.innerHTML = '';
const spacer = document.createElement('div');
spacer.classList.add('label-spacer');
el.appendChild(spacer);
for (let i = 1; i <= 10; i++) {
const div = document.createElement('div');
div.textContent = i;
el.appendChild(div);
}
}
for (const el of colLabels) {
el.innerHTML = '';
for (let i = 0; i < 10; i++) {
const div = document.createElement('div');
div.textContent = String.fromCharCode(65 + i); // A–J
el.appendChild(div);
}
}
}
/* === Platzierung ====================================================== */
const nextShipSize = () => ships.find((s) => s.count > 0)?.size ?? null;
function canPlace(start, size) {
const r0 = Math.floor(start / BOARD_SIZE);
const c0 = start % BOARD_SIZE;
for (let i = 0; i < size; i++) {
const r = orientation === 'horizontal' ? r0 : r0 + i;
const c = orientation === 'horizontal' ? c0 + i : c0;
// Grenzen prüfen
if (r >= BOARD_SIZE || c >= BOARD_SIZE) return false;
// Index der aktuellen Zelle
const currentIndex = r * BOARD_SIZE + c;
// Direkt belegte Felder verhindern
if (cfg.el.myBoard.children[currentIndex].classList.contains('ship')) return false;
// Nur orthogonale Nachbarn (oben, unten, links, rechts) prüfen
const orthogonalNeighbors = [
[r - 1, c], // oben
[r + 1, c], // unten
[r, c - 1], // links
[r, c + 1] // rechts
];
for (const [nr, nc] of orthogonalNeighbors) {
if (nr < 0 || nc < 0 || nr >= BOARD_SIZE || nc >= BOARD_SIZE) continue;
const neighborIndex = nr * BOARD_SIZE + nc;
if (cfg.el.myBoard.children[neighborIndex].classList.contains('ship')) {
setInfo('Schiff dürfen sich nicht berühren!');
return false;
}
}
}
setInfo('Schiff platziert');
return true;
}
const paintShip = (start, size) => {
for (let i = 0; i < size; i++) {
const off = orientation === 'horizontal' ? i : i * BOARD_SIZE;
cfg.el.myBoard.children[start + off].classList.add('ship');
boardState[start + off] = 1;
}
};
function placeShip(e) {
if (placedShips >= TOTAL_SHIPS) return;
const idx = +e.target.dataset.idx;
const size = nextShipSize();
if (!size || !canPlace(idx, size)) return;
paintShip(idx, size);
ships.find((s) => s.size === size).count--;
placedShips++;
placementsStack.push({
index: idx,
size: size,
orientation: orientation
});
if (placedShips === TOTAL_SHIPS) {
lockBoard(cfg.el.myBoard);
cfg.el.readyBtn.disabled = false;
setInfo('Alle Schiffe platziert – Fertig!');
}
}
function removeShip(start, size, orientation) {
for (let i = 0; i < size; i++) {
const off = orientation === 'horizontal' ? i : i * BOARD_SIZE;
cfg.el.myBoard.children[start + off].classList.remove('ship');
boardState[start + off] = 0;
}
const ship = ships.find(s => s.size === size);
if (ship) {
ship.count++; // Zähler erhöhen
}
placedShips--; // Verringere die Anzahl der platzierten Schiffe
console.log("Schiff entfernt:", { start, size, orientation, placedShips });
if (placedShips < TOTAL_SHIPS) {
cfg.el.readyBtn.disabled = true;
unlockBoard(cfg.el.myBoard, placeShip);
}
}
/* === Buttons ========================================================== */
function initButtons() {
cfg.el.orientationRadios.forEach(radio => {
radio.addEventListener('change', (e) => {
orientation = e.target.value;
});
});
cfg.el.resetBtn.addEventListener('click', () => {
if (placementsStack.length > 0) {
// Entferne die letzte Platzierung
const lastPlacement = placementsStack.pop();
// Entferne das Schiff von der Anzeige
removeShip(lastPlacement.index, lastPlacement.size, lastPlacement.orientation);
setInfo('Letzte Platzierung rückgängig gemacht.');
} else {
setInfo('Keine Platzierungen zum Rückgängigmachen.');
}
// Radio-Buttons reaktivieren und „horizontal" als Standard setzen
cfg.el.orientationRadios.forEach(radio => {
radio.disabled = false;
radio.checked = radio.value === 'horizontal';
});
document.getElementById('radioHorizontal').checked = true;
orientation = 'horizontal';
});
cfg.el.readyBtn.addEventListener('click', () => {
cfg.el.readyBtn.disabled = true;
cfg.el.resetBtn.disabled = true;
// Alle Radio-Buttons deaktivieren
cfg.el.orientationRadios.forEach(radio => {
radio.disabled = true;
// Zugehöriges Label visuell deaktivieren
const label = document.querySelector(`label[for="${radio.id}"]`);
if (label) {
label.classList.add('disabled-label');
}
});
lockBoard(cfg.el.myBoard);
postJSON('/save_board', {
gameCode: cfg.gameCode,
myBoard: boardStr(),
p_actual: cfg.p_actual
})
.then(() => setInfo('Spielbrett gesendet.'))
.catch(console.error);
});
}
/* === Setup & Init ===================================================== */
function startPlacement() {
ships = deepCopyShips();
boardState = Array(BOARD_SIZE ** 2).fill(0);
placedShips = 0;
placementsStack = [];
orientation = 'horizontal';
createBoard(cfg.el.myBoard, placeShip);
cfg.el.readyBtn.disabled = true;
}
function initGame(userCfg) {
cfg = {
...userCfg,
el: {
gameCode: document.getElementById('gameCode'),
p_1_name: document.getElementById('p_1_name'),
p_2_name: document.getElementById('p_2_name'),
p_actual: document.getElementById('p_actual'),
p_opponent: document.getElementById('p_opponent'),
myBoard: document.getElementById('myBoard'),
oppBoard: document.getElementById('oppBoard'),
resetBtn: document.getElementById('resetBtn'),
readyBtn: document.getElementById('readyBtn'),
info: document.getElementById('info'),
orientation: 'horizontal',
//orientationRadios: Array.from(document.querySelectorAll('input[name="orientation"]')),
orientationRadios: document.querySelectorAll('input[name="orientation"]'),
},
};
console.log("Game initialized with config:", cfg);
// Erstellen der Boards
createBoard(cfg.el.oppBoard, null);
generateBoardLabels();
initButtons();
startPlacement();
}
window.initGame = initGame;
})();
Wenn wir das jetzt laufen lassen, sollte der neue Funktionsumfang folgender sein:
- Nach Start landen wir auf der
index.html - Mit Klick auf "Go" landen wir auf der
setup.html - Egal ob wir uns als Spieler 1 oder Spieler 2 anmelden, landen wir auf der
game.html - Dort können wir die 5 Schiffe platzieren
- Vor dem Setzen können wir entscheiden, ob wir das Schiff horizontal oder vertikal platzieren wollen
- Erst wenn das 5. Schiff gesetzt wurde, wird der "Fertig"-Button aktiviert
- Sobald der "Fertig"-Button geklickt wurde, ist das Setzen der Schiffe beendet, alle Button sind deaktiviert
Inhaltsverzeichnis:
1. Vorwort2. Das Projekt
3. Vorarbeiten
4. Das Projekt „Schiffeversenken“
4.1. Der Funktionsumfang
4.2. Die Planung der Umsetzung
4.3. Das Coden
4.3.1 Arbeiten mit Flask
4.3.2 Die Datenbank
4.3.3 Der Spielstart
4.3.4 Der Spielcode
4.3.5 Die Spielfelder
4.3.6 Setzen der Schiffe
4.3.7 Das Spielen
4.4. Die Veröffentlichung
5. Abschluss
