/**
* Kernlogik für das RotateBox Spiel.
* @fileoverview
* Definiert den Spielzustand, das Parsen der Level und die Physik.
*/
/**
* Feldtyp: Leeres Feld
* @constant {number}
*/
const LEER = -1;
/**
* Feldtyp: Wand (nicht begehbar)
* @constant {number}
*/
const WAND = -2;
/**
* Feldtyp: Ziel (Endpunkt)
* @constant {number}
*/
const ZIEL = -3;
/**
* Repräsentiert das Spielbrett und dessen Zustand.
* Implementiert das GameState Interface für die KI.
* @implements {GameState}
*/
class RotateBoard {
/**
* Erstellt eine neue Board-Instanz.
* @param {string|null} idOrData - Die Level-ID ('0'-'3') oder null (für leeres Board/Klonen).
*/
constructor(idOrData) {
/**
* Anzahl der getätigten Züge.
* @type {number}
*/
this.moves = 0;
/**
* Gibt an, ob das Ziel erreicht wurde.
* @type {boolean}
*/
this.won = false;
/**
* Flag für laufende Fall-Animationen.
* @type {boolean}
*/
this.isFalling = false;
/**
* Speichert visuelle Offsets für fallende Boxen.
* @type {Object.<number, number>}
*/
this.fallOffsets = {};
// Grid Dimensionen
this.rows = 0;
this.cols = 0;
/**
* Das Spielfeld als 2D-Array (WAND, LEER, ZIEL, >=0 BoxID).
* @type {number[][]}
*/
this.grid = [];
// Bei null (z.B. beim Klonen) keine Initialisierung durchführen
if (idOrData === null) return;
// Level laden (Default zu '0' falls ungültig)
const id = (typeof idOrData === 'string') ? idOrData : '0';
this.initFromId(id);
}
/**
* Lädt die Leveldaten aus den Strings.
* WICHTIG: Die Strings enthalten Leerzeichen, die für das Layout essenziell sind.
* @param {string} id
*/
initFromId(id) {
const levels = {
'0': "5###### 0 ##10 ##10 ####x#",
'1': "8######### 0## 0##112222##33 4##55 4##666 4####x####",
'2': "12############# ## 01 ## 01 ## 01 ## 222222## 34 5 ## 34 5 ## 634 5 ## 63477775 ## 63888885 #######x#####",
'3': "10########### ## ## ## 7775## 11 5## 2 888##990233 ##44066666######x####"
};
const str = levels[id] || levels['0'];
// Dimensionen parsen: Zahl am Anfang = Zeilen
let offset = 0;
while (offset < str.length && str[offset] !== '#') offset++;
this.rows = parseInt(str.substring(0, offset));
const content = str.substring(offset);
this.cols = Math.floor(content.length / this.rows);
// Grid befüllen
this.grid = [];
for (let r = 0; r < this.rows; r++) {
let row = [];
for (let c = 0; c < this.cols; c++) {
const idx = r * this.cols + c;
if (idx < content.length) {
const char = content[idx];
let val = LEER; // Standard: Leer
if (char === '#') val = WAND;
else if (char === 'x') val = ZIEL;
else if (char !== ' ') { // Box (Zahl)
const p = parseInt(char);
if (!isNaN(p)) val = p;
}
row.push(val);
} else {
row.push(LEER);
}
}
this.grid.push(row);
}
}
/**
* Rotiert das Spielfeld um 90 Grad.
* - True für Rechtsdrehung, False für Links.
* @param {boolean} [clockwise=true]
*/
rotate(clockwise = true) {
const newGrid = Array.from({ length: this.cols }, () => Array(this.rows).fill(LEER));
for (let r = 0; r < this.rows; r++) {
for (let c = 0; c < this.cols; c++) {
if (clockwise) newGrid[c][this.rows - 1 - r] = this.grid[r][c];
else newGrid[this.cols - 1 - c][r] = this.grid[r][c];
}
}
this.grid = newGrid;
// Dimensionen tauschen
[this.rows, this.cols] = [this.cols, this.rows];
this.moves++;
}
/**
* Prüft, ob eine Box physikalisch fallen kann.
* Die ID der Box.
* @param {number} id
* True, wenn der Weg nach unten frei ist.
* @returns {boolean}
*/
canFall(id) {
for (let r = 0; r < this.rows; r++) {
for (let c = 0; c < this.cols; c++) {
if (this.grid[r][c] === id) {
// Boden erreicht?
if (r + 1 >= this.rows) return false;
const target = this.grid[r + 1][c];
// Blockiert, wenn darunter NICHT (Leer ODER Ziel ODER Selbst) ist
if (target !== LEER && target !== ZIEL && target !== id) return false;
}
}
}
return true;
}
/**
* Bewegt eine Box logisch um ein Feld nach unten.
* Die ID der Box.
* @param {number} id
*/
moveDown(id) {
let reachedExit = false;
// Wichtig: Iteration von unten nach oben, um Überschreiben zu vermeiden
for (let r = this.rows - 1; r >= 0; r--) {
for (let c = 0; c < this.cols; c++) {
if (this.grid[r][c] === id) {
// Prüfen ob Ziel erreicht (-3)
if (this.grid[r + 1][c] === ZIEL) {
reachedExit = true;
// Block bleibt sichtbar auf der Öffnung stehen
this.grid[r + 1][c] = id;
} else {
// Box an neue Position setzen
this.grid[r + 1][c] = id;
}
// Alte Position leeren
this.grid[r][c] = LEER;
}
}
}
if (reachedExit) this.won = true;
}
/**
* Lässt alle Boxen fallen, bis sie stabil liegen.
* Wird synchron ausgeführt (ohne Animation), z.B. für KI-Vorberechnung.
* Wenn das Spiel bereits gewonnen ist, werden keine Blöcke mehr bewegt.
*/
relaxBoardSync() {
// Wenn das Spiel bereits gewonnen ist, nicht weiter fallen lassen
if (this.won) return;
let changed = true;
while (changed) {
changed = false;
let seen = new Set();
// Scan von unten nach oben
for (let r = this.rows - 2; r >= 0; r--) {
for (let c = 0; c < this.cols; c++) {
const id = this.grid[r][c];
// Wenn es eine Box ist (>=0), wir sie noch nicht bewegt haben und sie fallen kann
if (id >= 0 && !seen.has(id) && this.canFall(id)) {
this.moveDown(id);
seen.add(id);
changed = true;
}
}
}
}
}
/**
* Erstellt eine tiefe Kopie des aktuellen Boards.
* Die Kopie.
* @returns {RotateBoard}
*/
clone() {
const c = new RotateBoard(null);
c.rows = this.rows;
c.cols = this.cols;
c.won = this.won;
c.grid = this.grid.map(row => [...row]);
c.moves = this.moves;
return c;
}
// --- KI Interface Methoden ---
/**
* Generiert einen eindeutigen Schlüssel für den Zustand (für HashMaps).
* String-Repräsentation des Grids.
* @returns {string}
*/
getStateKey() {
return this.grid.map(r => r.join(',')).join('|');
}
/**
* Prüft, ob das Spiel gewonnen ist.
* @returns {boolean}
*/
isGoal() {
return this.won;
}
/**
* Liefert alle möglichen Nachfolgezustände.
* @returns {Array<{move: string, state: RotateBoard}>}
*/
getNextStates() {
if (this.won) return [];
// RotateBox hat immer zwei mögliche Züge: Links (L) und Rechts (R)
return ['L', 'R'].map(dir => {
const next = this.clone();
next.rotate(dir === 'R'); // Rotation ausführen
next.relaxBoardSync(); // Physik anwenden (Fallen)
return { move: dir, state: next };
});
}
}