Homepage von Papa

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.html um den Aufruf von game.js.
  • Damit einhergehend die Erweiterung der CSS-Datei styles.css.
  • Erweiterung im "Backend" also der routes.py

Auch 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.py erfolgt die Verzweigung innerhalb der Anwendung.
  • In der HTML-Seite setup.html erfolgt 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.html in der wir bis zum Ende des Spiels bleiben.
  • Auf alle HTML-Seiten wirkt die CSS-Datei styles.css.
  • Als Unterstützung der game.html wird jetzt die JavaScript-Datei game.js herangezogen, das lernen wir in diesem Kapitel.
  • Die Kommunikation mit der Datenbank übernehmen die beiden Dateien app_init.py in Zusammenarbeit mit models.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.

  • Zurück
  • Weiter

Inhaltsverzeichnis:

1. Vorwort
2. 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

© by Papa. Die Seite ist online seit 2020.

Menu

  • Startseite
  • Projekte
    • Übersicht aller Projekte
    • Schiffeversenken
    • Taschenrechner
    • Nachbau Snake
  • Helferlein
    • Übersicht Hilfprogramme
    • Fonts in pygame
    • Quellcode nach HTML
    • Text nach HTML
  • Impressum
  • Disclaimer

Modal content goes here