Projekt Schiffeversenken
Die Spielfelder
In diesem Kapitel werden die beiden Spielfelder generiert, also das eigene und das des Gegner.
Auch hier wieder - wichtig zum Verständnis:
auf dieser Seite ist der Endzustand der version_04 dokumentiert. Die einzelnen Schritte sind:
- Füllen der bis jetzt noch leeren JavaScript-Datei
game.js- Erweiterung im "Frontend" der HTML-Seite
game.htmlum den Aufruf vongame.js.- Damit einhergehend die Erweiterung der CSS-Datei
styles.css.- Erweiterung im "Backend" also der
routes.pyAuch hier wieder der Hinweis auf die Projektdoku, zu finden hier, falls tiefergehende Infos gewünscht werden.
An der Struktur hat sich nichts geändert, wir haben keine neuen Komponenten hinzugefügt.
Vielleicht ist es aber doch ganz interessant, ein Bildchen zu haben, das uns das Zusammenspiel der Komponenten zeigt:
Gehen wir es kurz durch?
- Beide Spieler melden sich jeweils über Ihre Browser via Internet an unserer Applikation "Schiffeversenken" an.
- Start ist die HTML-Seite
index.html - Im Wechsel mit der Python-Datei
routes.pyerfolgt die Verzweigung innerhalb der Anwendung. - In der HTML-Seite
setup.htmlerfolgt die Anmeldung, entweder als Spieler 1 oder als Spieler 2. - Hat die Anmeldung als Spieler 1 oder Spieler 2 geklappt, erfolgt die Weiterleitung an die HTML-Seite
game.htmlin der wir bis zum Ende des Spiels bleiben. - Auf alle HTML-Seiten wirkt die CSS-Datei
styles.css. - Als Unterstützung der
game.htmlwird jetzt die JavaScript-Dateigame.jsherangezogen, das lernen wir in diesem Kapitel. - Die Kommunikation mit der Datenbank übernehmen die beiden Dateien
app_init.pyin Zusammenarbeit mitmodels.py
Damit haben wir alle Komponenten der Anwendung zusammen, im nächsten Kapitel kommt noch die HTML-Datei game_over.html dazu, aber soweit sind wir noch nicht.
und noch einmal der Hinweis...
Statt alles per Hand zu kopieren, könnt Ihr Euch auch die Sourcen hier oder über den Button unten herunterladen.
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>
<!-- Gegnerisches Feld -->
<div class="board-container">
<h3>Gegner Feld ({{ p_opponent }})</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="oppBoard" class="board"></div>
</div>
</div>
<div class="status" id="status">Status: Warte auf deinen Zug...</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/game.js') }}"></script>
<script>
initGame({
gameCode: "{{ game_code }}",
player: "{{ p_actual }}",
p1: "{{ p_1_name }}",
p2: "{{ p_2_name }}",
p1Status: "{{ p_1_status }}",
p2Status: "{{ p_2_status }}",
myBoard: "{{ my_board }}",
oppBoard: "{{ opp_board }}"
});
</script>
{% endblock %}
Da wir gelernt haben, dass HTML immer mit CSS zusammengeht, 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;
}
/* Statusanzeige */
#status {
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);
}
Ja, das war ein Brocken. Und gleich geht es weiter mit der routes.py:
from flask import Blueprint, render_template, request, redirect, url_for, session
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
)
Auch kein Pappenstiel, richtig? Dann gleich weiter mit dem letzten Sourcecode, der game.js:
(function () {
'use strict';
/* === Initialisierung ================================================= */
const BOARD_SIZE = 10;
/* === 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);
}
}
}
/* === Setup & Init ===================================================== */
function initGame(userCfg) {
const 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')
},
};
console.log("Game initialized with config:", cfg);
// Erstellen der Boards
createBoard(cfg.el.myBoard, null);
createBoard(cfg.el.oppBoard, null);
generateBoardLabels();
}
window.initGame = initGame;
})();
Jetzt kommt der Moment, wo die Kuh den Schwanz hebt, lasst es uns ausprobieren. Spätestens jetzt brauchen wir 2 Browser-Instanzen, damit wir beide Spieler simulieren können.
Spieler 1 ist bei mir die Maus, Spieler 2 der Elefant. Spielcode ist HSKGNL. Anmeldung Maus im Firefox:
Anmeldung Elefant im Chrome:
Da sich der Elefant nach der Maus angemeldet hat, hat er schon die Information, wer sein Gegner ist, die Maus hatte das zum Zeitpunkt des Screenshots noch nicht. Sobald die Maus den Browser aktualisiert (mit F5 im Browser), wird ihr der Elefant angezeigt:
Bei Euch sollte das alles genauso aussehen, falls nicht, Ihr wisst ja, version_04 liegt auf dem Server.
Jetzt müssen wir das Setzen der Schiffe einbauen, das wird noch ein schöner Brocken werden, versprochen, mehr dazu im nächsten Kapitel.
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



