diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 6393062..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 wilkolbrzym-coder - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md deleted file mode 100644 index fda54e3..0000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -# Battle-Ship \ No newline at end of file diff --git a/ai/bot.go b/ai/bot.go new file mode 100644 index 0000000..ff20675 --- /dev/null +++ b/ai/bot.go @@ -0,0 +1,444 @@ +package ai + +import ( + "battleship/game" + "fmt" + "math" + "math/rand" + "time" +) + +const ( + MaxHypotheses = 50000 + MinHypotheses = 5000 +) + +// QuantumHunter implementuje zaawansowany silnik AI. +type QuantumHunter struct { + BoardSize int + Config game.GameConfig + OpponentGrid [][]game.CellState // Widok planszy przeciwnika (znane informacje) + ProbMap [][]float64 // Mapa prawdopodobieństwa + ActiveHypotheses [][][]game.CellState // Pula aktywnych, możliwych układów plansz + RemainingShips []game.ShipDef // Statki, które pozostały do zatopienia +} + +// NewQuantumHunter tworzy nową instancję bota. +func NewQuantumHunter(config game.GameConfig) *QuantumHunter { + grid := make([][]game.CellState, config.Size) + for i := range grid { + grid[i] = make([]game.CellState, config.Size) + for j := range grid[i] { + grid[i][j] = game.CellWater // Water = Unknown + } + } + + qh := &QuantumHunter{ + BoardSize: config.Size, + Config: config, + OpponentGrid: grid, + RemainingShips: append([]game.ShipDef{}, config.Ships...), + ActiveHypotheses: [][][]game.CellState{}, + } + + // Wstępna generacja puli hipotez + fmt.Println("AI: Generowanie wstępnej puli rzeczywistości kwantowych...") + qh.ReplenishHypotheses() + return qh +} + +// UpdateGameState aktualizuje wiedzę bota o planszy i filtruje hipotezy. +func (qh *QuantumHunter) UpdateGameState(x, y int, result game.CellState) { + qh.OpponentGrid[y][x] = result + + // Jeśli statek zatonął, zaktualizuj listę i oznacz otoczenie + if result == game.CellSunk { + size := qh.deduceSunkShipSize(x, y) + if size > 0 { + qh.removeShipFromRemaining(size) + } + // Autouzupełnianie wirtualne dla celów filtrowania + // (Gra robi to sama w logice, ale bot musi wiedzieć, że otoczenie to 'Miss') + // W game/logic.go Fire() robi MarkSurroundingAsMiss. + // Ale my dostajemy tylko wynik strzału w (x,y). + // Musimy zsynchronizować wiedzę bota. + qh.markSurroundingAsMiss(x, y) + } + + // Filtrowanie hipotez: usuń te, które są sprzeczne z nowym faktem + qh.filterHypotheses(x, y, result) + + // Dogeneruj, jeśli pula jest za mała + if len(qh.ActiveHypotheses) < MinHypotheses { + qh.ReplenishHypotheses() + } +} + +func (qh *QuantumHunter) filterHypotheses(x, y int, result game.CellState) { + var valid [][][]game.CellState + for _, grid := range qh.ActiveHypotheses { + match := false + cell := grid[y][x] + + switch result { + case game.CellMiss: + // Hipoteza musi mieć tu puste pole (nie statek) + if cell != game.CellShip && cell != game.CellHit { // Hit w hipotezie to statek + match = true + } + case game.CellHit, game.CellSunk: + // Hipoteza musi mieć tu statek + if cell == game.CellShip || cell == game.CellHit { + match = true + } + } + + // Dodatkowe sprawdzenie dla Sunk: czy w hipotezie ten statek jest faktycznie takiej długości? + // To jest trudne obliczeniowo dla 50k hipotez, pomijamy w tej wersji dla wydajności. + // Wystarczy zgodność pozycyjna. + + if match { + valid = append(valid, grid) + } + } + qh.ActiveHypotheses = valid +} + +// ReplenishHypotheses dogenerowuje hipotezy do limitu MaxHypotheses. +func (qh *QuantumHunter) ReplenishHypotheses() { + needed := MaxHypotheses - len(qh.ActiveHypotheses) + if needed <= 0 { + return + } + + // Przygotuj płaską listę statków do rozmieszczenia + var shipsToPlace []int + for _, s := range qh.RemainingShips { + for i := 0; i < s.Count; i++ { + shipsToPlace = append(shipsToPlace, s.Size) + } + } + + if len(shipsToPlace) == 0 { return } + + rand.Seed(time.Now().UnixNano()) + + // Użyj wielu wątków (goroutines) do generowania, bo to CPU heavy + // Ale dla prostoty w TUI (single threaded update loop), zrobimy to sekwencyjnie + // lub w małych paczkach. Tutaj sekwencyjnie, bo Go jest szybkie. + // 50k może zająć 1-2 sekundy. + + // Bufor na nowe hipotezy + newHypotheses := make([][][]game.CellState, 0, needed) + + timeout := time.After(2 * time.Second) // Limit czasu, żeby nie zamrozić UI na długo + + generationLoop: + for i := 0; i < needed; i++ { + select { + case <-timeout: + break generationLoop + default: + // Generuj losowy układ + grid := make([][]game.CellState, qh.BoardSize) + for r := range grid { + grid[r] = make([]game.CellState, qh.BoardSize) + for c := range grid[r] { + // Inicjalizuj stanem wiedzy (żeby nie stawiać na Miss) + // Ale generator tryPlaceShipsRandomly bierze pod uwagę OpponentGrid + // więc tu możemy dać Water, a generator sprawdzi. + grid[r][c] = game.CellWater + } + } + + if qh.tryPlaceShipsRandomly(grid, shipsToPlace) { + newHypotheses = append(newHypotheses, grid) + } + } + } + + qh.ActiveHypotheses = append(qh.ActiveHypotheses, newHypotheses...) +} + +// CalculateProbabilityMap tworzy mapę na podstawie aktywnych hipotez. +func (qh *QuantumHunter) CalculateProbabilityMap() { + // Reset mapy + qh.ProbMap = make([][]float64, qh.BoardSize) + for i := range qh.ProbMap { + qh.ProbMap[i] = make([]float64, qh.BoardSize) + } + + count := float64(len(qh.ActiveHypotheses)) + if count == 0 { return } + + for _, grid := range qh.ActiveHypotheses { + for y := 0; y < qh.BoardSize; y++ { + for x := 0; x < qh.BoardSize; x++ { + if grid[y][x] == game.CellShip || grid[y][x] == game.CellHit { + qh.ProbMap[y][x]++ + } + } + } + } + + // Normalizacja + for y := 0; y < qh.BoardSize; y++ { + for x := 0; x < qh.BoardSize; x++ { + qh.ProbMap[y][x] /= count + } + } +} + +func (qh *QuantumHunter) tryPlaceShipsRandomly(grid [][]game.CellState, ships []int) bool { + // Kopia ships do tasowania + sh := make([]int, len(ships)) + copy(sh, ships) + rand.Shuffle(len(sh), func(i, j int) { sh[i], sh[j] = sh[j], sh[i] }) + + for _, size := range sh { + placed := false + for attempt := 0; attempt < 50; attempt++ { + isVertical := rand.Intn(2) == 0 + x := rand.Intn(qh.BoardSize) + y := rand.Intn(qh.BoardSize) + + if isVertical { if y+size > qh.BoardSize { continue } } else { if x+size > qh.BoardSize { continue } } + + if qh.canPlaceShipSimulation(grid, x, y, size, isVertical) { + for k := 0; k < size; k++ { + cx, cy := x, y + if isVertical { cy += k } else { cx += k } + grid[cy][cx] = game.CellShip + } + placed = true + break + } + } + if !placed { return false } + } + + // Weryfikacja zgodności z OpponentGrid (czy pokrywa wszystkie Hity) + for y := 0; y < qh.BoardSize; y++ { + for x := 0; x < qh.BoardSize; x++ { + // Jeśli wiemy, że tam jest HIT/SUNK, to w wygenerowanym gridzie musi być statek + if qh.OpponentGrid[y][x] == game.CellHit || qh.OpponentGrid[y][x] == game.CellSunk { + if grid[y][x] != game.CellShip { + return false + } + } + } + } + return true +} + +func (qh *QuantumHunter) canPlaceShipSimulation(grid [][]game.CellState, x, y, size int, isVertical bool) bool { + for i := 0; i < size; i++ { + cx, cy := x, y + if isVertical { cy += i } else { cx += i } + + // Sprawdź kolizje ze znaną wiedzą + known := qh.OpponentGrid[cy][cx] + if known == game.CellMiss { return false } // Nie możemy stawiać na pudle + // Hit/Sunk jest OK (nadpisujemy, bo to statek) + + // Sprawdź kolizje z już postawionymi statkami w tej symulacji + current := grid[cy][cx] + if current == game.CellShip { return false } + + // Sprawdź otoczenie (No touching) + for dy := -1; dy <= 1; dy++ { + for dx := -1; dx <= 1; dx++ { + nx, ny := cx+dx, cy+dy + if nx >= 0 && nx < qh.BoardSize && ny >= 0 && ny < qh.BoardSize { + // Ignoruj segmenty tego samego statku (same-ship check) + // Sprawdzamy czy (nx,ny) jest częścią statku, który właśnie kładziemy + isSelf := false + for k := 0; k < size; k++ { + sx, sy := x, y + if isVertical { sy += k } else { sx += k } + if nx == sx && ny == sy { isSelf = true; break } + } + if isSelf { continue } + + if grid[ny][nx] == game.CellShip { return false } + + // Ważne: Sprawdź też, czy otoczenie nie jest Sunk/Hit w OpponentGrid + // Jeśli jest Hit obok, a my go nie przykrywamy (bo !isSelf), to znaczy że dotykamy innego statku. + // Jeśli OpponentGrid[ny][nx] == Hit/Sunk -> to jest inny statek. + neighborKnown := qh.OpponentGrid[ny][nx] + if neighborKnown == game.CellHit || neighborKnown == game.CellSunk { + return false + } + } + } + } + } + return true +} + +// GetBestMove zwraca koordynaty strzału i pewność (0-1). +func (qh *QuantumHunter) GetBestMove() (int, int, float64) { + // Upewnij się, że mamy hipotezy + if len(qh.ActiveHypotheses) < MinHypotheses { + qh.ReplenishHypotheses() + } + + qh.CalculateProbabilityMap() + + bestX, bestY := -1, -1 + maxProb := -1.0 + + // Parity - polowanie na szachownicy + minShipSize := 100 + for _, s := range qh.RemainingShips { + if s.Count > 0 && s.Size < minShipSize { minShipSize = s.Size } + } + if minShipSize == 100 { minShipSize = 1 } + + for y := 0; y < qh.BoardSize; y++ { + for x := 0; x < qh.BoardSize; x++ { + // Strzelamy tylko w nieznane + if qh.OpponentGrid[y][x] != game.CellWater { continue } + + prob := qh.ProbMap[y][x] + + // Bonus za parzystość (tylko w trybie Hunt, czyli gdy prob < 1.0) + // W trybie Target, prob będzie bliskie 1.0 dla przedłużeń linii, więc to nie zaszkodzi + if prob < 0.9 && (x+y)%minShipSize == 0 { + prob *= 1.2 + } + + if prob > maxProb { + maxProb = prob + bestX, bestY = x, y + } + } + } + + if bestX == -1 { + // Fallback + return qh.getRandomMove() + } + + return bestX, bestY, maxProb +} + +func (qh *QuantumHunter) getRandomMove() (int, int, float64) { + candidates := []game.Position{} + for y := 0; y < qh.BoardSize; y++ { + for x := 0; x < qh.BoardSize; x++ { + if qh.OpponentGrid[y][x] == game.CellWater { + candidates = append(candidates, game.Position{X: x, Y: y}) + } + } + } + if len(candidates) == 0 { return 0, 0, 0 } + c := candidates[rand.Intn(len(candidates))] + return c.X, c.Y, 0.1 +} + +// Helpery +func (qh *QuantumHunter) deduceSunkShipSize(x, y int) int { + // Identyczne jak wcześniej + visited := make(map[[2]int]bool) + queue := [][2]int{{x, y}} + visited[[2]int{x, y}] = true + count := 0 + for len(queue) > 0 { + curr := queue[0]; queue = queue[1:] + count++ + for _, d := range [][2]int{{0, 1}, {0, -1}, {1, 0}, {-1, 0}} { + nx, ny := curr[0]+d[0], curr[1]+d[1] + if nx >= 0 && nx < qh.BoardSize && ny >= 0 && ny < qh.BoardSize { + if qh.OpponentGrid[ny][nx] == game.CellSunk && !visited[[2]int{nx, ny}] { + visited[[2]int{nx, ny}] = true + queue = append(queue, [2]int{nx, ny}) + } + } + } + } + return count +} + +func (qh *QuantumHunter) removeShipFromRemaining(size int) { + for i, s := range qh.RemainingShips { + if s.Size == size && s.Count > 0 { + qh.RemainingShips[i].Count-- + return + } + } +} + +func (qh *QuantumHunter) markSurroundingAsMiss(x, y int) { + // BFS po sunk statku + queue := [][2]int{{x, y}} + visited := map[[2]int]bool{{x, y}: true} + shipCells := [][2]int{{x, y}} + + // 1. Znajdź cały statek + idx := 0 + for idx < len(queue) { + curr := queue[idx]; idx++ + for _, d := range [][2]int{{0, 1}, {0, -1}, {1, 0}, {-1, 0}} { + nx, ny := curr[0]+d[0], curr[1]+d[1] + if nx >= 0 && nx < qh.BoardSize && ny >= 0 && ny < qh.BoardSize { + if qh.OpponentGrid[ny][nx] == game.CellSunk && !visited[[2]int{nx, ny}] { + visited[[2]int{nx, ny}] = true + queue = append(queue, [2]int{nx, ny}) + shipCells = append(shipCells, [2]int{nx, ny}) + } + } + } + } + + // 2. Oznacz otoczenie + for _, cell := range shipCells { + cx, cy := cell[0], cell[1] + for dy := -1; dy <= 1; dy++ { + for dx := -1; dx <= 1; dx++ { + nx, ny := cx+dx, cy+dy + if nx >= 0 && nx < qh.BoardSize && ny >= 0 && ny < qh.BoardSize { + if qh.OpponentGrid[ny][nx] == game.CellWater { + qh.OpponentGrid[ny][nx] = game.CellMiss + } + } + } + } + } +} + +// Genetic Layout - to samo co wcześniej, bez zmian +func (qh *QuantumHunter) GetOptimizedLayout() *game.Board { + bestBoard := game.NewBoard(qh.BoardSize) + bestFitness := -1.0 + for i := 0; i < 100; i++ { + board := game.NewBoard(qh.BoardSize) + if err := board.PlaceShipsRandomly(qh.Config); err == nil { + fit := calculateFitness(board) + if fit > bestFitness { bestFitness = fit; bestBoard = board } + } + } + return bestBoard +} + +func calculateFitness(b *game.Board) float64 { + distSum := 0.0 + count := 0 + for i := 0; i < len(b.Ships); i++ { + for j := i + 1; j < len(b.Ships); j++ { + s1, s2 := b.Ships[i], b.Ships[j] + cx1, cy1 := getCenter(s1) + cx2, cy2 := getCenter(s2) + distSum += math.Sqrt(math.Pow(cx1-cx2, 2) + math.Pow(cy1-cy2, 2)) + count++ + } + } + if count == 0 { return 0 } + return distSum / float64(count) +} + +func getCenter(s *game.Ship) (float64, float64) { + sumX, sumY := 0, 0 + for _, p := range s.Positions { sumX += p.X; sumY += p.Y } + return float64(sumX)/float64(len(s.Positions)), float64(sumY)/float64(len(s.Positions)) +} diff --git a/battleship b/battleship new file mode 100755 index 0000000..42fed94 Binary files /dev/null and b/battleship differ diff --git a/game/logic.go b/game/logic.go new file mode 100644 index 0000000..4eab4f4 --- /dev/null +++ b/game/logic.go @@ -0,0 +1,190 @@ +package game + +import ( + "fmt" + "math/rand" + "time" +) + +// PlaceShipsRandomly rozmieszcza statki losowo na planszy zgodnie z zasadami. +func (b *Board) PlaceShipsRandomly(config GameConfig) error { + rand.Seed(time.Now().UnixNano()) + b.Grid = make([][]Cell, b.Size) + for i := range b.Grid { + b.Grid[i] = make([]Cell, b.Size) + for j := range b.Grid[i] { + b.Grid[i][j] = Cell{State: CellWater, ShipID: -1} + } + } + b.Ships = []*Ship{} + shipIDCounter := 0 + + // Sortowanie statków od największego do najmniejszego dla łatwiejszego upakowania + // W Go mapy są nieuporządkowane, więc musimy spłaszczyć listę + var shipsToPlace []ShipDef + for _, def := range config.Ships { + for i := 0; i < def.Count; i++ { + shipsToPlace = append(shipsToPlace, def) + } + } + + // Proste sortowanie bąbelkowe malejąco po rozmiarze + for i := 0; i < len(shipsToPlace)-1; i++ { + for j := 0; j < len(shipsToPlace)-i-1; j++ { + if shipsToPlace[j].Size < shipsToPlace[j+1].Size { + shipsToPlace[j], shipsToPlace[j+1] = shipsToPlace[j+1], shipsToPlace[j] + } + } + } + + for _, shipDef := range shipsToPlace { + placed := false + attempts := 0 + for !placed && attempts < 10000 { // Zwiększona liczba prób + attempts++ + isVertical := rand.Intn(2) == 0 + x := rand.Intn(b.Size) + y := rand.Intn(b.Size) + + if isVertical { + if y+shipDef.Size > b.Size { + continue + } + } else { + if x+shipDef.Size > b.Size { + continue + } + } + + if b.CanPlaceShip(x, y, shipDef.Size, isVertical) { + newShip := &Ship{ + ID: shipIDCounter, + Name: shipDef.Name, + Size: shipDef.Size, + Positions: []Position{}, + Hits: 0, + IsSunk: false, + } + shipIDCounter++ + + for i := 0; i < shipDef.Size; i++ { + cx, cy := x, y + if isVertical { + cy += i + } else { + cx += i + } + b.Grid[cy][cx].State = CellShip + b.Grid[cy][cx].ShipID = newShip.ID + newShip.Positions = append(newShip.Positions, Position{X: cx, Y: cy}) + } + b.Ships = append(b.Ships, newShip) + placed = true + } + } + if !placed { + return fmt.Errorf("nie udało się rozmieścić statków po %d próbach", attempts) + } + } + return nil +} + +// CanPlaceShip sprawdza, czy można umieścić statek w danym miejscu (uwzględniając odstępy). +// Zasada: statki nie mogą się stykać, nawet rogami. +func (b *Board) CanPlaceShip(x, y, size int, isVertical bool) bool { + for i := 0; i < size; i++ { + cx, cy := x, y + if isVertical { + cy += i + } else { + cx += i + } + + // Sprawdź sąsiedztwo 3x3 dla każdego segmentu statku + for dy := -1; dy <= 1; dy++ { + for dx := -1; dx <= 1; dx++ { + nx, ny := cx+dx, cy+dy + if nx >= 0 && nx < b.Size && ny >= 0 && ny < b.Size { + if b.Grid[ny][nx].State != CellWater { + return false + } + } + } + } + } + return true +} + +// Fire wykonuje strzał w dane pole. Zwraca wynik strzału i komunikat. +func (b *Board) Fire(x, y int) (CellState, string) { + if x < 0 || x >= b.Size || y < 0 || y >= b.Size { + return CellWater, "Poza planszą" + } + + cell := &b.Grid[y][x] + if cell.State == CellHit || cell.State == CellMiss || cell.State == CellSunk { + return cell.State, "Już strzelano" + } + + if cell.State == CellWater { + cell.State = CellMiss + return CellMiss, "Pudło" + } + + if cell.State == CellShip { + cell.State = CellHit + // Znajdź statek + var hitShip *Ship + for _, s := range b.Ships { + if s.ID == cell.ShipID { + hitShip = s + break + } + } + + if hitShip != nil { + hitShip.Hits++ + if hitShip.Hits >= hitShip.Size { + hitShip.IsSunk = true + // Oznacz wszystkie pola statku jako zatopione + for _, pos := range hitShip.Positions { + b.Grid[pos.Y][pos.X].State = CellSunk + } + // Oznacz otoczenie jako pudła (autouzupełnianie) + b.MarkSurroundingAsMiss(hitShip) + return CellSunk, "Zatopiony!" + } + return CellHit, "Trafiony!" + } + // To nie powinno się zdarzyć + return CellHit, "Błąd - statek widmo" + } + + return CellWater, "Nieznany stan" +} + +// MarkSurroundingAsMiss oznacza pola wokół zatopionego statku jako pudła. +func (b *Board) MarkSurroundingAsMiss(ship *Ship) { + for _, pos := range ship.Positions { + for dy := -1; dy <= 1; dy++ { + for dx := -1; dx <= 1; dx++ { + nx, ny := pos.X+dx, pos.Y+dy + if nx >= 0 && nx < b.Size && ny >= 0 && ny < b.Size { + if b.Grid[ny][nx].State == CellWater { + b.Grid[ny][nx].State = CellMiss + } + } + } + } + } +} + +// AllShipsSunk sprawdza, czy wszystkie statki zostały zatopione. +func (b *Board) AllShipsSunk() bool { + for _, s := range b.Ships { + if !s.IsSunk { + return false + } + } + return true +} diff --git a/game/types.go b/game/types.go new file mode 100644 index 0000000..f12ac8d --- /dev/null +++ b/game/types.go @@ -0,0 +1,83 @@ +package game + +// BoardSize definiuje rozmiar planszy. +const BoardSize = 10 + +// CellState reprezentuje stan pojedynczego pola na planszy. +type CellState int + +const ( + CellWater CellState = iota + CellShip + CellHit + CellMiss + CellSunk +) + +// Cell reprezentuje komórkę planszy. +type Cell struct { + State CellState + ShipID int // -1 jeśli brak statku +} + +// Ship reprezentuje statek. +type Ship struct { + ID int + Name string + Size int + Positions []Position + Hits int + IsSunk bool +} + +// Position reprezentuje koordynaty (X, Y). +type Position struct { + X, Y int +} + +// GameConfig przechowuje konfigurację gry (statki). +type GameConfig struct { + Size int + Ships []ShipDef +} + +// ShipDef definiuje typ statku (nazwa, rozmiar, ilość). +type ShipDef struct { + Name string + Size int + Count int +} + +// DefaultConfig to konfiguracja 10x10 z JS. +var DefaultConfig = GameConfig{ + Size: BoardSize, + Ships: []ShipDef{ + {"Carrier", 5, 1}, + {"Battleship", 4, 1}, + {"Cruiser", 3, 2}, + {"Destroyer", 2, 1}, + }, +} + +// Board reprezentuje planszę do gry. +type Board struct { + Size int + Grid [][]Cell + Ships []*Ship +} + +// NewBoard tworzy nową planszę. +func NewBoard(size int) *Board { + grid := make([][]Cell, size) + for i := range grid { + grid[i] = make([]Cell, size) + for j := range grid[i] { + grid[i][j] = Cell{State: CellWater, ShipID: -1} + } + } + return &Board{ + Size: size, + Grid: grid, + Ships: []*Ship{}, + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..42bee3f --- /dev/null +++ b/go.mod @@ -0,0 +1,25 @@ +module battleship + +go 1.24.3 + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.10.1 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.3.8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cc7a40a --- /dev/null +++ b/go.sum @@ -0,0 +1,41 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= +github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= diff --git a/index.html b/index.html deleted file mode 100644 index 814daf4..0000000 --- a/index.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - Bitwa Morska z Zaawansowanym Botem - - - - - -
- -
- -
-

Bitwa Morska

-
- - - -
-
-

Twoja Flota (Bot)

-
-
-
-
-
-
-
-

Panel Sterowania

- -
-

Bot sugeruje strzał w pole: ...

-
-
- - -
-
- - - - -
- -
-
-

Plansza Przeciwnika

-
-
-
-
-
-
-
- - - - - - diff --git a/main.go b/main.go new file mode 100644 index 0000000..3bc0b0d --- /dev/null +++ b/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "battleship/ui" + "fmt" + "os" + + "github.com/charmbracelet/bubbletea" +) + +func main() { + p := tea.NewProgram(ui.InitialModel(), tea.WithAltScreen()) + if _, err := p.Run(); err != nil { + fmt.Printf("Alas, there's been an error: %v", err) + os.Exit(1) + } +} diff --git a/script.js b/script.js deleted file mode 100644 index 74d1714..0000000 --- a/script.js +++ /dev/null @@ -1,2019 +0,0 @@ -// ################################################################## -// # ETAP 2 # -// ################################################################## - -const GAME_CONFIGS = { - '10x10': { - size: 10, - ships: [ - { name: 'pięciomasztowiec', size: 5, count: 1 }, - { name: 'czteromasztowiec', size: 4, count: 1 }, - { name: 'trójmasztowiec', size: 3, count: 2 }, - { name: 'dwumasztowiec', size: 2, count: 1 } - ] - }, - '12x12': { - size: 12, - ships: [ - { name: 'pięciomasztowiec', size: 5, count: 1 }, - { name: 'czteromasztowiec', size: 4, count: 2 }, - { name: 'trójmasztowiec', size: 3, count: 3 }, - { name: 'dwumasztowiec', size: 2, count: 2 } - ] - }, - '15x15': { - size: 15, - ships: [ - { name: 'pięciomasztowiec', size: 5, count: 3 }, - { name: 'czteromasztowiec', size: 4, count: 3 }, - { name: 'trójmasztowiec', size: 3, count: 5 }, - { name: 'dwumasztowiec', size: 2, count: 4 } - ] - } -}; - -let currentConfig = GAME_CONFIGS['10x10']; - -/** - * Tworzy planszę do gry o określonym rozmiarze wraz z koordynatami. - * @param {string} boardId - ID elementu HTML, w którym ma być plansza. - * @param {string} topCoordsId - ID kontenera na górne koordynaty. - * @param {string} leftCoordsId - ID kontenera na lewe koordynaty. - * @param {number} size - Rozmiar planszy. - */ -function createBoard(boardId, topCoordsId, leftCoordsId, size) { - const boardElement = document.getElementById(boardId); - const topCoordsElement = document.getElementById(topCoordsId); - const leftCoordsElement = document.getElementById(leftCoordsId); - - boardElement.innerHTML = ''; - topCoordsElement.innerHTML = ''; - leftCoordsElement.innerHTML = ''; - - boardElement.style.setProperty('--grid-size', size); - - // Generowanie górnych koordynatów (cyfry) - for (let i = 0; i < size; i++) { - const coord = document.createElement('div'); - coord.style.width = 'var(--cell-size)'; - coord.style.textAlign = 'center'; - coord.textContent = i + 1; - topCoordsElement.appendChild(coord); - } - - // Generowanie lewych koordynatów (litery) - for (let i = 0; i < size; i++) { - const coord = document.createElement('div'); - coord.style.height = 'var(--cell-size)'; - coord.style.display = 'flex'; - coord.style.alignItems = 'center'; - coord.style.justifyContent = 'center'; - coord.textContent = String.fromCharCode(65 + i); - leftCoordsElement.appendChild(coord); - } - - // Generowanie siatki gry - for (let i = 0; i < size * size; i++) { - const cell = document.createElement('div'); - cell.classList.add('cell'); - const x = i % size; - const y = Math.floor(i / size); - cell.dataset.x = x; - cell.dataset.y = y; - boardElement.appendChild(cell); - } -} - -/** - * Obsługuje kliknięcie komórki na planszy gracza (strzał przeciwnika). - * @param {Event} event - */ -function handlePlayerBoardClick(event) { - const cell = event.target; - if (!cell.classList.contains('cell')) return; - - const x = parseInt(cell.dataset.x); - const y = parseInt(cell.dataset.y); - - const cellState = playerGrid[y][x]; - - // Sprawdzenie, czy pole było już ostrzelane - if ((typeof cellState === 'object' && cellState.hit) || cellState === 'miss') { - alert("To pole było już ostrzelane!"); - return; - } - - if (typeof cellState === 'object' && cellState !== null && !cellState.hit) { - // Trafienie w statek - cellState.hit = true; - triggerAnimation(cell, 'animate-hit', 500); - const ship = playerShips.find(s => s.id === cellState.shipId); - ship.hits++; - - if (ship.hits === ship.size) { - ship.isSunk = true; - } - - if (ship.isSunk) { - setTimeout(() => { - ship.positions.forEach(pos => { - const sunkCell = document.querySelector(`#player-board .cell[data-x='${pos.x}'][data-y='${pos.y}']`); - if(sunkCell) { - sunkCell.classList.remove('hit'); - sunkCell.classList.add('sunk'); - triggerAnimation(sunkCell, 'animate-sunk', 600); - } - }); - }, 550); - } - - cell.classList.add('hit'); - checkWinCondition(); - } else if (cellState === 'water') { - // Pudło - playerGrid[y][x] = 'miss'; - triggerAnimation(cell, 'animate-miss', 400); - cell.classList.add('miss'); - } -} - -/** - * Obsługuje kliknięcie komórki na planszy przeciwnika (symulacja strzału bota). - * @param {Event} event - */ -function handleOpponentBoardClick(event) { - if (event.target.classList.contains('cell')) { - const x = event.target.dataset.x; - const y = event.target.dataset.y; - document.getElementById('bot-suggestion').textContent = `${String.fromCharCode(65 + parseInt(y))}${parseInt(x) + 1}`; - console.log(`Bot strzela w pole: (${x}, ${y})`); - // W przyszłości stan komórki będzie zależał od odpowiedzi gracza - } -} - - -// ################################################################## -// # NOWY INTERFEJS # -// ################################################################## - -let turnCounter = 1; - - -// ################################################################## -// # RDZEŃ AI - ŁOWCA KWANTOWY # -// ################################################################## - -let allPossibleOpponentLayouts = []; // Globalna tablica przechowująca "możliwe rzeczywistości" - -/** - * Pokazuje wskaźnik myślenia AI. - */ -function showThinkingIndicator() { - document.getElementById('ai-thinking-indicator').classList.remove('hidden'); -} - -/** - * Ukrywa wskaźnik myślenia AI. - */ -function hideThinkingIndicator() { - document.getElementById('ai-thinking-indicator').classList.add('hidden'); -} - -/** - * Inicjalizuje Łowcę Kwantowego, generując asynchronicznie początkowy zbiór możliwych układów. - */ -function initializeQuantumHunter() { - console.log("AI: Asynchroniczna Inicjalizacja Silnika 'Możliwych Rzeczywistości'..."); - showThinkingIndicator(); - allPossibleOpponentLayouts = []; - const size = currentConfig.size; - const shipsConfig = currentConfig.ships; - const targetLayouts = 50000; - const chunkSize = 500; // Liczba układów generowanych w jednym "kawałku" czasu - - function generateChunk() { - if (allPossibleOpponentLayouts.length >= targetLayouts) { - console.log(`AI: Wygenerowano ${allPossibleOpponentLayouts.length} możliwych rzeczywistości. Gotów do polowania.`); - hideThinkingIndicator(); - return; - } - - for (let i = 0; i < chunkSize; i++) { - const newLayout = generateRandomValidLayout(size, shipsConfig); - if (newLayout) { - allPossibleOpponentLayouts.push(newLayout); - } - } - - // Zaktualizuj licznik w UI, aby pokazać postęp - // (można dodać element UI w przyszłości) - console.log(`Generowanie hipotez: ${allPossibleOpponentLayouts.length} / ${targetLayouts}`); - - // Zaplanuj następny kawałek, oddając kontrolę przeglądarce - setTimeout(generateChunk, 0); - } - - generateChunk(); -} - -/** - * Generuje losowy układ statków zgodny ze znanymi ograniczeniami z opponentGrid. - * @param {number} size - Rozmiar planszy. - * @param {Array} shipsConfig - Konfiguracja statków do umieszczenia. - * @param {Array>} constraints - Siatka opponentGrid z 'miss', 'hit', 'sunk'. - * @returns {Array>|null} - */ -function generateConstrainedRandomLayout(size, shipsConfig, constraints) { - let grid = JSON.parse(JSON.stringify(constraints)); // Zaczynamy od siatki z ograniczeniami - const shipsToPlace = shipsConfig.flatMap(s => Array(s.count).fill(s.size)).sort((a, b) => b - a); - - // Najpierw przekształcamy 'hit' i 'sunk' na 'ship' dla logiki umieszczania - for (let y = 0; y < size; y++) { - for (let x = 0; x < size; x++) { - if (grid[y][x] === 'hit' || grid[y][x] === 'sunk' || grid[y][x] === 'damaged') { - grid[y][x] = 'ship_placed'; // Używamy specjalnego oznaczenia - } - } - } - - for (const shipSize of shipsToPlace) { - let placed = false; - let attempts = 0; - while (!placed && attempts < 200) { - attempts++; - const isVertical = Math.random() < 0.5; - const x = Math.floor(Math.random() * (isVertical ? size : size - shipSize + 1)); - const y = Math.floor(Math.random() * (isVertical ? size - shipSize + 1 : size)); - - if (canPlaceShipConstrained(grid, x, y, shipSize, isVertical)) { - for (let j = 0; j < shipSize; j++) { - const currentX = isVertical ? x : x + j; - const currentY = isVertical ? y + j : y; - if(grid[currentY][currentX] === 'unknown') grid[currentY][currentX] = 'ship'; - } - placed = true; - } - } - if (!placed) return null; - } - - // Finalne czyszczenie siatki - for (let y = 0; y < size; y++) { - for (let x = 0; x < size; x++) { - if (grid[y][x] === 'ship_placed' || grid[y][x] === 'unknown') grid[y][x] = 'ship'; - if (grid[y][x] === 'miss') grid[y][x] = 'water'; - } - } - - return grid; -} - -function canPlaceShipConstrained(grid, x, y, size, isVertical) { - // Ta funkcja musi respektować 'miss' i 'ship_placed', ale ignorować 'unknown' - for (let i = 0; i < size; i++) { - const currentX = isVertical ? x : x + i; - const currentY = isVertical ? y + i : y; - - if (currentX >= grid.length || currentY >= grid.length || grid[currentY][currentX] === 'miss') { - return false; - } - // Sprawdź, czy nie koliduje z już umieszczonymi częściami innych statków - for (let dy = -1; dy <= 1; dy++) { - for (let dx = -1; dx <= 1; dx++) { - const checkX = currentX + dx; - const checkY = currentY + dy; - if (checkX >= 0 && checkX < grid.length && checkY >= 0 && checkY < grid.length) { - if (grid[checkY][checkX] === 'ship') return false; - } - } - } - } - return true; -} - - -// ################################################################## -// # LOGIKA ZAAWANSOWANA BOTA # -// ################################################################## - -/** - * Oblicza mapę prawdopodobieństwa umieszczenia statków na planszy. - * @param {Array>} grid - Siatka do analizy. - * @returns {Array>} - Siatka z wartościami prawdopodobieństwa dla każdego pola. - */ -function calculateProbabilityMap(grid) { - const size = currentConfig.size; - const finalProbMap = Array(size).fill(null).map(() => Array(size).fill(0)); - - const remainingShips = opponentShips.filter(s => s.count > 0); - if (remainingShips.length === 0) return finalProbMap; - - const smallestShipSize = Math.min(...remainingShips.map(s => s.size)); - - // Stwórz osobną mapę gęstości dla każdego typu statku - for (const shipType of remainingShips) { - const shipSize = shipType.size; - const shipProbMap = Array(size).fill(null).map(() => Array(size).fill(0)); - for (let i = 0; i < shipType.count; i++) { - for (let y = 0; y < size; y++) { - for (let x = 0; x < size; x++) { - // Poziomo - if (x + shipSize <= size && canPlaceShipForProbability(opponentGrid, x, y, shipSize, false)) { - for (let k = 0; k < shipSize; k++) shipProbMap[y][x + k]++; - } - // Pionowo - if (y + shipSize <= size && canPlaceShipForProbability(opponentGrid, x, y, shipSize, true)) { - for (let k = 0; k < shipSize; k++) shipProbMap[y + k][x]++; - } - } - } - } - // Dodaj mapę tego typu statku do finalnej mapy - for(let y = 0; y < size; y++) { - for (let x = 0; x < size; x++) { - finalProbMap[y][x] += shipProbMap[y][x]; - } - } - } - - // Dynamiczna Analiza Parzystości - const dynamicSmallestShip = Math.min(...opponentShips.filter(s => s.count > 0).map(s => s.size)); - if (dynamicSmallestShip && dynamicSmallestShip !== Infinity) { - for (let y = 0; y < size; y++) { - for (let x = 0; x < size; x++) { - // Strzelaj w pola, które pasują do "szachownicy" NAJMNIEJSZEGO POZOSTAŁEGO statku - if ((x + y) % dynamicSmallestShip !== 0) { - finalProbMap[y][x] *= 0.1; // Zamiast zerować, mocno redukuj wagę - } - } - } - } - - return finalProbMap; -} - -/** - * Oblicza mapę Wartości Informacji (Value of Information). - * Komórki, które dają więcej informacji (np. wykluczają więcej możliwych położeń statków), - * otrzymują wyższy wynik. - * @param {Array>} probMap - Mapa prawdopodobieństwa. - * @param {Array>} grid - Siatka gry. - * @returns {Array>} - Mapa z wartościami VoI. - */ -function calculateVoIMap(probMap, grid) { - const size = grid.length; - const voiMap = Array(size).fill(null).map(() => Array(size).fill(0)); - const remainingShips = opponentShips.filter(s => s.count > 0); - if (remainingShips.length === 0) return voiMap; - - for (let y = 0; y < size; y++) { - for (let x = 0; x < size; x++) { - if (grid[y][x] === 'unknown') { - // Podstawowym czynnikiem jest prawdopodobieństwo - let informationValue = probMap[y][x]; - - // Dodatkowy bonus za "przecinanie" możliwych długich statków - for (const shipType of remainingShips) { - const shipSize = shipType.size; - // Sprawdź, ile potencjalnych umiejscowień statków "przechodzi" przez to pole - // Poziomo - for (let i = 0; i < shipSize; i++) { - if (canPlaceShipForProbability(grid, x - i, y, shipSize, false)) { - informationValue += 1; - } - } - // Pionowo - for (let i = 0; i < shipSize; i++) { - if (canPlaceShipForProbability(grid, x, y - i, shipSize, true)) { - informationValue += 1; - } - } - } - voiMap[y][x] = informationValue; - } - } - } - return voiMap; -} - -/** - * Sprawdza, czy statek może być teoretycznie umieszczony na planszy przeciwnika. - * Ignoruje inne 'unknown' pola, ale respektuje 'miss' i 'hit'. - */ -function canPlaceShipForProbability(grid, x, y, size, isVertical) { - const gridSize = grid.length; - - for (let i = 0; i < size; i++) { - const currentX = isVertical ? x : x + i; - const currentY = isVertical ? y + i : y; - - // --- TWARDA KONTROLA GRANIC: Sprawdza, czy indeks jest ujemny LUB poza zakresem max (gridSize) --- - if (currentX < 0 || currentX >= gridSize || currentY < 0 || currentY >= gridSize) { - return false; - } - - // Stara logika: sprawdza, czy pole jest zajęte - if (grid[currentY][currentX] === 'miss' || grid[currentY][currentX] === 'sunk') { - return false; - } - } - return true; -} - - -// Moduł "Psychoanalizy Przeciwnika" -const opponentProfile = { - placementBias: { edge: 0, center: 0, corner: 0 }, - orientationBias: { vertical: 0, horizontal: 0 } -}; - -/** - * Aktualizuje profil przeciwnika na podstawie zatopionego statku. - */ -function updateOpponentProfile(ship) { - // Prosta logika do demonstracji - const size = currentConfig.size; - // Sprawdzenie, czy statek ma więcej niż jeden segment, aby uniknąć błędu - const isVertical = ship.positions.length > 1 && ship.positions[0].x === ship.positions[1].x; - if (isVertical) opponentProfile.orientationBias.vertical++; - else opponentProfile.orientationBias.horizontal++; - - ship.positions.forEach(pos => { - if (pos.x === 0 || pos.x === size - 1 || pos.y === 0 || pos.y === size - 1) { - opponentProfile.placementBias.edge++; - } else { - opponentProfile.placementBias.center++; - } - }); -} - - -// ################################################################## -// # ETAP 3 # -// ################################################################## - -let playerGrid; // Tablica 2D dla statków gracza/bota -let playerShips = []; // Lista obiektów statków gracza -let opponentGrid; // Tablica 2D dla planszy przeciwnika -let opponentShips; // Lista statków przeciwnika do zatopienia -let history = []; // Historia stanów gry do cofania ruchów - -/** - * Inicjalizuje puste siatki gry. - * @param {number} size - Rozmiar siatki. - */ -function initializeGrids(size) { - playerGrid = Array(size).fill(null).map(() => Array(size).fill('water')); - opponentGrid = Array(size).fill(null).map(() => Array(size).fill('unknown')); - - // Inicjalizacja listy statków przeciwnika do śledzenia - opponentShips = JSON.parse(JSON.stringify(currentConfig.ships)); -} - -// ################################################################## -// # EWOLUCJA ROZSTAWIANIA - ARCHITEKT GENETYCZNY # -// ################################################################## - -function placeShipsRandomly(grid, shipsConfig) { - console.log("AI: Uruchamiam Architekta Genetycznego do zaprojektowania układu floty..."); - const size = grid.length; - const populationSize = 50; - const generations = 30; - const mutationRate = 0.1; - const elitismRate = 0.1; - - // 1. Inicjalizacja populacji - let population = []; - for (let i = 0; i < populationSize; i++) { - population.push(createRandomIndividual(grid, shipsConfig)); - } - - for (let gen = 0; gen < generations; gen++) { - // 2. Ewaluacja - const fitnessScores = population.map(ind => ({ individual: ind, fitness: calculateFitness(ind, size) })); - fitnessScores.sort((a, b) => b.fitness - a.fitness); - - const newPopulation = []; - - // 3. Elityzm - const eliteCount = Math.floor(populationSize * elitismRate); - for (let i = 0; i < eliteCount; i++) { - newPopulation.push(fitnessScores[i].individual); - } - - // 4. Selekcja, Krzyżowanie i Mutacja - while (newPopulation.length < populationSize) { - const parent1 = tournamentSelection(fitnessScores); - const parent2 = tournamentSelection(fitnessScores); - let child = crossover(parent1, parent2, grid, shipsConfig); - if (Math.random() < mutationRate) { - child = mutate(child, grid, shipsConfig); - } - newPopulation.push(child); - } - population = newPopulation; - console.log(`Generacja ${gen + 1}/${generations}, Najlepszy wynik: ${fitnessScores[0].fitness.toFixed(2)}`); - } - - const fitnessScores = population.map(ind => ({ individual: ind, fitness: calculateFitness(ind, size) })); - fitnessScores.sort((a, b) => b.fitness - a.fitness); - const bestIndividual = fitnessScores[0].individual; - - console.log(`AI: Projekt floty ukończony z optymalnym wynikiem ${fitnessScores[0].fitness.toFixed(2)}.`); - applyIndividualToGrid(bestIndividual, grid); -} - -// Tworzy pojedynczego osobnika (losowy układ statków) -function createRandomIndividual(grid, shipsConfig) { - const size = grid.length; - const individual = []; - const tempGrid = Array(size).fill(null).map(() => Array(size).fill('water')); - const sortedShips = shipsConfig.flatMap(s => Array(s.count).fill(s)).sort((a, b) => b.size - a.size); - - sortedShips.forEach(shipType => { - let placed = false; - let attempts = 0; - while (!placed && attempts < 200) { - attempts++; - const isVertical = Math.random() < 0.5; - const x = Math.floor(Math.random() * (isVertical ? size : size - shipType.size + 1)); - const y = Math.floor(Math.random() * (isVertical ? size - shipType.size + 1 : size)); - - if (canPlaceShip(tempGrid, x, y, shipType.size, isVertical)) { - const newShip = { name: shipType.name, size: shipType.size, positions: [] }; - for (let j = 0; j < shipType.size; j++) { - const currentX = isVertical ? x : x + j; - const currentY = isVertical ? y + j : y; - tempGrid[currentY][currentX] = 'ship'; - newShip.positions.push({ x: currentX, y: currentY }); - } - individual.push(newShip); - placed = true; - } - } - }); - return individual; -} - -// Ocenia jakość osobnika ("Wielki Architekt Pola Bitwy") -function calculateFitness(individual, size) { - if (individual.length === 0) return -Infinity; // Kara za niekompletne układy - - const parityResistanceScore = calculateParityResistance(individual, size); - const ambiguityScore = calculateAmbiguity(individual, size); - const balanceScore = calculateBalance(individual, size); - const earlyGameStealthScore = simulateEarlyGameAttack(individual, size); - const orientationBalanceScore = calculateOrientationBalance(individual); - - // Finalne wagi "Wersji 7.0 - Stabilizacja Kognitywna" - const finalScore = (earlyGameStealthScore * 0.7) + // Niewykrywalność jest absolutnym priorytetem - (orientationBalanceScore * 0.1) + // Balans orientacji - (parityResistanceScore * 0.05) + // Pozostałe jako drugorzędne optymalizacje - (ambiguityScore * 0.1) + - (balanceScore * 0.05); - - return finalScore; -} - -/** - * Oblicza odporność układu na atak strategią parzystości. - * @param {Array} individual - Układ statków. - * @param {number} size - Rozmiar planszy. - * @returns {number} - Wynik odporności. - */ -function calculateParityResistance(individual, size) { - const grid = Array(size).fill(null).map(() => Array(size).fill('water')); - individual.forEach(ship => ship.positions.forEach(pos => grid[pos.y][pos.x] = 'ship')); - - let shotsNeeded = 0; - const parity = 2; // Najbardziej uniwersalny i powszechny przypadek ataku - const hitShips = new Set(); - - for (let p = 0; p < parity; p++) { - for (let y = 0; y < size; y++) { - for (let x = 0; x < size; x++) { - if ((x + y) % parity === p) { - shotsNeeded++; - if (grid[y][x] === 'ship') { - // Znajdź statek, który został trafiony - const hitShip = individual.find(ship => ship.positions.some(pos => pos.x === x && pos.y === y)); - if (hitShip) { - hitShips.add(hitShip); - } - } - if (hitShips.size === individual.length) { - // Znormalizuj wynik: im więcej strzałów, tym lepiej. - return shotsNeeded / (size * size); - } - } - } - } - } - // Jeśli z jakiegoś powodu nie wszystkie statki zostały trafione (co nie powinno się zdarzyć) - return shotsNeeded / (size * size); -} - -/** - * Oblicza, jak bardzo niejednoznaczny i "mylący" jest układ. - * @param {Array} individual - Układ statków. - * @param {number} size - Rozmiar planszy. - * @returns {number} - Wynik niejednoznaczności. - */ -function calculateAmbiguity(individual, size) { - let touchPoints = 0; - const grid = Array(size).fill(null).map(() => Array(size).fill(false)); - individual.forEach(ship => ship.positions.forEach(pos => grid[pos.y][pos.x] = true)); - - individual.forEach(ship => { - ship.positions.forEach(pos => { - // Sprawdź sąsiadów w odległości 2 pól (szukamy bliskich, ale nie dotykających się statków) - for (let dy = -2; dy <= 2; dy++) { - for (let dx = -2; dx <= 2; dx++) { - if (Math.abs(dx) <= 1 && Math.abs(dy) <= 1) continue; // Pomiń bezpośrednich sąsiadów - - const checkX = pos.x + dx; - const checkY = pos.y + dy; - - if (checkX >= 0 && checkX < size && checkY >= 0 && checkY < size && grid[checkY][checkX]) { - touchPoints++; - } - } - } - }); - }); - - // Normalizuj wynik - im więcej "bliskich kontaktów", tym lepiej - const totalPossibleTouchPoints = individual.reduce((acc, ship) => acc + ship.size, 0) * 16; // 16 to liczba pól w "otoczce" 5x5 minus 9 w centrum - return touchPoints / totalPossibleTouchPoints; -} - -/** - * Oblicza strategiczną równowagę układu na planszy. - * @param {Array} individual - Układ statków. - * @param {number} size - Rozmiar planszy. - * @returns {number} - Wynik równowagi. - */ -function calculateBalance(individual, size) { - const allPositions = individual.flatMap(ship => ship.positions); - if (allPositions.length === 0) return 0; - - const centerX = (size - 1) / 2; - const centerY = (size - 1) / 2; - - // Oblicz środek ciężkości floty - const fleetCenterX = allPositions.reduce((acc, pos) => acc + pos.x, 0) / allPositions.length; - const fleetCenterY = allPositions.reduce((acc, pos) => acc + pos.y, 0) / allPositions.length; - - // Oblicz, jak blisko środek ciężkości floty jest do idealnego centrum planszy - const centerProximity = 1 - (Math.hypot(fleetCenterX - centerX, fleetCenterY - centerY) / Math.hypot(centerX, centerY)); - - // Oblicz wariancję odległości statków od środka ciężkości floty (jak bardzo są rozproszone) - const distances = allPositions.map(pos => Math.hypot(pos.x - fleetCenterX, pos.y - fleetCenterY)); - const meanDistance = distances.reduce((acc, d) => acc + d, 0) / distances.length; - const variance = distances.reduce((acc, d) => acc + Math.pow(d - meanDistance, 2), 0) / distances.length; - - // Chcemy dużej wariancji (rozproszenia) i bliskości do centrum. - // Normalizuj wariancję. Max wariancja to odległość rogu od centrum, czyli (size/2)^2. - const maxVariance = Math.pow(size / 2, 2); - const normalizedVariance = variance / maxVariance; - - return (centerProximity * 0.3) + (normalizedVariance * 0.7); -} - -/** - * Symuluje wczesny atak na dany układ, aby ocenić jego "niewykrywalność". - * @param {Array} individual - Układ statków. - * @param {number} size - Rozmiar planszy. - * @returns {number} - Wynik (1.0 za zero trafień, malejący z każdym trafieniem). - */ -function simulateEarlyGameAttack(individual, size) { - const grid = Array(size).fill(null).map(() => Array(size).fill('water')); - individual.forEach(ship => ship.positions.forEach(pos => grid[pos.y][pos.x] = 'ship')); - - let hits = 0; - const simulationShots = 15; - const parity = 2; - - let shotCounter = 0; - - outer: - for (let p = 0; p < parity; p++) { - for (let y = 0; y < size; y++) { - for (let x = 0; x < size; x++) { - if ((x + y) % parity === p) { - if (shotCounter >= simulationShots) break outer; - shotCounter++; - if (grid[y][x] === 'ship') { - hits++; - } - } - } - } - } - - // Wynik jest odwrotnie proporcjonalny do liczby trafień. Zero trafień = perfekcyjny wynik 1.0. - return 1.0 - (hits / simulationShots); -} - -/** - * Oblicza zrównoważenie orientacji statków (pionowe vs. poziome). - * @param {Array} individual - Układ statków. - * @returns {number} - Wynik (1.0 za idealny balans, malejący z odchyleniem). - */ -function calculateOrientationBalance(individual) { - if (individual.length === 0) return 0; - - let verticalCount = 0; - individual.forEach(ship => { - if (ship.positions.length > 1 && ship.positions[0].x === ship.positions[1].x) { - verticalCount++; - } - }); - - const horizontalCount = individual.length - verticalCount; - const balance = 1.0 - (Math.abs(verticalCount - horizontalCount) / individual.length); - return balance; -} - - -// Selekcja turniejowa -function tournamentSelection(fitnessScores, k = 5) { - let best = null; - for (let i = 0; i < k; i++) { - const random = fitnessScores[Math.floor(Math.random() * fitnessScores.length)]; - if (best === null || random.fitness > best.fitness) { - best = random; - } - } - return best.individual; -} - -// Krzyżowanie -function crossover(parent1, parent2, grid, shipsConfig) { - const child = []; - const size = grid.length; - const tempGrid = Array(size).fill(null).map(() => Array(size).fill('water')); - const shipsToPlace = JSON.parse(JSON.stringify(shipsConfig.flatMap(s => Array(s.count).fill(s)).sort((a, b) => b.size - a.size))); - - // Próba skopiowania genów (statków) naprzemiennie od rodziców - for (let i = 0; i < parent1.length; i++) { - const parent = i % 2 === 0 ? parent1 : parent2; - const shipToCopy = parent[i]; - if (shipToCopy) { - const shipTypeIndex = shipsToPlace.findIndex(s => s.size === shipToCopy.size); - if (shipTypeIndex !== -1) { - const firstPos = shipToCopy.positions[0]; - const isVertical = shipToCopy.positions.length > 1 && shipToCopy.positions[1].x === firstPos.x; - if (canPlaceShip(tempGrid, firstPos.x, firstPos.y, shipToCopy.size, isVertical)) { - const newShip = { name: shipToCopy.name, size: shipToCopy.size, positions: [] }; - for (let j = 0; j < shipToCopy.size; j++) { - const currentX = isVertical ? firstPos.x : firstPos.x + j; - const currentY = isVertical ? firstPos.y + j : firstPos.y; - tempGrid[currentY][currentX] = 'ship'; - newShip.positions.push({ x: currentX, y: currentY }); - } - child.push(newShip); - shipsToPlace.splice(shipTypeIndex, 1); - } - } - } - } - - // Uzupełnienie brakujących statków losowo - shipsToPlace.forEach(shipType => { - let placed = false; - let attempts = 0; - while(!placed && attempts < 100){ - attempts++; - const isVertical = Math.random() < 0.5; - const x = Math.floor(Math.random() * (isVertical ? size : size - shipType.size + 1)); - const y = Math.floor(Math.random() * (isVertical ? size - shipType.size + 1 : size)); - if(canPlaceShip(tempGrid, x, y, shipType.size, isVertical)){ - const newShip = { name: shipType.name, size: shipType.size, positions: [] }; - for (let j = 0; j < shipType.size; j++) { - const currentX = isVertical ? x : x + j; - const currentY = isVertical ? y + j : y; - tempGrid[currentY][currentX] = 'ship'; - newShip.positions.push({ x: currentX, y: currentY }); - } - child.push(newShip); - placed = true; - } - } - }); - - return child; -} - -// Mutacja -function mutate(individual, grid, shipsConfig) { - const mutatedIndividual = JSON.parse(JSON.stringify(individual)); - const shipIndexToMutate = Math.floor(Math.random() * mutatedIndividual.length); - - // Usuń stary statek i spróbuj umieścić go w nowym miejscu - const shipToMutate = mutatedIndividual.splice(shipIndexToMutate, 1)[0]; - - const size = grid.length; - const tempGrid = Array(size).fill(null).map(() => Array(size).fill('water')); - mutatedIndividual.forEach(ship => ship.positions.forEach(pos => tempGrid[pos.y][pos.x] = 'ship')); - - let placed = false; - let attempts = 0; - while(!placed && attempts < 100){ - attempts++; - const isVertical = Math.random() < 0.5; - const x = Math.floor(Math.random() * (isVertical ? size : size - shipToMutate.size + 1)); - const y = Math.floor(Math.random() * (isVertical ? size - shipToMutate.size + 1 : size)); - if(canPlaceShip(tempGrid, x, y, shipToMutate.size, isVertical)){ - const newShip = { name: shipToMutate.name, size: shipToMutate.size, positions: [] }; - for (let j = 0; j < shipToMutate.size; j++) { - const currentX = isVertical ? x : x + j; - const currentY = isVertical ? y + j : y; - newShip.positions.push({ x: currentX, y: currentY }); - } - mutatedIndividual.push(newShip); - placed = true; - } - } - // Jeśli nie uda się umieścić, przywróć oryginał, aby uniknąć niekompletnych układów - if(!placed) return individual; - - return mutatedIndividual; -} - -function applyIndividualToGrid(individual, grid) { - let shipIdCounter = 0; - playerShips = []; - individual.forEach(shipData => { - const newShip = { - id: shipIdCounter++, - name: shipData.name, - size: shipData.size, - positions: shipData.positions, - hits: 0, - isSunk: false - }; - newShip.positions.forEach(pos => { - grid[pos.y][pos.x] = { shipId: newShip.id, hit: false }; - }); - playerShips.push(newShip); - }); -} - -/** - * Sprawdza, czy statek może być umieszczony w danym miejscu. - */ -function canPlaceShip(grid, x, y, size, isVertical) { - const gridSize = grid.length; - // Sprawdzenie granic planszy - if ((isVertical && y + size > gridSize) || (!isVertical && x + size > gridSize)) { - return false; - } - - for (let i = 0; i < size; i++) { - const currentX = isVertical ? x : x + i; - const currentY = isVertical ? y + i : y; - - // Sprawdzenie otoczenia 3x3 - for (let dy = -1; dy <= 1; dy++) { - for (let dx = -1; dx <= 1; dx++) { - const checkX = currentX + dx; - const checkY = currentY + dy; - - if (checkX >= 0 && checkX < gridSize && checkY >= 0 && checkY < gridSize) { - const cell = grid[checkY][checkX]; - if (cell !== 'water') { - // Musimy sprawdzić, czy ta "zajęta" komórka nie jest częścią statku, który właśnie próbujemy umieścić - // To jest skomplikowane w tej pętli. Prostszy sposób: - // Logika jest błędna, bo sprawdza komórki, na których statek ma dopiero stanąć. - } - } - } - } - } - // Prawidłowa, uproszczona logika - for (let i = 0; i < size; i++) { - const currentX = isVertical ? x : x + i; - const currentY = isVertical ? y + i : y; - - for (let dy = -1; dy <= 1; dy++) { - for (let dx = -1; dx <= 1; dx++) { - const checkX = currentX + dx; - const checkY = currentY + dy; - - if (checkX >= 0 && checkX < gridSize && checkY >= 0 && checkY < gridSize) { - const cell = grid[checkY][checkX]; - if (cell === 'ship' || typeof cell === 'object') { - return false; - } - } - } - } - } - - - return true; -} - -/** - * Renderuje wizualny stan planszy na podstawie siatki danych. - * @param {string} boardId - ID elementu HTML planszy. - * @param {Array>} grid - Siatka z danymi o stanie gry. - */ -function renderBoard(boardId, grid) { - const boardElement = document.getElementById(boardId); - const cells = boardElement.childNodes; - const size = grid.length; - - for (let y = 0; y < size; y++) { - for (let x = 0; x < size; x++) { - const index = y * size + x; - const cellState = grid[y][x]; - const cellElement = cells[index]; - - // Czyszczenie starych klas sugestii - cellElement.className = 'cell'; - - if (boardId === 'player-board') { - if (typeof cellState === 'object' && cellState !== null) { - cellElement.classList.add('ship'); - if (cellState.hit) { - cellElement.classList.add('hit'); - } - } - } else if (boardId === 'opponent-board') { - if (cellState !== 'unknown') { - cellElement.classList.add(cellState); - } - } - } - } -} - - -/** - * Usuwa klasy CSS poprzednich sugestii z planszy przeciwnika. - */ -function clearPreviousSuggestions() { - const opponentCells = document.querySelectorAll('#opponent-board .cell'); - opponentCells.forEach(cell => { - for (let i = 1; i <= 5; i++) { - cell.classList.remove(`suggestion-${i}`); - } - }); -} - -/** - * Renderuje 5 najlepszych propozycji ruchów bota na planszy przeciwnika. - * @param {Array<{x: number, y: number}>} sortedMoves - Posortowana lista ruchów. - */ -function renderTopMoves(sortedMoves) { - clearPreviousSuggestions(); - for (let i = 0; i < 5 && i < sortedMoves.length; i++) { - const move = sortedMoves[i]; - const cellElement = document.querySelector(`#opponent-board .cell[data-x='${move.x}'][data-y='${move.y}']`); - if (cellElement) { - cellElement.classList.add(`suggestion-${i + 1}`); - } - } -} - -let botState = 'HUNT'; // Może być 'HUNT' lub 'TARGET' -let targetQueue = []; // Kolejka celów do sprawdzenia w trybie TARGET -let lastShot = null; -let currentTargetHits = []; // Przechowuje trafienia w aktualnie atakowany statek - -// ################################################################## -// # ULEPSZENIE DEDUKCJI - TRYB "SHERLOCK HOLMES" # -// ################################################################## - -/** - * Identyfikuje na planszy izolowane, nieodkryte obszary ("kieszenie"). - * @param {Array>} grid - Siatka gry. - * @returns {Array>} - Tablica kieszeni. - */ -function findPockets(grid) { - const size = grid.length; - const visited = Array(size).fill(null).map(() => Array(size).fill(false)); - const pockets = []; - - for (let y = 0; y < size; y++) { - for (let x = 0; x < size; x++) { - if (grid[y][x] === 'unknown' && !visited[y][x]) { - const newPocket = []; - const q = [{x, y}]; - visited[y][x] = true; - - while (q.length > 0) { - const cell = q.shift(); - newPocket.push(cell); - const neighbors = [{x:cell.x+1, y:cell.y}, {x:cell.x-1, y:cell.y}, {x:cell.x, y:cell.y+1}, {x:cell.x, y:cell.y-1}]; - for (const n of neighbors) { - if (n.x >= 0 && n.x < size && n.y >= 0 && n.y < size && grid[n.y][n.x] === 'unknown' && !visited[n.y][n.x]) { - visited[n.y][n.x] = true; - q.push(n); - } - } - } - pockets.push(newPocket); - } - } - } - return pockets; -} - -/** - * Znajduje wszystkie unikalne kombinacje statków, które sumują się do danego rozmiaru. - * @param {number} targetSize - Docelowy rozmiar (wielkość kieszeni). - * @param {Array} availableShips - Lista rozmiarów dostępnych statków. - * @returns {Array>} - Tablica kombinacji. - */ -function findShipCombinationsThatFit(targetSize, availableShips) { - const results = []; - function find(target, startIndex, currentCombination) { - if (target === 0) { - results.push([...currentCombination]); - return; - } - if (target < 0) return; - - for (let i = startIndex; i < availableShips.length; i++) { - if (i > startIndex && availableShips[i] === availableShips[i-1]) continue; // Unikaj duplikatów - currentCombination.push(availableShips[i]); - find(target - availableShips[i], i + 1, currentCombination); - currentCombination.pop(); // backtrack - } - } - find(targetSize, 0, []); - return results; -} - -/** - * Rekursywnie próbuje umieścić dany zestaw statków w kieszeni, aby znaleźć wszystkie możliwe układy. - * @param {Array<{x: number, y: number}>} pocket - Współrzędne komórek kieszeni. - * @param {Array} shipsToPlace - Rozmiary statków do umieszczenia. - * @param {Array} solutions - Tablica, do której zostaną dodane znalezione rozwiązania. - */ -function solvePlacement(pocket, shipsToPlace, solutions) { - const minX = Math.min(...pocket.map(p => p.x)); - const minY = Math.min(...pocket.map(p => p.y)); - const localGridW = Math.max(...pocket.map(p => p.x)) - minX + 1; - const localGridH = Math.max(...pocket.map(p => p.y)) - minY + 1; - const localGrid = Array(localGridH).fill(null).map(() => Array(localGridW).fill(false)); - pocket.forEach(cell => { localGrid[cell.y - minY][cell.x - minX] = true; }); - - function canPlace(x, y, size, isVertical, grid) { - for (let i = 0; i < size; i++) { - const lx = isVertical ? x : x + i; - const ly = isVertical ? y : y + i; - if (lx >= localGridW || ly >= localGridH || !grid[ly][lx]) return false; - } - return true; - } - - function backtrack(shipIndex, gridState, placedShips) { - if (solutions.length > 1) return; - if (shipIndex === shipsToPlace.length) { - solutions.push(JSON.parse(JSON.stringify(placedShips))); - return; - } - const shipSize = shipsToPlace[shipIndex]; - for (let r = 0; r < localGridH; r++) { - for (let c = 0; c < localGridW; c++) { - if (canPlace(c, r, shipSize, false, gridState)) { - const newGridState = JSON.parse(JSON.stringify(gridState)); - const positions = []; - for (let i = 0; i < shipSize; i++) { - newGridState[r][c + i] = false; - positions.push({ x: c + minX, y: r + minY }); - } - placedShips.push({ size: shipSize, positions }); - backtrack(shipIndex + 1, newGridState, placedShips); - placedShips.pop(); - if (solutions.length > 1) return; - } - if (shipSize > 1 && canPlace(c, r, shipSize, true, gridState)) { - const newGridState = JSON.parse(JSON.stringify(gridState)); - const positions = []; - for (let i = 0; i < shipSize; i++) { - newGridState[r + i][c] = false; - positions.push({ x: c + minX, y: r + i + minY }); - } - placedShips.push({ size: shipSize, positions }); - backtrack(shipIndex + 1, newGridState, placedShips); - placedShips.pop(); - if (solutions.length > 1) return; - } - } - } - } - backtrack(0, localGrid, []); -} - -/** - * Główna funkcja dedukcyjna, która szuka gwarantowanych trafień poprzez analizę kieszeni. - */ -function findGuaranteedHit() { - const remainingShips = opponentShips.flatMap(s => Array(s.count).fill(s.size)).sort((a, b) => b - a); - if (remainingShips.length === 0) return null; - - const pockets = findPockets(opponentGrid); - const totalUnknown = pockets.reduce((acc, p) => acc + p.length, 0); - const totalShipSize = remainingShips.reduce((acc, s) => acc + s, 0); - - // Warunek konieczny: analiza jest możliwa tylko jeśli wszystkie pozostałe puste pola muszą być statkami. - if (totalUnknown !== totalShipSize) return null; - - console.log("AI: Uruchamiam tryb dedukcji 'Sherlock Holmes'..."); - - for (const pocket of pockets) { - const fittingCombinations = findShipCombinationsThatFit(pocket.length, remainingShips); - if (fittingCombinations.length === 1) { - const shipsToPlace = fittingCombinations[0]; - const placementSolutions = []; - solvePlacement(pocket, shipsToPlace, placementSolutions); - - if (placementSolutions.length === 1) { - console.log("Sherlock AI: Deducuję z absolutną pewnością!"); - return placementSolutions[0][0].positions[0]; // Zwróć pierwszą komórkę pierwszego statku - } - } - } - return null; -} - -// ################################################################## -// # ARCHIMISTRZ AI - SAMODOSTOSOWUJĄCY SIĘ KONTROLER TAKTYCZNY # -// ################################################################## - -class TacticalController { - constructor() { - this.currentMode = 'BALANCED'; // 'AGGRESSIVE', 'BALANCED', 'CAUTIOUS' - this.recentShotHistory = []; // Przechowuje wyniki ostatnich N strzałów (true for hit, false for miss) - this.HISTORY_LENGTH = 10; - } - - /** - * Analizuje stan gry i decyduje o trybie taktycznym. - * @param {object} gameState - Obiekt zawierający informacje o stanie gry. - * { playerShipsLeft: number, opponentShipsLeft: number, - * totalPlayerShips: number, totalOpponentShips: number } - */ - updateAndDecide(gameState) { - const playerShipsLeftRatio = gameState.playerShipsLeft / gameState.totalPlayerShips; - const opponentShipsLeftRatio = gameState.opponentShipsLeft / gameState.totalOpponentShips; - const advantage = playerShipsLeftRatio - opponentShipsLeftRatio; - const hitRate = this.recentShotHistory.filter(Boolean).length / this.recentShotHistory.length; - - let previousMode = this.currentMode; - - if (advantage > 0.25 || (hitRate > 0.6 && this.recentShotHistory.length === this.HISTORY_LENGTH)) { - this.currentMode = 'AGGRESSIVE'; - } else if (advantage < -0.25 || (hitRate < 0.2 && this.recentShotHistory.length === this.HISTORY_LENGTH)) { - this.currentMode = 'CAUTIOUS'; - } else { - this.currentMode = 'BALANCED'; - } - - if (previousMode !== this.currentMode) { - console.log(`Kontroler Taktyczny: Zmiana trybu z ${previousMode} na ${this.currentMode}. Przewaga: ${(advantage * 100).toFixed(0)}%, Skuteczność: ${(hitRate * 100).toFixed(0)}%`); - } - } - - /** - * Rejestruje wynik ostatniego strzału. - * @param {boolean} wasHit - True, jeśli strzał był celny. - */ - recordShot(wasHit) { - this.recentShotHistory.push(wasHit); - if (this.recentShotHistory.length > this.HISTORY_LENGTH) { - this.recentShotHistory.shift(); - } - } - - /** - * Zwraca parametry dla algorytmu MCTS na podstawie aktualnego trybu. - * @returns {{timeout: number, isParanoid: boolean}} - */ - getMctsParameters() { - switch (this.currentMode) { - case 'AGGRESSIVE': - // Szybsze decyzje, mniejsza ostrożność - return { timeout: 10000, isParanoid: false }; - case 'CAUTIOUS': - // Dłuższy namysł, wysoka ostrożność - return { timeout: 25000, isParanoid: true }; - case 'BALANCED': - default: - // Domyślny, zbalansowany czas - return { timeout: 20000, isParanoid: false }; - } - } -} - -const tacticalController = new TacticalController(); - -// ################################################################## -// # ARCHIMISTRZ AI - MODUŁ KOREKCJI BŁĘDÓW # -// ################################################################## - -class ErrorCorrection { - /** - * Weryfikuje, czy zatopienie statku o podanym rozmiarze jest zgodne z aktualną wiedzą o flocie przeciwnika. - * @param {number} sunkShipSize - Rozmiar zatopionego statku. - * @param {Array} knownOpponentShips - Lista pozostałych statków przeciwnika. - * @returns {{error: boolean, message: string|null}} - */ - verifySunkShip(sunkShipSize, knownOpponentShips) { - const shipType = knownOpponentShips.find(s => s.size === sunkShipSize); - if (!shipType || shipType.count === 0) { - const message = `BŁĄD KRYTYCZNY: Zgłoszono zatopienie statku o rozmiarze ${sunkShipSize}, ale według mojej wiedzy nie ma już takich jednostek! Sprawdź swoje zapiski. Możesz cofnąć ostatni ruch, aby skorygować stan gry.`; - return { error: true, message: message }; - } - return { error: false, message: null }; - } -} - -const errorCorrector = new ErrorCorrection(); - - -/** - * Główna funkcja tury bota - decyduje gdzie strzelić. - */ -let isFirstShot = true; - -function botTurn() { - const size = currentConfig.size; - let x, y; - - if (isFirstShot) { - const center = Math.floor(size / 2); - const options = [{x: center, y: center}, {x: center - 1, y: center}, {x: center, y: center - 1}, {x: center - 1, y: center - 1}]; - const choice = options[Math.floor(Math.random() * options.length)]; - x = choice.x; - y = choice.y; - isFirstShot = false; - } else if (botState === 'HUNT') { - const guaranteedHit = findGuaranteedHit(); - if (guaranteedHit) { - console.log("AI dedukuje: GWARANTOWANE trafienie!"); - x = guaranteedHit.x; - y = guaranteedHit.y; - } else { - // "Polowanie na Grubego Zwierza": Sztywna hierarchia priorytetów - let huntingPriority = null; - const remainingShips = opponentShips.filter(s => s.count > 0); - if (remainingShips.some(s => s.size === 5)) { - huntingPriority = 5; - } else if (remainingShips.some(s => s.size === 4)) { - huntingPriority = 4; - } else if (remainingShips.some(s => s.size === 3)) { - huntingPriority = 3; - } else if (remainingShips.length > 0) { - huntingPriority = Math.min(...remainingShips.map(s => s.size)); // Gdy zostaną tylko małe, skup się na najmniejszym - } - console.log(`AI (Polowanie na Grubego Zwierza): Priorytet na statek o rozmiarze: ${huntingPriority || 'brak'}`); - - const gameState = { - playerShipsLeft: playerShips.filter(s => !s.isSunk).length, - opponentShipsLeft: opponentShips.reduce((acc, s) => acc + s.count, 0), - totalPlayerShips: playerShips.length, - totalOpponentShips: currentConfig.ships.reduce((acc, s) => acc + s.count, 0) - }; - tacticalController.updateAndDecide(gameState); - - console.log(`AI: Rozpoczynam analizę ${allPossibleOpponentLayouts.length} możliwych rzeczywistości...`); - let allMoves = []; - const unknownCells = []; - for(let r=0; r s.count > 0).map(s => s.size)); - if (dynamicSmallestShip <= 2 && unknownCells.length > (size * size * 0.5)) { - console.log("AI: Aktywacja Ulepszonego Trybu Egzekutora."); - const parityMoves = unknownCells.filter(cell => (cell.x + cell.y) % dynamicSmallestShip === 0); - - if (parityMoves.length > 0) { - let executorMoves = []; - for (const cell of parityMoves) { - const layoutsWithShipAtCell = allPossibleOpponentLayouts.filter(layout => layout[cell.y][cell.x] === 'ship').length; - const hitProbability = layoutsWithShipAtCell / allPossibleOpponentLayouts.length; - const layoutsWithoutShipAtCell = allPossibleOpponentLayouts.length - layoutsWithShipAtCell; - const informationGainScore = Math.min(layoutsWithShipAtCell, layoutsWithoutShipAtCell); - const quantumScore = informationGainScore * 0.7 + (layoutsWithShipAtCell * 0.3); - executorMoves.push({ x: cell.x, y: cell.y, score: quantumScore }); - } - - executorMoves.sort((a, b) => b.score - a.score); - const bestMove = executorMoves[0]; - x = bestMove.x; - y = bestMove.y; - - lastShot = { x, y }; - const coordString = `${String.fromCharCode(65 + y)}${x + 1}`; - document.getElementById('bot-suggestion').textContent = coordString; - console.log(`Bot (Egzekutor) sugeruje strzał w: (${x}, ${y}) z wynikiem kwantowym ${bestMove.score.toFixed(2)}`); - return; - } - } - - for (const cell of unknownCells) { - const layoutsWithShipAtCell = allPossibleOpponentLayouts.filter(layout => layout[cell.y][cell.x] === 'ship').length; - - if (allPossibleOpponentLayouts.length > 0) { - const hitProbability = layoutsWithShipAtCell / allPossibleOpponentLayouts.length; - - if (hitProbability > 0.99) { - allMoves.push({ x: cell.x, y: cell.y, score: Infinity }); - continue; - } - - const layoutsWithoutShipAtCell = allPossibleOpponentLayouts.length - layoutsWithShipAtCell; - let quantumScore; - - if (huntingPriority) { - const targetedVoIScore = calculateTargetedVoI(cell.x, cell.y, huntingPriority); - let voiWeight = (huntingPriority >= 4) ? 0.8 : 0.6; // Wzmocnienie VoI dla dużych statków - quantumScore = (targetedVoIScore * voiWeight) + (hitProbability * (1 - voiWeight)); - } else { - // Tryb ogólny, gdy nie ma jasnego priorytetu - const informationGainScore = Math.min(layoutsWithShipAtCell, layoutsWithoutShipAtCell); - quantumScore = informationGainScore * 0.7 + (layoutsWithShipAtCell * 0.3); - } - - allMoves.push({ x: cell.x, y: cell.y, score: quantumScore }); - } - } - - if (allMoves.length > 0) { - allMoves.sort((a, b) => b.score - a.score); - const bestMove = allMoves[0]; - x = bestMove.x; - y = bestMove.y; - renderTopMoves(allMoves); - console.log(`AI: Najlepszy ruch ma wynik kwantowy ${bestMove.score.toFixed(2)}.`); - } else { - // Sytuacja awaryjna - const randomCell = unknownCells[Math.floor(Math.random() * unknownCells.length)]; - x = randomCell.x; - y = randomCell.y; - } - } - } else { // botState === 'TARGET' - generateTargetQueue(); - if (targetQueue.length > 0) { - const nextTarget = targetQueue.shift(); - x = nextTarget.x; - y = nextTarget.y; - } else { - // Jeśli kolejka jest pusta (co nie powinno się zdarzyć, ale na wszelki wypadek) - botState = 'HUNT'; - botTurn(); - return; - } - } - - lastShot = { x, y }; - // Aktualizacja UI z sugestią - const coordString = `${String.fromCharCode(65 + y)}${x + 1}`; - document.getElementById('bot-suggestion').textContent = coordString; - console.log(`Bot sugeruje strzał w: (${x}, ${y})`); -} - -/** - * Aktualizuje stan gry po strzale bota na podstawie odpowiedzi gracza. - * @param {string} result - Wynik strzału ('Pudło', 'Trafiony', 'Uszkodzony', 'Zatopiony'). - */ - -/** - * Przeszukuje całą planszę w poszukiwaniu pól, które na pewno są puste. - * Działa na zasadzie "Constraint Satisfaction". - */ -function globalConstraintSolver() { - const size = currentConfig.size; - const remainingShipSizes = opponentShips.flatMap(s => Array(s.count).fill(s.size)); - if (remainingShipSizes.length === 0) return; - - let changed = false; - - for (let y = 0; y < size; y++) { - for (let x = 0; x < size; x++) { - if (opponentGrid[y][x] === 'unknown') { - let canAnyShipFit = false; - for (const shipSize of remainingShipSizes) { - // Sprawdź wszystkie możliwe pozycje i orientacje - // Poziomo - for (let i = 0; i < shipSize; i++) { - const checkXStart = x - i; - if (checkXStart >= 0 && checkXStart + shipSize <= size) { - if (canPlaceShipForProbability(opponentGrid, checkXStart, y, shipSize, false)) { - canAnyShipFit = true; - break; - } - } - } - if (canAnyShipFit) break; - // Pionowo - for (let i = 0; i < shipSize; i++) { - const checkYStart = y - i; - if (checkYStart >= 0 && checkYStart + shipSize <= size) { - if (canPlaceShipForProbability(opponentGrid, x, checkYStart, shipSize, true)) { - canAnyShipFit = true; - break; - } - } - } - if (canAnyShipFit) break; - } - - if (!canAnyShipFit) { - opponentGrid[y][x] = 'miss'; - console.log(`AI (Globalna Dedykcja): Pole (${x},${y}) oznaczone jako PUDŁO.`); - changed = true; - } - } - } - } - return changed; -} - -/** - * Oblicza "ukierunkowaną" Wartość Informacji dla konkretnego rozmiaru statku. - * @param {number} x - Współrzędna X pola. - * @param {number} y - Współrzędna Y pola. - * @param {number} shipSize - Rozmiar statku, na który polujemy. - * @returns {number} - Wynik VoI dla tego celu. - */ -function calculateTargetedVoI(x, y, shipSize) { - let informationValue = 0; - // Poziomo - for (let i = 0; i < shipSize; i++) { - if (canPlaceShipForProbability(opponentGrid, x - i, y, shipSize, false)) { - informationValue++; - } - } - // Pionowo - for (let i = 0; i < shipSize; i++) { - if (canPlaceShipForProbability(opponentGrid, x, y - i, shipSize, true)) { - informationValue++; - } - } - return informationValue; -} - - -function updatePossibleLayouts(shotX, shotY, result) { - const previousCount = allPossibleOpponentLayouts.length; - if (result === 'Pudło') { - allPossibleOpponentLayouts = allPossibleOpponentLayouts.filter(layout => layout[shotY][shotX] === 'water'); - } else { // Trafiony, Uszkodzony, Zatopiony - allPossibleOpponentLayouts = allPossibleOpponentLayouts.filter(layout => layout[shotY][shotX] === 'ship'); - } - console.log(`AI: Redukcja rzeczywistości. Pozostało ${allPossibleOpponentLayouts.length} z ${previousCount}.`); - - // Sprawdź, czy nie trzeba uruchomić regeneracji - if (allPossibleOpponentLayouts.length < 10000 && opponentShips.reduce((acc, s) => acc + s.count, 0) > 3) { - regenerateHypothesesAsync(); - } - - // Weryfikator Kontradykcji - if (allPossibleOpponentLayouts.length === 0) { - console.error("AI: WYKRYTO KONTRADYKCJĘ! Liczba hipotez spadła do zera."); - alert("Wykryto sprzeczność w podanych informacjach. Prawdopodobnie ostatni zgłoszony wynik strzału był nieprawidłowy. Automatycznie cofam ostatni ruch, proszę o ponowne podanie wyniku."); - - // Użyj istniejącej logiki cofania - if (history.length > 0) { - const lastState = history.pop(); - opponentGrid = lastState.grid; - opponentShips = lastState.ships; - botState = lastState.botState; - currentTargetHits = lastState.currentTargetHits; - lastShot = lastState.lastShot; - renderBoard('opponent-board', opponentGrid); - console.log("Cofnięto ostatni ruch z powodu kontradykcji."); - // Nie wykonuj tury bota, czekaj na ponowne wprowadzenie danych przez gracza - return; // Zwróć, aby uniknąć wywołania botTurn() w updateAfterBotShot - } - } -} - -/** - * Zlicza statki o określonych rozmiarach w danym układzie. - * @param {Array>} layout - Siatka do analizy. - * @returns {object} - Obiekt z liczbą statków dla każdego rozmiaru (np. {5: 1, 4: 1, 3: 2}). - */ -function countShipsInLayout(layout) { - const size = layout.length; - const counts = {}; - const visited = Array(size).fill(null).map(() => Array(size).fill(false)); - - for (let y = 0; y < size; y++) { - for (let x = 0; x < size; x++) { - if (layout[y][x] === 'ship' && !visited[y][x]) { - let shipSize = 1; - visited[y][x] = true; - - // Sprawdź w prawo - let currentX = x + 1; - while (currentX < size && layout[y][currentX] === 'ship') { - visited[y][currentX] = true; - shipSize++; - currentX++; - } - - // Sprawdź w dół - let currentY = y + 1; - while (currentY < size && layout[currentY][x] === 'ship') { - visited[currentY][x] = true; - shipSize++; - currentY++; - } - - counts[shipSize] = (counts[shipSize] || 0) + 1; - } - } - } - return counts; -} - -/** - * Asynchronicznie dogenerowuje i filtruje nowe hipotezy w tle z "Weryfikacją Kryptograficzną". - */ -function regenerateHypothesesAsync() { - console.log("AI: Uruchomiono regenerację z weryfikacją kryptograficzną..."); - showThinkingIndicator(); - - const size = currentConfig.size; - const shipsConfig = currentConfig.ships; - const regenerationChunk = 250; // Mniejsze paczki dla bardziej złożonej logiki - const targetAddition = 5000; - let addedCount = 0; - - // 1. Stwórz "Wzorzec Floty" na podstawie aktualnego stanu gry - const expectedShips = {}; - opponentShips.forEach(s => { - if (s.count > 0) expectedShips[s.size] = s.count; - }); - - function generateAndVerifyChunk() { - if (addedCount >= targetAddition) { - console.log(`AI: Zakończono regenerację. Dodano ${addedCount} czystych hipotez.`); - hideThinkingIndicator(); - return; - } - - let verifiedHypotheses = []; - let attempts = 0; - while (verifiedHypotheses.length < (regenerationChunk / 10) && attempts < regenerationChunk) { - attempts++; - // Użyj nowej, inteligentniejszej funkcji generującej - const newLayout = generateConstrainedRandomLayout(size, shipsConfig, opponentGrid); - - if (newLayout) { - // Krok 2: Weryfikacja Kompozycji Floty - const layoutShipCounts = countShipsInLayout(newLayout); - let compositionIsValid = true; - - const expectedKeys = Object.keys(expectedShips); - const actualKeys = Object.keys(layoutShipCounts); - - if (expectedKeys.length !== actualKeys.length) { - compositionIsValid = false; - } else { - for (const sizeKey of expectedKeys) { - if (expectedShips[sizeKey] !== layoutShipCounts[sizeKey]) { - compositionIsValid = false; - break; - } - } - } - - if (compositionIsValid) { - verifiedHypotheses.push(newLayout); - } - } - } - - allPossibleOpponentLayouts.push(...verifiedHypotheses); - addedCount += verifiedHypotheses.length; - - console.log(`Regeneracja: ${allPossibleOpponentLayouts.length} / docelowo ${10000 + targetAddition}`); - setTimeout(generateAndVerifyChunk, 0); - } - generateAndVerifyChunk(); -} - -function updateAfterBotShot(result) { - if (!lastShot) return; - - clearPreviousSuggestions(); - - // Zapisz aktualny stan przed modyfikacją - history.push(JSON.parse(JSON.stringify({ - grid: opponentGrid, - ships: opponentShips, - botState: botState, - currentTargetHits: currentTargetHits, - lastShot: lastShot - }))); - - const { x, y } = lastShot; - - const cellElement = document.querySelector(`#opponent-board .cell[data-x='${x}'][data-y='${y}']`); - - switch (result) { - case 'Pudło': - opponentGrid[y][x] = 'miss'; - triggerAnimation(cellElement, 'animate-miss', 400); - cellElement.classList.add('miss'); - tacticalController.recordShot(false); - - // Uruchom globalny solver z ograniczeniem iteracji - for (let i = 0; i < 4; i++) { - if (!globalConstraintSolver()) { - break; // Przerwij, jeśli nie znaleziono żadnych nowych pewników - } - console.log(`AI (Globalna Dedykcja): Iteracja ${i+1}...`); - } - break; - case 'Trafiony': - case 'Uszkodzony': - opponentGrid[y][x] = 'hit'; - triggerAnimation(cellElement, 'animate-hit', 500); - cellElement.classList.add(result === 'Trafiony' ? 'hit' : 'damaged'); - botState = 'TARGET'; - currentTargetHits.push({ x, y }); - tacticalController.recordShot(true); - break; - case 'Zatopiony': - opponentGrid[y][x] = 'hit'; // Najpierw oznacz jako trafienie dla animacji - triggerAnimation(cellElement, 'animate-hit', 500); - - currentTargetHits.push({ x, y }); - tacticalController.recordShot(true); - - // Zidentyfikuj statek PRZED oznaczeniem go jako zatopiony - const shipCoords = identifyAndGetSunkShipCoords(x,y); - - // Uruchom animację zatopienia na wszystkich częściach z opóźnieniem - setTimeout(() => { - shipCoords.forEach(pos => { - const sunkCellEl = document.querySelector(`#opponent-board .cell[data-x='${pos.x}'][data-y='${pos.y}']`); - if(sunkCellEl) { - opponentGrid[pos.y][pos.x] = 'sunk'; // Zaktualizuj model danych - sunkCellEl.classList.remove('hit', 'damaged'); - sunkCellEl.classList.add('sunk'); - triggerAnimation(sunkCellEl, 'animate-sunk', 600); - } - }); - - // Najpierw standardowe oznaczanie, a potem globalna dedukcja - markSurroundingAsMiss(shipCoords); - - // Uruchom globalny solver z ograniczeniem iteracji - for (let i = 0; i < 4; i++) { - if (!globalConstraintSolver()) { - break; // Przerwij, jeśli nie znaleziono żadnych nowych pewników - } - console.log(`AI (Globalna Dedykcja): Iteracja ${i+1}...`); - } - - renderBoard('opponent-board', opponentGrid); // Przerenderuj, aby pokazać wszystkie nowe 'miss' - }, 550); - - - // Reset stanu bota - const verification = errorCorrector.verifySunkShip(shipCoords.length, opponentShips); - if (verification.error) { - alert(verification.message); - // Cofnij operację: oznacz tylko jako trafione, nie zatopione - shipCoords.forEach(part => { - const cellEl = document.querySelector(`#opponent-board .cell[data-x='${part.x}'][data-y='${part.y}']`); - if (cellEl) cellEl.classList.add('hit'); - opponentGrid[part.y][part.x] = 'hit'; - }); - botState = 'TARGET'; // Wróć do trybu dobijania, bo coś jest nie tak - return; // Przerwij dalsze przetwarzanie - } - - // Kontynuuj, jeśli weryfikacja przebiegła pomyślnie - const shipIndex = opponentShips.findIndex(s => s.size === shipCoords.length); - opponentShips[shipIndex].count--; - updateOpponentProfile({ size: shipCoords.length, positions: shipCoords }); - checkWinCondition(); - - botState = 'HUNT'; - targetQueue = []; - currentTargetHits = []; // Wyczyść dopiero po całej operacji - break; - } - renderBoard('opponent-board', opponentGrid); // Odśwież widok planszy - - // Zaktualizuj "pamięć" bota o nową informację - teraz, gdy plansza jest już w pełni zaktualizowana - updatePossibleLayouts(x, y, result); -} - -/** - * Pomocnicza funkcja do uruchamiania animacji na komórce. - * @param {HTMLElement} cellElement - Element komórki do animowania. - * @param {string} animationClass - Klasa CSS animacji. - * @param {number} duration - Czas trwania animacji w ms. - */ -function triggerAnimation(cellElement, animationClass, duration) { - cellElement.classList.add(animationClass); - setTimeout(() => { - cellElement.classList.remove(animationClass); - }, duration); -} - -/** - * Generuje kolejkę celów w trybie TARGET. - */ -function generateTargetQueue() { - targetQueue = []; - const size = currentConfig.size; - - const isValidTarget = (x, y) => { - return x >= 0 && x < size && y >= 0 && y < size && opponentGrid[y][x] === 'unknown'; - }; - - if (currentTargetHits.length === 1) { - const { x, y } = currentTargetHits[0]; - // Dodaj 4 sąsiadów - [{x:x, y:y-1}, {x:x, y:y+1}, {x:x-1, y:y}, {x:x+1, y:y}].forEach(t => { - if(isValidTarget(t.x, t.y)) targetQueue.push(t); - }); - } else { - // Ustal orientację i dodaj cele na końcach - currentTargetHits.sort((a, b) => a.x - b.x || a.y - b.y); - const firstHit = currentTargetHits[0]; - const lastHit = currentTargetHits[currentTargetHits.length - 1]; - const isVertical = firstHit.x === lastHit.x; - - if (isVertical) { - if (isValidTarget(firstHit.x, firstHit.y - 1)) targetQueue.push({ x: firstHit.x, y: firstHit.y - 1 }); - if (isValidTarget(lastHit.x, lastHit.y + 1)) targetQueue.push({ x: lastHit.x, y: lastHit.y + 1 }); - } else { // Poziomo - if (isValidTarget(firstHit.x - 1, firstHit.y)) targetQueue.push({ x: firstHit.x - 1, y: firstHit.y }); - if (isValidTarget(lastHit.x + 1, lastHit.y)) targetQueue.push({ x: lastHit.x + 1, y: lastHit.y }); - } - } -} - -/** - * Po zatopieniu statku, oznacza otaczające go pola jako 'miss'. - * @param {Array<{x: number, y: number}>} shipPositions - Pozycje zatopionego statku. - */ -function markSurroundingAsMiss(shipPositions) { - const size = currentConfig.size; - shipPositions.forEach(pos => { - for (let dy = -1; dy <= 1; dy++) { - for (let dx = -1; dx <= 1; dx++) { - if (dx === 0 && dy === 0) continue; - const newX = pos.x + dx; - const newY = pos.y + dy; - if (newX >= 0 && newX < size && newY >= 0 && newY < size && opponentGrid[newY][newX] === 'unknown') { - opponentGrid[newY][newX] = 'miss'; - } - } - } - }); -} - - -// Etap 4: Rozbudowa bota do poziomu zaawansowanego -// W tym miejscu pojawią się zaawansowane algorytmy, takie jak mapa prawdopodobieństwa. - -// Etap 5: Finalizacja -// Kod do obsługi ekranów startowych/końcowych i ustawień gry. - -function identifyAndGetSunkShipCoords(lastHitX, lastHitY) { - const size = currentConfig.size; - const q = [{x: lastHitX, y: lastHitY}]; - const visited = new Set([`${lastHitX},${lastHitY}`]); - const shipParts = [{x: lastHitX, y: lastHitY}]; - - while(q.length > 0){ - const curr = q.shift(); - const neighbors = [ - {x: curr.x + 1, y: curr.y}, {x: curr.x - 1, y: curr.y}, - {x: curr.x, y: curr.y + 1}, {x: curr.x, y: curr.y - 1} - ]; - for(const n of neighbors){ - const key = `${n.x},${n.y}`; - if (n.x >= 0 && n.x < size && n.y >= 0 && n.y < size && !visited.has(key)) { - const cellState = opponentGrid[n.y][n.x]; - if (cellState === 'hit' || cellState === 'damaged') { - visited.add(key); - q.push(n); - shipParts.push(n); - } - } - } - } - return shipParts; -} - -function checkWinCondition() { - // Sprawdzenie, czy bot wygrał (wszystkie statki przeciwnika zatopione) - const opponentShipsLeft = opponentShips.reduce((acc, ship) => acc + ship.count, 0); - if (opponentShipsLeft === 0) { - showEndScreen('Gratulacje! Wygrałeś razem z botem!'); - return; - } - - // Sprawdzenie, czy gracz 2 wygrał (wszystkie statki bota zniszczone) - const playerShipsLeft = playerShips.some(ship => !ship.isSunk); - if (!playerShipsLeft && playerShips.length > 0) { - showEndScreen('Niestety, przeciwnik wygrał.'); - } -} - -function showEndScreen(message) { - document.getElementById('game-over-message').textContent = message; - document.getElementById('end-screen').classList.remove('hidden'); - saveOpponentProfile(); // Zapisz profil po zakończeniu gry -} - -function saveOpponentProfile() { - try { - const fullProfile = { - placement: opponentProfile.placementBias, - orientation: opponentProfile.orientationBias - // W przyszłości można dodać: tactical: tacticalProfile - }; - localStorage.setItem('battleshipAI_opponentProfile_v2', JSON.stringify(fullProfile)); - console.log("Pamięć Długotrwała: Rozbudowany profil przeciwnika został zapisany."); - } catch (e) { - console.error("Nie udało się zapisać profilu przeciwnika:", e); - } -} - -function loadOpponentProfile() { - try { - const savedProfile = localStorage.getItem('battleshipAI_opponentProfile_v2'); - if (savedProfile) { - const loaded = JSON.parse(savedProfile); - // Stopniowe dostosowanie, a nie całkowite zastąpienie - opponentProfile.placementBias.edge = (opponentProfile.placementBias.edge + loaded.placement.edge) / 2; - opponentProfile.placementBias.center = (opponentProfile.placementBias.center + loaded.placement.center) / 2; - opponentProfile.placementBias.corner = (opponentProfile.placementBias.corner + loaded.placement.corner) / 2; - opponentProfile.orientationBias.vertical = (opponentProfile.orientationBias.vertical + loaded.orientation.vertical) / 2; - opponentProfile.orientationBias.horizontal = (opponentProfile.orientationBias.horizontal + loaded.orientation.horizontal) / 2; - console.log("Pamięć Długotrwała: Rozbudowany profil przeciwnika został załadowany i zaadaptowany."); - } else { - console.log("Pamięć Długotrwała: Nie znaleziono zapisanego profilu. Rozpoczynam z czystą kartą."); - } - } catch (e) { - console.error("Nie udało się załadować profilu przeciwnika:", e); - } -} - - -function startGame(sizeConfig) { - console.log(`Rozpoczynanie gry z planszą ${sizeConfig}...`); - currentConfig = GAME_CONFIGS[sizeConfig]; - loadOpponentProfile(); // Załaduj profil przed rozpoczęciem gry - document.getElementById('start-screen').classList.add('hidden'); - document.getElementById('shot-input-container').classList.remove('hidden'); - - // Inicjalizacja i generowanie widoku - createBoard('player-board', 'player-coords-top', 'player-coords-left', currentConfig.size); - createBoard('opponent-board', 'opponent-coords-top', 'opponent-coords-left', currentConfig.size); - - // Inicjalizacja logicznej reprezentacji plansz - initializeGrids(currentConfig.size); - initializeQuantumHunter(); // Inicjalizacja nowego systemu ataku - placeShipsRandomly(playerGrid, currentConfig.ships); - - // Renderowanie stanu początkowego - renderBoard('player-board', playerGrid); - renderBoard('opponent-board', opponentGrid); - - - // Dodanie obsługi kliknięć na planszach - document.getElementById('player-board').addEventListener('click', handlePlayerBoardClick); - document.getElementById('opponent-board').addEventListener('click', handleOpponentBoardClick); - - // Dodanie obsługi przycisków - const buttons = { - 'btn-miss': 'Pudło', - 'btn-hit': 'Trafiony', - 'btn-damaged': 'Uszkodzony', - 'btn-sunk': 'Zatopiony' - }; - - for (const [id, action] of Object.entries(buttons)) { - document.getElementById(id).addEventListener('click', () => { - updateAfterBotShot(action); - if(botState === 'HUNT') turnCounter++; - botTurn(); - }); - } - - // Rozpoczęcie pierwszej tury bota - turnCounter = 1; - botTurn(); -} - -document.addEventListener('DOMContentLoaded', () => { - console.log('Gra w statki załadowana. Oczekiwanie na wybór gracza...'); - - document.querySelectorAll('.board-size-btn').forEach(button => { - button.addEventListener('click', () => { - startGame(button.dataset.size); - }); - }); - - document.getElementById('restart-btn').addEventListener('click', () => { - location.reload(); - }); - - document.getElementById('submit-player-shot-btn').addEventListener('click', handlePlayerOverrideShot); - - document.getElementById('btn-undo').addEventListener('click', () => { - if (history.length > 0) { - const lastState = history.pop(); - opponentGrid = lastState.grid; - opponentShips = lastState.ships; - botState = lastState.botState; - currentTargetHits = lastState.currentTargetHits; - lastShot = lastState.lastShot; - renderBoard('opponent-board', opponentGrid); - console.log("Cofnięto ostatni ruch."); - } else { - alert("Brak ruchów do cofnięcia."); - } - }); - - const shotInput = document.getElementById('shot-input'); - const submitShotBtn = document.getElementById('submit-shot-btn'); - - const handleSubmitShot = () => { - const value = shotInput.value.trim().toUpperCase(); - if (!value) return; - - const letter = value.charAt(0); - const number = parseInt(value.substring(1), 10); - - if (letter >= 'A' && letter <= String.fromCharCode(65 + currentConfig.size - 1) && number >= 1 && number <= currentConfig.size) { - const y = letter.charCodeAt(0) - 65; - const x = number - 1; - const cell = document.querySelector(`#player-board .cell[data-x='${x}'][data-y='${y}']`); - if (cell) { - cell.click(); - shotInput.value = ''; // Wyczyść pole po strzale - } - } else { - alert("Nieprawidłowe koordynaty. Wpisz np. 'A5'."); - } - }; - - submitShotBtn.addEventListener('click', handleSubmitShot); - shotInput.addEventListener('keydown', (event) => { - if (event.key === 'Enter') { - handleSubmitShot(); - } - }); - - document.getElementById('player-shot-input').addEventListener('keydown', (event) => { - if (event.key === 'Enter') { - handlePlayerOverrideShot(); - } - }); -}); - -function handlePlayerOverrideShot() { - const inputElement = document.getElementById('player-shot-input'); - const value = inputElement.value.trim().toUpperCase(); - if (!value) return; - - const letter = value.charAt(0); - const number = parseInt(value.substring(1), 10); - - if (letter >= 'A' && letter <= String.fromCharCode(65 + currentConfig.size - 1) && number >= 1 && number <= currentConfig.size) { - const y = letter.charCodeAt(0) - 65; - const x = number - 1; - - if (opponentGrid[y][x] !== 'unknown') { - alert("To pole było już ostrzelane! Wybierz inne."); - return; - } - - // Gracz przejmuje inicjatywę - lastShot = { x, y }; - document.getElementById('bot-suggestion').textContent = `${value} (Twój wybór)`; - clearPreviousSuggestions(); - - // Podświetl wybór gracza - const cellElement = document.querySelector(`#opponent-board .cell[data-x='${x}'][data-y='${y}']`); - if (cellElement) { - cellElement.classList.add('suggestion-1'); // Użyj stylu najlepszej sugestii - } - - console.log(`Gracz przejmuje kontrolę, strzelając w: (${x}, ${y})`); - inputElement.value = ''; // Wyczyść pole po strzale - } else { - alert("Nieprawidłowe koordynaty. Wpisz np. 'A5'."); - } -} diff --git a/style.css b/style.css deleted file mode 100644 index 361f5f7..0000000 --- a/style.css +++ /dev/null @@ -1,422 +0,0 @@ -:root { - /* Kolory w stylu Material Design 3 - Ciemny motyw */ - --md-sys-color-primary: #9BFAFF; - --md-sys-color-on-primary: #00363A; - --md-sys-color-primary-container: #004F54; - --md-sys-color-on-primary-container: #B8FBFF; - --md-sys-color-background: #191C1D; - --md-sys-color-on-background: #E1E3E3; - --md-sys-color-surface: #191C1D; - --md-sys-color-on-surface: #E1E3E3; - --md-sys-color-surface-variant: #3F4849; - --md-sys-color-on-surface-variant: #BFC8C9; - --md-sys-color-outline: #899293; - - --font-family-sans-serif: 'Roboto', sans-serif; - --cell-size: 3.5vmin; -} - -body { - background-color: var(--md-sys-color-background); - color: var(--md-sys-color-on-background); - font-family: var(--font-family-sans-serif); - margin: 0; - padding: 1rem; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-height: 100vh; -} - -header { - width: 100%; - text-align: center; - margin-bottom: 2rem; -} - -h1, h2 { - font-weight: 400; -} - -main { - display: flex; - justify-content: space-around; - width: 100%; - max-width: 1600px; - gap: 2rem; -} - -.board-container { - text-align: center; -} - -.board-wrapper { - display: grid; - grid-template-areas: - ". top" - "left board"; - grid-template-columns: auto 1fr; - grid-template-rows: auto 1fr; - gap: 0.5rem; -} - -.coords { - display: flex; - color: var(--md-sys-color-on-surface-variant); - font-size: calc(var(--cell-size) * 0.5); -} - -.coords-top { - grid-area: top; - flex-direction: row; - justify-content: space-around; - padding-left: 0.5rem; /* Kompensacja za lewą kolumnę */ -} - -.coords-left { - grid-area: left; - flex-direction: column; - justify-content: space-around; -} - -.board { - grid-area: board; - display: grid; - grid-template-columns: repeat(var(--grid-size), var(--cell-size)); - grid-template-rows: repeat(var(--grid-size), var(--cell-size)); - border: 1px solid var(--md-sys-color-outline); - background-color: var(--md-sys-color-surface-variant); - box-shadow: 0 4px 8px rgba(0,0,0,0.4); - border-radius: 8px; - padding: 0.5rem; /* Drobny padding dla lepszego wyglądu */ - gap: 2px; -} - -.cell { - width: var(--cell-size); - height: var(--cell-size); - border: 1px solid var(--md-sys-color-outline); - box-sizing: border-box; - cursor: pointer; - transition: background-color 0.2s ease-in-out, transform 0.2s ease-in-out; - display: flex; - justify-content: center; - align-items: center; - font-size: calc(var(--cell-size) * 0.7); - border-radius: 4px; - background-color: rgba(255, 255, 255, 0.05); -} - -.cell:hover { - background-color: rgba(255, 255, 255, 0.1); - transform: scale(1.05); -} - -/* Stany komórek */ -.cell.miss::after { - font-family: 'Material Symbols Outlined'; - content: 'water_drop'; - color: #75D1FF; /* Niebieski kolor z palety Material Design */ - font-variation-settings: 'FILL' 0, 'wght' 200, 'GRAD' 0, 'opsz' 24; - opacity: 0.8; -} - -.cell.hit { - background: radial-gradient(circle, rgba(255,180,50,0.6) 0%, transparent 70%); -} - -.cell.hit::after { - font-family: 'Material Symbols Outlined'; - content: 'local_fire_department'; - color: #FFB4AB; /* Kolor z palety Material Design Error */ - font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24; -} - -.cell.damaged { /* Teraz będzie to samo co hit */ - background: radial-gradient(circle, rgba(255,180,50,0.6) 0%, transparent 70%); -} - -.cell.sunk { - background-color: #4C0004; /* Ciemny czerwony */ - border-color: #FFB4AB; -} - -.cell.sunk::after { - font-family: 'Material Symbols Outlined'; - content: 'skull'; - color: #FFB4AB; - font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24; -} - -.cell.ship { - background-color: var(--md-sys-color-primary-container); - opacity: 0.7; -} - -#opponent-board .cell.suggestion-1 { background-color: rgba(155, 250, 255, 0.9); } /* Najlepsza sugestia - najbardziej nasycony */ -#opponent-board .cell.suggestion-2 { background-color: rgba(155, 250, 255, 0.75); } -#opponent-board .cell.suggestion-3 { background-color: rgba(155, 250, 255, 0.6); } -#opponent-board .cell.suggestion-4 { background-color: rgba(155, 250, 255, 0.45); } -#opponent-board .cell.suggestion-5 { background-color: rgba(155, 250, 255, 0.3); } /* Najsłabsza z top 5 */ - - -.controls-container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 1.5rem; - padding: 2rem; - background-color: var(--md-sys-color-surface-variant); - border-radius: 16px; -} - -.bot-communication { - text-align: center; -} - -#bot-suggestion { - font-weight: bold; - color: var(--md-sys-color-primary); - font-size: 1.5rem; -} - -.action-buttons { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1rem; -} - -.action-buttons button { - background-color: var(--md-sys-color-primary-container); - color: var(--md-sys-color-on-primary-container); - border: none; - padding: 1rem 1.5rem; - border-radius: 24px; - cursor: pointer; - font-size: 1rem; - transition: background-color 0.3s; -} - -.action-buttons button:hover { - background-color: var(--md-sys-color-primary); - color: var(--md-sys-color-on-primary); -} - -.player-shot-container { - display: flex; - gap: 0.5rem; - align-items: center; - border: 1px solid var(--md-sys-color-outline); - padding: 1rem; - border-radius: 16px; -} - -#player-shot-input { - background-color: var(--md-sys-color-surface); - color: var(--md-sys-color-on-surface); - border: 1px solid var(--md-sys-color-outline); - border-radius: 16px; - padding: 0.75rem; - font-size: 1rem; -} - -#submit-player-shot-btn { - background-color: var(--md-sys-color-primary); - color: var(--md-sys-color-on-primary); - border: none; - padding: 0.75rem 1rem; - border-radius: 16px; - cursor: pointer; - font-size: 1rem; - transition: background-color 0.3s; -} - -#submit-player-shot-btn:hover { - background-color: var(--md-sys-color-primary-container); - color: var(--md-sys-color-on-primary-container); -} - -.spinner { - border: 4px solid rgba(255, 255, 255, 0.3); - border-radius: 50%; - border-top: 4px solid var(--md-sys-color-primary); - width: 40px; - height: 40px; - animation: spin 1s linear infinite; - margin: 0 auto 1rem auto; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -#btn-undo { - background-color: #B3261E; /* Material Design Error Color */ - color: #FFFFFF; - border: none; - padding: 1rem 1.5rem; - border-radius: 24px; - cursor: pointer; - font-size: 1rem; - transition: background-color 0.3s; - margin-top: 1rem; /* Dodatkowy margines dla oddzielenia */ -} - -#btn-undo:hover { - background-color: #8C1D18; /* Ciemniejszy odcień */ -} - -/* ################################################################## - # ANIMACJE BITWY # - ################################################################## */ - -@keyframes hit-animation { - 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 180, 50, 0.7); } - 50% { transform: scale(1.2); box-shadow: 0 0 15px 10px rgba(255, 180, 50, 0); } - 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 180, 50, 0); } -} - -@keyframes miss-animation { - 0%, 100% { transform: translateX(0); } - 25% { transform: translateX(-5px); } - 75% { transform: translateX(5px); } -} - -@keyframes sunk-animation { - 0% { filter: brightness(1); } - 50% { filter: brightness(2) saturate(2); transform: rotate(5deg) scale(1.1); } - 100% { filter: brightness(1); } -} - -.cell.animate-hit { - animation: hit-animation 0.5s ease-out; -} - -.cell.animate-miss { - animation: miss-animation 0.4s ease-in-out; -} - -.cell.animate-sunk { - animation: sunk-animation 0.6s ease; -} - -#game-log-container { - width: 100%; - margin-top: 1rem; -} - -#game-log { - background-color: rgba(0,0,0,0.2); - border: 1px solid var(--md-sys-color-outline); - border-radius: 12px; - height: 150px; - overflow-y: auto; - padding: 0.5rem; - text-align: left; - font-size: 0.9rem; -} - -.log-entry { - margin-bottom: 0.25rem; -} - -.icon-btn { - background: none; - border: none; - color: var(--md-sys-color-on-surface-variant); - cursor: pointer; - padding: 0.5rem; - border-radius: 50%; -} -.icon-btn:hover { - background-color: rgba(255,255,255,0.1); -} - -.setting-option { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - margin: 1rem 0; -} - -/* Ekran startowy i modale */ -.overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.8); - display: flex; - justify-content: center; - align-items: center; - z-index: 1000; -} - -.modal { - background-color: var(--md-sys-color-surface-variant); - padding: 3rem; - border-radius: 24px; - text-align: center; -} - -.board-size-options { - display: flex; - gap: 1rem; - margin-top: 1.5rem; -} - -.board-size-btn { - background-color: var(--md-sys-color-primary-container); - color: var(--md-sys-color-on-primary-container); - border: none; - padding: 1rem 1.5rem; - border-radius: 24px; - cursor: pointer; - font-size: 1rem; - transition: background-color 0.3s; -} - -.board-size-btn:hover { - background-color: var(--md-sys-color-primary); - color: var(--md-sys-color-on-primary); -} - -.hidden { - display: none; -} - -#shot-input-container { - display: flex; - gap: 0.5rem; - align-items: center; -} - -#shot-input { - background-color: var(--md-sys-color-surface); - color: var(--md-sys-color-on-surface); - border: 1px solid var(--md-sys-color-outline); - border-radius: 16px; - padding: 0.75rem; - font-size: 1rem; -} - -#submit-shot-btn { - background-color: var(--md-sys-color-primary-container); - color: var(--md-sys-color-on-primary-container); - border: none; - padding: 0.75rem 1rem; - border-radius: 16px; - cursor: pointer; - font-size: 1rem; - transition: background-color 0.3s; -} - -#submit-shot-btn:hover { - background-color: var(--md-sys-color-primary); - color: var(--md-sys-color-on-primary); -} diff --git a/ui/view.go b/ui/view.go new file mode 100644 index 0000000..9eb76a6 --- /dev/null +++ b/ui/view.go @@ -0,0 +1,414 @@ +package ui + +import ( + "battleship/ai" + "battleship/game" + "fmt" + "strings" + + "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +// Definicje stylów +var ( + subtleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")) + titleStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FFF")). + Background(lipgloss.Color("#333")). + Padding(0, 1). + Bold(true) + + // Kolory komórek - wyraźniejsze rozróżnienie + waterStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#244")).SetString("·") // Ciemny szary + shipStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#DDD")).SetString("■") // Jasny szary + hitStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500")).SetString("X") // Pomarańczowy dla trafienia + missStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#55F")).SetString("○") // Niebieski dla pudła + sunkStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#F00")).Bold(true).SetString("#") // Czerwony dla zatopienia + cursorStyle = lipgloss.NewStyle().Background(lipgloss.Color("#444")) // Tło kursora + + // Układ + boardBorderStyle = lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("63")). + Padding(0, 1) + + logStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#AAA")) +) + +type GameState int + +const ( + StateWelcome GameState = iota + StatePlayerTurn + StateBotTurn + StateGameOver + StateHelp +) + +type Model struct { + state GameState + playerBoard *game.Board + botBoard *game.Board // Prawdziwa plansza bota (ukryta przed graczem) + botView [][]game.CellState // To co widzi gracz na planszy bota + + botAI *ai.QuantumHunter + + cursorX, cursorY int + + logs []string + + width, height int + winner string +} + +func InitialModel() Model { + // Konfiguracja gry + config := game.DefaultConfig + + // Inicjalizacja gracza + pBoard := game.NewBoard(config.Size) + // Gracz też dostaje optymalny układ, żeby było sprawiedliwie + if err := pBoard.PlaceShipsRandomly(config); err != nil { + panic("Nie udało się stworzyć planszy gracza") + } + + // Inicjalizacja bota + botAI := ai.NewQuantumHunter(config) + bBoard := botAI.GetOptimizedLayout() + + botView := make([][]game.CellState, config.Size) + for i := range botView { + botView[i] = make([]game.CellState, config.Size) + for j := range botView[i] { + botView[i][j] = game.CellWater + } + } + + return Model{ + state: StateWelcome, + playerBoard: pBoard, + botBoard: bBoard, + botView: botView, + botAI: botAI, + cursorX: 0, + cursorY: 0, + logs: []string{"Welcome to Battleship 2.0!", "Press '?' for help, Enter to start."}, + } +} + +func (m Model) Init() tea.Cmd { + return nil +} + +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + return m, tea.Quit + } + + if msg.String() == "?" { + if m.state != StateHelp { + m.log("Showing help...") + // Zapisz poprzedni stan? Uproszczenie: help jest osobnym stanem + // Ale lepiej zrobić toggle overlay. + // Tutaj: jeśli nie jesteśmy w menu, pauzujemy. + if m.state != StateGameOver && m.state != StateWelcome { + // Dla prostoty, logujemy legendę zamiast zmiany stanu ekranu + m.log("LEGEND: ■ Ship, X Hit, # Sunk, ○ Miss") + m.log("CONTROLS: Arrows to move, Enter/Space to fire") + return m, nil + } + } + } + + switch m.state { + case StateWelcome: + if msg.String() == "enter" { + m.state = StatePlayerTurn + m.log("Game started! Your turn.") + } + + case StatePlayerTurn: + switch msg.String() { + case "up", "k": + if m.cursorY > 0 { m.cursorY-- } + case "down", "j": + if m.cursorY < game.BoardSize-1 { m.cursorY++ } + case "left", "h": + if m.cursorX > 0 { m.cursorX-- } + case "right", "l": + if m.cursorX < game.BoardSize-1 { m.cursorX++ } + case "enter", "space": + m.handlePlayerShot() + } + + case StateGameOver: + if msg.String() == "enter" { + return InitialModel(), nil + } + } + } + + if m.state == StateBotTurn { + m.handleBotTurn() + } + + return m, nil +} + +func (m *Model) handlePlayerShot() { + if m.cursorY < 0 || m.cursorY >= game.BoardSize || m.cursorX < 0 || m.cursorX >= game.BoardSize { + return // Sanity check + } + + state := m.botView[m.cursorY][m.cursorX] + if state == game.CellHit || state == game.CellMiss || state == game.CellSunk { + m.log("Field already shot! Choose another.") + return + } + + // Strzał + res, msg := m.botBoard.Fire(m.cursorX, m.cursorY) + m.botView[m.cursorY][m.cursorX] = res // Aktualizacja lokalnego widoku + + if res == game.CellSunk { + m.syncBotView() // Pociągnij info o całym zatopionym statku + } + + m.log(fmt.Sprintf("You fired at %s: %s", coordStr(m.cursorX, m.cursorY), msg)) + + if m.botBoard.AllShipsSunk() { + m.state = StateGameOver + m.winner = "PLAYER" + m.log("Congratulations! You defeated the AI!") + return + } + + if res == game.CellMiss { + m.state = StateBotTurn + m.log("Bot is calculating 50,000 realities...") + } else { + m.log("Hit! Take another shot!") + } +} + +func (m *Model) handleBotTurn() { + bx, by, confidence := m.botAI.GetBestMove() + + // Formatowanie pewności siebie bota + confStr := fmt.Sprintf("%.1f%%", confidence*100) + + res, msg := m.playerBoard.Fire(bx, by) + + m.botAI.UpdateGameState(bx, by, res) + + botMsg := fmt.Sprintf("Bot targets %s (conf: %s): %s", coordStr(bx, by), confStr, msg) + m.log(botMsg) + + if m.playerBoard.AllShipsSunk() { + m.state = StateGameOver + m.winner = "BOT" + m.log("Game Over. The Quantum Hunter prevails.") + return + } + + if res == game.CellMiss { + m.state = StatePlayerTurn + m.log("Bot missed. Your turn.") + } else { + // Bot trafia - ma dodatkowy ruch (zasady Battleship często tak mówią, a to też przyspiesza grę bota) + // Tutaj trzymamy się zasady, że jak trafisz to strzelasz dalej. + m.log("Bot hit! Bot fires again...") + } +} + +func (m *Model) syncBotView() { + // Kopiuje stany Sunk i Miss (autouzupełnione) z botBoard do botView + for y := 0; y < game.BoardSize; y++ { + for x := 0; x < game.BoardSize; x++ { + realState := m.botBoard.Grid[y][x].State + currentView := m.botView[y][x] + + // Jeśli pole jest zatopione, pokaż to + if realState == game.CellSunk { + m.botView[y][x] = game.CellSunk + } + // Jeśli pole jest pudłem, a my go nie widzieliśmy (autouzupełnianie), pokaż to + if realState == game.CellMiss && currentView == game.CellWater { + // Sprawdź czy to autouzupełnianie (sąsiad Sunk) + if isNearSunk(m.botBoard, x, y) { + m.botView[y][x] = game.CellMiss + } + } + } + } +} + +func isNearSunk(b *game.Board, x, y int) bool { + // Sprawdź czy (x,y) sąsiaduje z jakimś Sunk (w tym po skosie) + for dy := -1; dy <= 1; dy++ { + for dx := -1; dx <= 1; dx++ { + nx, ny := x+dx, y+dy + if nx >= 0 && nx < game.BoardSize && ny >= 0 && ny < game.BoardSize { + if b.Grid[ny][nx].State == game.CellSunk { + return true + } + } + } + } + return false +} + +func (m Model) View() string { + if m.width == 0 { + return "Initializing Quantum Engine..." + } + + var s strings.Builder + + s.WriteString(titleStyle.Render(" BATTLESHIP 2.0 :: QUANTUM HUNTER EDITION ")) + s.WriteString("\n\n") + + playerView := renderBoard(m.playerBoard.Grid, true, -1, -1) // Gracz widzi swoje statki + // Gracz widzi botView (swoją wiedzę o planszy bota), kursor jest aktywny + botViewStr := renderBoardView(m.botView, m.state == StatePlayerTurn, m.cursorX, m.cursorY) + + boards := lipgloss.JoinHorizontal(lipgloss.Top, + boardBorderStyle.Render(lipgloss.JoinVertical(lipgloss.Center, "YOUR FLEET", playerView)), + " ", + boardBorderStyle.Render(lipgloss.JoinVertical(lipgloss.Center, "TARGET SECTOR", botViewStr)), + ) + + s.WriteString(boards) + s.WriteString("\n\n") + + s.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#AAA")).Bold(true).Render("COMBAT LOG:")) + s.WriteString("\n") + + // Pokaż ostatnie 6 logów + limit := 6 + start := 0 + if len(m.logs) > limit { + start = len(m.logs) - limit + } + for i := start; i < len(m.logs); i++ { + line := m.logs[i] + // Kolorowanie logów + if strings.Contains(line, "Hit") || strings.Contains(line, "targets") { + s.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500")).Render("> "+line) + "\n") + } else if strings.Contains(line, "Sunk") || strings.Contains(line, "defeated") || strings.Contains(line, "Game Over") { + s.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#F00")).Bold(true).Render("> "+line) + "\n") + } else { + s.WriteString(logStyle.Render("> "+line) + "\n") + } + } + + s.WriteString("\n") + + if m.state == StateGameOver { + resColor := lipgloss.Color("#0F0") + if m.winner == "BOT" { resColor = lipgloss.Color("#F00") } + s.WriteString(lipgloss.NewStyle().Foreground(resColor).Bold(true).Render(fmt.Sprintf("GAME OVER - %s WINS!", m.winner))) + s.WriteString("\nPress Enter to restart.") + } else if m.state == StateBotTurn { + s.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("208")).Render("Quantum AI is analyzing timelines...")) + } else { + s.WriteString(subtleStyle.Render("ARROWS/HJKL: Move | ENTER/SPACE: Fire | ?: Legend | Q: Quit")) + } + + return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, s.String()) +} + +func renderBoard(grid [][]game.Cell, showShips bool, curX, curY int) string { + var s strings.Builder + + // Nagłówek kolumn + s.WriteString(" ") + for i := 0; i < len(grid); i++ { + if i < 9 { s.WriteString(" ") } + s.WriteString(fmt.Sprintf("%d ", i+1)) + } + s.WriteString("\n") + + for y := 0; y < len(grid); y++ { + s.WriteString(fmt.Sprintf("%c ", 'A'+y)) + for x := 0; x < len(grid[y]); x++ { + cell := grid[y][x] + char := "" + + switch cell.State { + case game.CellWater: + if showShips && cell.ShipID != -1 { + char = shipStyle.Render() + } else { + char = waterStyle.Render() + } + case game.CellShip: // Stan wewnętrzny + if showShips { char = shipStyle.Render() } else { char = waterStyle.Render() } + case game.CellHit: + char = hitStyle.Render() + case game.CellMiss: + char = missStyle.Render() + case game.CellSunk: + char = sunkStyle.Render() + } + + if x == curX && y == curY { + char = cursorStyle.Render(char) + } + s.WriteString(char + " ") + } + s.WriteString("\n") + } + return s.String() +} + +func renderBoardView(view [][]game.CellState, isCursorActive bool, curX, curY int) string { + var s strings.Builder + s.WriteString(" ") + for i := 0; i < len(view); i++ { + if i < 9 { s.WriteString(" ") } + s.WriteString(fmt.Sprintf("%d ", i+1)) + } + s.WriteString("\n") + + for y := 0; y < len(view); y++ { + s.WriteString(fmt.Sprintf("%c ", 'A'+y)) + for x := 0; x < len(view[y]); x++ { + state := view[y][x] + char := "" + switch state { + case game.CellWater: + char = waterStyle.Render() + case game.CellHit: + char = hitStyle.Render() + case game.CellMiss: + char = missStyle.Render() + case game.CellSunk: + char = sunkStyle.Render() + } + + if isCursorActive && x == curX && y == curY { + char = cursorStyle.Render(char) + } + s.WriteString(char + " ") + } + s.WriteString("\n") + } + return s.String() +} + +func (m *Model) log(msg string) { + m.logs = append(m.logs, msg) +} + +func coordStr(x, y int) string { + return fmt.Sprintf("%c%d", 'A'+y, x+1) +}