pdo = $pdo; $this->ensureTableExists(); } /** * Upewnia się, że tabela settings_disciplines istnieje */ private function ensureTableExists() { $this->pdo->exec(" CREATE TABLE IF NOT EXISTS settings_disciplines ( id INT AUTO_INCREMENT PRIMARY KEY, discipline VARCHAR(50) NOT NULL UNIQUE, -- Reguły gry (logika) pointsToWin INT NOT NULL DEFAULT 10, setsToWin INT NOT NULL DEFAULT 2, serveRotation INT NOT NULL DEFAULT 2, specialRules TEXT, -- Personalizacja UI (nie wpływa na logiką gry) customization JSON, -- Versioning ustawień settingsVersion INT NOT NULL DEFAULT 1, -- Metadane created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_by INT, INDEX idx_discipline (discipline), INDEX idx_version (settingsVersion) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; "); } /** * Pobiera ustawienia dla dyscypliny * * @param string $discipline Nazwa dyscypliny (np. 'ping-pong') * @return array|null Ustawienia lub null jeśli nie istnieją */ public function getSettings($discipline) { $stmt = $this->pdo->prepare(" SELECT * FROM settings_disciplines WHERE discipline = :discipline LIMIT 1 "); $stmt->execute([':discipline' => $discipline]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!$row) { return null; } // Rzutuj INT kolumny $row['pointsToWin'] = (int)$row['pointsToWin']; $row['setsToWin'] = (int)$row['setsToWin']; $row['serveRotation'] = (int)$row['serveRotation']; $row['settingsVersion'] = (int)$row['settingsVersion']; $row['updated_by'] = $row['updated_by'] ? (int)$row['updated_by'] : null; // Dekoduj JSON fields if (!empty($row['customization'])) { $row['customization'] = json_decode($row['customization'], true); } return $row; } /** * Pobiera ustawienia z określonej wersji * (do snapshot'ów przy starcie meczu) * * @param string $discipline Nazwa dyscypliny * @param int $version Numer wersji * @return array|null Ustawienia danej wersji */ public function getSettingsByVersion($discipline, $version) { // TODO: W przyszłości można dodać tabelę settings_disciplines_history // dla pełnej historii zmian $stmt = $this->pdo->prepare(" SELECT * FROM settings_disciplines WHERE discipline = :discipline AND settingsVersion = :version LIMIT 1 "); $stmt->execute([ ':discipline' => $discipline, ':version' => (int)$version ]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!$row) { return null; } // Rzutuj INT kolumny $row['pointsToWin'] = (int)$row['pointsToWin']; $row['setsToWin'] = (int)$row['setsToWin']; $row['serveRotation'] = (int)$row['serveRotation']; $row['settingsVersion'] = (int)$row['settingsVersion']; $row['updated_by'] = $row['updated_by'] ? (int)$row['updated_by'] : null; if (!empty($row['customization'])) { $row['customization'] = json_decode($row['customization'], true); } return $row; } /** * Aktualizuje ustawienia dla dyscypliny * Automatycznie zwiększa versioning * * @param string $discipline Nazwa dyscypliny * @param array $settings Nowe ustawienia * @param int $userId ID użytkownika wykonującego zmianę * @return array Zaktualizowane ustawienia * @throws Exception */ public function updateSettings($discipline, array $settings, $userId) { // Waliduj dane $this->validateSettingsInput($settings); // Pobierz obecne ustawienia, aby zwiększyć versioning $current = $this->getSettings($discipline); $newVersion = ($current ? (int)$current['settingsVersion'] + 1 : 1); // Przygotuj dane do insertu/update $data = [ ':discipline' => $discipline, ':pointsToWin' => (int)$settings['pointsToWin'], ':setsToWin' => (int)$settings['setsToWin'], ':serveRotation' => (int)$settings['serveRotation'], ':specialRules' => $settings['specialRules'] ?? null, ':customization' => !empty($settings['customization']) ? json_encode($settings['customization'], JSON_UNESCAPED_UNICODE) : null, ':settingsVersion' => $newVersion, ':updated_by' => $userId ]; $this->pdo->beginTransaction(); try { if ($current) { // UPDATE $stmt = $this->pdo->prepare(" UPDATE settings_disciplines SET pointsToWin = :pointsToWin, setsToWin = :setsToWin, serveRotation = :serveRotation, specialRules = :specialRules, customization = :customization, settingsVersion = :settingsVersion, updated_by = :updated_by, updated_at = NOW() WHERE discipline = :discipline "); } else { // INSERT (nowa dyscyplina) $stmt = $this->pdo->prepare(" INSERT INTO settings_disciplines ( discipline, pointsToWin, setsToWin, serveRotation, specialRules, customization, settingsVersion, updated_by ) VALUES ( :discipline, :pointsToWin, :setsToWin, :serveRotation, :specialRules, :customization, :settingsVersion, :updated_by ) "); } $stmt->execute($data); $result = $this->getSettings($discipline); $this->pdo->commit(); return $result; } catch (Exception $e) { $this->pdo->rollBack(); throw $e; } } /** * Waliduje dane wejściowe ustawień * * @param array $settings Ustawienia do walidacji * @throws InvalidArgumentException */ private function validateSettingsInput(array $settings) { $errors = []; // Walidacja pointsToWin if (!isset($settings['pointsToWin'])) { $errors[] = 'pointsToWin is required'; } else { $ptw = (int)$settings['pointsToWin']; if ($ptw < 1 || $ptw > 100) { $errors[] = 'pointsToWin must be between 1 and 100'; } } // Walidacja setsToWin if (!isset($settings['setsToWin'])) { $errors[] = 'setsToWin is required'; } else { $stw = (int)$settings['setsToWin']; if ($stw < 1 || $stw > 100) { $errors[] = 'setsToWin must be between 1 and 100'; } } // Walidacja serveRotation if (!isset($settings['serveRotation'])) { $errors[] = 'serveRotation is required'; } else { $sr = (int)$settings['serveRotation']; if ($sr < 1 || $sr > 50) { $errors[] = 'serveRotation must be between 1 and 50'; } } // Walidacja specialRules (opcjonalne, ale jeśli podane to string) if (isset($settings['specialRules']) && !is_string($settings['specialRules'])) { $errors[] = 'specialRules must be a string'; } // Walidacja customization (opcjonalne, ale jeśli podane to musi być array/object) if (isset($settings['customization'])) { if (!is_array($settings['customization']) && !is_object($settings['customization'])) { $errors[] = 'customization must be an object/array'; } } // Logika biznesowa // Remis - wymuszenie override reguł if (isset($settings['pointsToWin']) && isset($settings['setsToWin'])) { $ptw = (int)$settings['pointsToWin']; $stw = (int)$settings['setsToWin']; // Jeśli oba są parzyste, możliwy jest remis - lepiej wymusić nieparzyste // W przypadku remisu w ostatnim secie, gracze muszą grać dalej if ($ptw % 2 === 0 || $stw % 2 === 0) { $errors[] = 'pointsToWin and setsToWin should be odd numbers to avoid draws in final set'; } } if (!empty($errors)) { throw new InvalidArgumentException(implode('; ', $errors)); } } /** * Zwraca domyślne ustawienia dla dyscypliny * * @param string $discipline Nazwa dyscypliny * @return array Domyślne ustawienia */ public static function getDefaults($discipline = 'ping-pong') { $defaults = [ 'ping-pong' => [ 'pointsToWin' => 11, 'setsToWin' => 3, 'serveRotation' => 2, 'specialRules' => 'Deuce at 10-10 (play until 2 points ahead)', 'customization' => [ 'tableColor' => '#2d5016', 'ballColor' => '#ff6600', 'paddleColor' => '#000000', 'uiTheme' => 'dark' ] ], 'rock-paper-scissors' => [ 'pointsToWin' => 5, 'setsToWin' => 1, 'serveRotation' => 1, 'specialRules' => 'Best of 1, instant rounds', 'customization' => [ 'animationSpeed' => 'fast', 'uiTheme' => 'light' ] ], 'table-football' => [ 'pointsToWin' => 5, 'setsToWin' => 1, 'serveRotation' => 3, 'specialRules' => 'Standard foosball rules, auto-restart after goal', 'customization' => [ 'tableColor' => '#000000', 'figureColor' => '#ffffff', 'uiTheme' => 'dark' ] ] ]; return $defaults[$discipline] ?? $defaults['ping-pong']; } /** * Inicjalizuje ustawienia dla dyscypliny (jeśli nie istnieją) * * @param string $discipline Nazwa dyscypliny * @param int $userId ID administratora */ public function initializeIfNotExists($discipline, $userId) { if (!$this->getSettings($discipline)) { $defaults = self::getDefaults($discipline); $this->updateSettings($discipline, $defaults, $userId); } } /** * Pobiera snapshot ustawień dla meczu * (snapshot to kopia ustawień w momencie startu meczu) * * @param string $discipline Nazwa dyscypliny * @param int $version Opcjonalnie: wersja ustawień. Jeśli null, bierze najnowsze. * @return array Snapshot do zapisania w meczu */ public function getSnapshot($discipline, $version = null) { if ($version !== null) { $settings = $this->getSettingsByVersion($discipline, (int)$version); } else { $settings = $this->getSettings($discipline); } if (!$settings) { throw new RuntimeException("Settings not found for discipline: $discipline"); } return [ 'discipline' => $discipline, 'settingsVersion' => (int)$settings['settingsVersion'], 'rules' => [ 'pointsToWin' => (int)$settings['pointsToWin'], 'setsToWin' => (int)$settings['setsToWin'], 'serveRotation' => (int)$settings['serveRotation'], 'specialRules' => $settings['specialRules'] ], 'snapshotTimestamp' => $settings['updated_at'] ]; } /** * Pobiera historię zmian dla dyscypliny * (przydatne do debuggowania i audytu) * * @param string $discipline Nazwa dyscypliny * @return array Historia */ public function getHistory($discipline) { // TODO: W przyszłości należy dodać tabelę settings_disciplines_history // Po prostu zwracamy obecne dane z metadata $current = $this->getSettings($discipline); if (!$current) { return []; } return [ [ 'version' => $current['settingsVersion'], 'updated_at' => $current['updated_at'], 'updated_by' => $current['updated_by'], 'changes' => 'Latest version' ] ]; } /** * Czyści ustawienia dyscypliny z bazy (dla testów) * * @param string $discipline Nazwa dyscypliny do usunięcia * @return bool True jeśli usunięto */ public function deleteSettings($discipline) { try { $stmt = $this->pdo->prepare("DELETE FROM settings_disciplines WHERE discipline = ?"); return $stmt->execute([$discipline]); } catch (Exception $e) { return false; } } } ?>