/**
* @fileoverview Zentrale Spiellogik für die Tic-Tac-Toe Varianten.
* Beinhaltet die Klassen für Regular (3x3), 3D (NxNxN) und Ultimate.
* Implementiert das GameState Interface.
*/
// Verwende zentrale Konstanten aus config/constants.js
if (typeof GAME_CONSTANTS === 'undefined') {
if (typeof DebugConfig !== 'undefined') {
DebugConfig.log(DEBUG_DOMAINS.GAMES_TTT_LOGIC, "error", '❌ FEHLER: GAME_CONSTANTS ist nicht geladen!');
}
}
// Lokale Referenzen zu zentralen Konstanten (für Rückwärtskompatibilität)
// HINWEIS: NONE, PLAYER1, PLAYER2, DRAW, CELL_EMPTY, INVALID_INDEX sind bereits global
// in constants.js definiert und daher direkt verfügbar!
/**
* Abstrakte Basisklasse für Tic-Tac-Toe Spiele.
* @abstract
*/
class TTTBase {
constructor() {
/** * Aktueller Spieler.
* 1 = Spieler 1 (Blau/Kreis), 2 = Spieler 2 (Rot/Kreuz).
* @type {number}
*/
this.currentPlayer = PLAYER1;
/**
* Gewinner des Spiels.
* 0 = Laufend, 1 = Spieler 1, 2 = Spieler 2, 3 = Remis.
* @type {number}
*/
this.winner = NONE;
}
/**
* Wechselt den aktiven Spieler (1 -> 2 -> 1).
*/
switchPlayer() {
this.currentPlayer = (this.currentPlayer === PLAYER1) ? PLAYER2 : PLAYER1;
}
}
/**
* Klassisches 3x3 Tic-Tac-Toe Board.
* @extends TTTBase
*/
class TTTRegularBoard extends TTTBase {
constructor() {
super();
/** * Das 3x3 Gitter als flaches Array (Indizes 0-8).
* 0 = Leer, 1 = Spieler 1, 2 = Spieler 2.
* @type {number[]}
*/
this.grid = Array(9).fill(CELL_EMPTY);
}
/**
* Liefert alle Indizes von leeren Feldern.
* Liste der möglichen Züge.
* ✅ WICHTIG: Prüft NICHT auf winner, weil Simulationen kaputte Klone haben können!
* @returns {number[]}
*/
getAllValidMoves() {
// ✅ Gib alle leeren Felder zurück, unabhängig vom winner Status
return this.grid.map((val, idx) => val === CELL_EMPTY ? idx : INVALID_INDEX).filter(idx => idx !== INVALID_INDEX);
}
/**
* Führt einen Zug an der Position index aus.
* - Index des Feldes (0-8).
* @param {number} index
* True, wenn der Zug gültig war.
* @returns {boolean}
*/
makeMove(index) {
// Validierung: Index im Bereich, Feld leer, Spiel läuft
if (index < 0 || index >= 9 || this.grid[index] !== CELL_EMPTY || this.winner !== NONE) {
return false;
}
// Setzen
this.grid[index] = this.currentPlayer;
// Status prüfen
this.checkWin();
// Spielerwechsel (nur wenn Spiel nicht vorbei)
if (this.winner === NONE) {
this.switchPlayer();
}
return true;
}
/**
* Überprüft alle 8 Gewinnlinien auf 3 Gleiche.
* Setzt this.winner entsprechend.
*/
checkWin() {
const lines = [
[0,1,2], [3,4,5], [6,7,8], // Horizontal
[0,3,6], [1,4,7], [2,5,8], // Vertikal
[0,4,8], [2,4,6] // Diagonal
];
for (const line of lines) {
const [a, b, c] = line;
if (this.grid[a] !== CELL_EMPTY &&
this.grid[a] === this.grid[b] &&
this.grid[b] === this.grid[c]) {
this.winner = this.grid[a];
return;
}
}
// Remis Check (Brett voll, kein Gewinner)
if (!this.grid.includes(CELL_EMPTY)) {
this.winner = DRAW;
}
}
/**
* Erstellt eine tiefe Kopie des Boards (für KI-Simulationen).
* @returns {TTTRegularBoard}
*/
clone() {
const copy = new TTTRegularBoard();
copy.grid = [...this.grid];
copy.currentPlayer = this.currentPlayer;
copy.winner = this.winner;
return copy;
}
/**
* Generiert einen eindeutigen String für diesen Zustand.
* Hash Key.
* @returns {string}
*/
getStateKey() {
return this.grid.join('') + this.currentPlayer;
}
}
/**
* 3D Tic-Tac-Toe Board (Würfel).
* Unterstützt variable Größen (z.B. 3x3x3 oder 4x4x4).
* @extends TTTBase
*/
class TTT3DBoard extends TTTBase {
/**
* @param {number} [size=3] - Kantenlänge des Würfels.
*/
constructor(size = 3) {
super();
this.size = size;
this.totalCells = size * size * size;
/** * Das 3D Gitter als flaches Array.
* Index = z * size^2 + y * size + x
* @type {number[]}
*/
this.grid = Array(this.totalCells).fill(CELL_EMPTY);
}
/**
* Liefert alle leeren Felder im Würfel.
* ✅ WICHTIG: Prüft NICHT auf winner!
* @returns {number[]}
*/
getAllValidMoves() {
const moves = [];
for (let i = 0; i < this.totalCells; i++) {
if (this.grid[i] === CELL_EMPTY) moves.push(i);
}
return moves;
}
/**
* Setzt einen Stein an index.
* - Berechneter Index im flachen Array.
* @param {number} index
* @returns {boolean}
*/
makeMove(index) {
if (index < 0 || index >= this.totalCells || this.grid[index] !== CELL_EMPTY || this.winner !== NONE) {
return false;
}
this.grid[index] = this.currentPlayer;
this.checkWin();
if (this.winner === NONE) {
this.switchPlayer();
}
return true;
}
/**
* Prüft alle möglichen Gewinnlinien im 3D Raum.
* Es gibt 13 Richtungsvektoren (Achsen, Flächendiagonalen, Raumdiagonalen).
*/
checkWin() {
// Richtungsvektoren (dx, dy, dz)
const directions = [
[1,0,0], [0,1,0], [0,0,1], // 3 Achsen
[1,1,0], [1,-1,0], [1,0,1], [1,0,-1], [0,1,1], [0,1,-1], // 6 Flächendiagonalen
[1,1,1], [1,1,-1], [1,-1,1], [1,-1,-1] // 4 Raumdiagonalen
];
// Wir iterieren durch jede Zelle als potentiellen Startpunkt
for (let z = 0; z < this.size; z++) {
for (let y = 0; y < this.size; y++) {
for (let x = 0; x < this.size; x++) {
const idx = this._getIndex(x, y, z);
const player = this.grid[idx];
if (player === CELL_EMPTY) continue;
// Von hier aus in alle Richtungen prüfen
for (const dir of directions) {
if (this._checkLine(x, y, z, dir[0], dir[1], dir[2], player)) {
this.winner = player;
return;
}
}
}
}
}
// Remis
if (!this.grid.includes(CELL_EMPTY)) {
this.winner = DRAW;
}
}
/**
* Prüft eine spezifische Linie vom Startpunkt (x,y,z) in Richtung (dx,dy,dz).
* @private
*/
_checkLine(x, y, z, dx, dy, dz, player) {
// 1. Prüfen, ob die Linie überhaupt lang genug sein kann (Bounds Check am Endpunkt)
const endX = x + dx * (this.size - 1);
const endY = y + dy * (this.size - 1);
const endZ = z + dz * (this.size - 1);
if (endX < 0 || endX >= this.size ||
endY < 0 || endY >= this.size ||
endZ < 0 || endZ >= this.size) {
return false;
}
// 2. Linie ablaufen
for (let i = 1; i < this.size; i++) {
const nx = x + dx * i;
const ny = y + dy * i;
const nz = z + dz * i;
if (this.grid[this._getIndex(nx, ny, nz)] !== player) {
return false;
}
}
return true;
}
/** * Hilfsmethode: x,y,z zu Array-Index
* @private
*/
_getIndex(x, y, z) {
return z * (this.size * this.size) + y * this.size + x;
}
clone() {
const c = new TTT3DBoard(this.size);
c.grid = [...this.grid];
c.currentPlayer = this.currentPlayer;
c.winner = this.winner;
return c;
}
getStateKey() { return this.grid.join('') + this.currentPlayer; }
}
/**
* Ultimate Tic-Tac-Toe.
* 9 kleine Boards (3x3) in einem großen Board.
* @extends TTTBase
*/
class UltimateBoard extends TTTBase {
constructor() {
super();
/**
* 9 Arrays à 9 Felder.
* @type {number[][]}
*/
this.boards = Array(9).fill(null).map(() => Array(9).fill(CELL_EMPTY));
/**
* Status der 9 großen Felder (Makro-Board). 0=Offen, 1/2=Sieg, 3=Remis.
* @type {number[]}
* */
this.macroBoard = Array(9).fill(CELL_EMPTY);
/**
* Index des Boards, in das der nächste Spieler setzen MUSS. -1 = Freie Wahl.
* @type {number}
* */
this.nextBoardIdx = INVALID_INDEX;
}
/**
* Liefert alle gültigen Züge als Objekte {big, small}.
* ✅ WICHTIG: Prüft NICHT auf winner!
* @returns {Array<{big:number, small:number}>}
*/
getAllValidMoves() {
const moves = [];
let targetBoards = [];
// Regel: Wenn man in ein Board geschickt wird und es noch nicht VOLL ist, MUSS man dort spielen.
// Ein gewonnenes Board mit freien Feldern ist weiterhin spielbar!
if (this.nextBoardIdx !== INVALID_INDEX && !this._isBoardFull(this.nextBoardIdx)) {
targetBoards = [this.nextBoardIdx];
} else {
// Sonst: Freie Wahl auf allen nicht vollen Boards
for (let i = 0; i < 9; i++) {
if (!this._isBoardFull(i)) {
targetBoards.push(i);
}
}
}
// Alle freien Felder in den Ziel-Boards sammeln
for (const bIdx of targetBoards) {
for (let sIdx = 0; sIdx < 9; sIdx++) {
if (this.boards[bIdx][sIdx] === 0) {
moves.push({ big: bIdx, small: sIdx });
}
}
}
return moves;
}
/**
* Führt einen Zug aus.
* Akzeptiert flexibel:
* - makeMove({big: 0, small: 4}) - Objekt-Format
* - makeMove(0, 4) - zwei Parameter
*
* @param {number|object} big - Index des großen Boards (0-8) oder Move-Objekt
* @param {number} [small] - Index des kleinen Feldes (0-8), optional wenn big ein Objekt ist
* @returns {boolean} True bei Erfolg
*/
makeMove(big, small) {
// Flexibles Format: akzeptiere {big, small} Objekt oder zwei Parameter
if (typeof big === 'object' && big !== null) {
small = big.small;
big = big.big;
}
// 1. Basis-Checks
if (this.winner !== NONE) return false;
// 2. Regel-Check: Darf ich in dieses 'big' Board setzen?
// Wenn nextBoardIdx aktiv (nicht -1) ist und das Zielboard noch nicht VOLL ist,
// muss 'big' gleich 'nextBoardIdx' sein.
// (Ein gewonnenes Board mit freien Feldern ist noch spielbar!)
if (this.nextBoardIdx !== INVALID_INDEX && !this._isBoardFull(this.nextBoardIdx)) {
if (big !== this.nextBoardIdx) return false; // Ungültiges Board gewählt!
}
// 3. Board darf nicht voll sein
if (this._isBoardFull(big)) return false;
// 4. Feld belegt?
if (this.boards[big][small] !== CELL_EMPTY) return false;
// --- ZUG AUSFÜHREN ---
this.boards[big][small] = this.currentPlayer;
// 4. Prüfen, ob das kleine Board gewonnen wurde
// (Nur wenn es noch nicht entschieden war)
if (this.macroBoard[big] === CELL_EMPTY) {
const w = this._checkSmallWin(this.boards[big]);
if (w !== CELL_EMPTY) {
this.macroBoard[big] = w; // Board gewonnen
} else if (!this.boards[big].includes(CELL_EMPTY)) {
this.macroBoard[big] = DRAW; // Board voll (Remis)
}
}
// 5. Prüfen, ob das große Board (Spiel) gewonnen wurde
const gameWin = this._checkSmallWin(this.macroBoard);
if (gameWin !== NONE) {
this.winner = gameWin;
} else if (!this.macroBoard.includes(CELL_EMPTY)) {
// Alle großen Felder entschieden, aber keine Reihe -> Remis
this.winner = DRAW;
}
if (this.winner === NONE) {
this.switchPlayer();
}
// 6. Nächstes Board bestimmen
// Der Spieler wird in das Board geschickt, das dem 'small' Index entspricht.
this.nextBoardIdx = small;
// Wenn das Zielboard aber schon VOLL ist, hat der nächste Spieler freie Wahl.
// Ein gewonnenes Board mit freien Feldern ist weiterhin spielbar!
if (this._isBoardFull(this.nextBoardIdx)) {
this.nextBoardIdx = INVALID_INDEX;
}
return true;
}
/** Prüft, ob ein kleines Board keine freien Felder mehr hat. */
_isBoardFull(idx) {
// Keine Nullen im Grid = Voll
return !this.boards[idx].includes(CELL_EMPTY);
}
/** Hilfsfunktion: 3-in-einer-Reihe auf einem 9er Array. */
_checkSmallWin(grid) {
const wins = [[0,1,2],[3,4,5],[6,7,8], [0,3,6],[1,4,7],[2,5,8], [0,4,8],[2,4,6]];
for (const w of wins) {
// Ignoriere 0 (leer) und 3 (Remis-Marker) bei der Gewinnprüfung
if (grid[w[0]] !== CELL_EMPTY && grid[w[0]] !== DRAW &&
grid[w[0]] === grid[w[1]] &&
grid[w[1]] === grid[w[2]]) {
return grid[w[0]];
}
}
return CELL_EMPTY;
}
clone() {
const c = new UltimateBoard();
// Arrays kopieren
c.boards = this.boards.map(r => [...r]);
c.macroBoard = [...this.macroBoard];
c.currentPlayer = this.currentPlayer;
c.nextBoardIdx = this.nextBoardIdx;
c.winner = this.winner;
return c;
}
getStateKey() {
return JSON.stringify(this.boards) + this.currentPlayer;
}
}