Homepage von Papa

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.html und der zugehörigen styles.css, sowie die game.js und die route.py

Und 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
  • 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