diff --git a/part_5/tic_tac_toe_v4/game/game_core.go b/part_5/tic_tac_toe_v4/game/game_core.go index ee5d81f..f5404e3 100644 --- a/part_5/tic_tac_toe_v4/game/game_core.go +++ b/part_5/tic_tac_toe_v4/game/game_core.go @@ -21,8 +21,6 @@ type Game struct { Mode GameMode `json:"mode"` // Уровень сложности компьютера (только для PvC) Difficulty p.Difficulty `json:"difficulty,omitempty"` - // Флаг для определения текущего игрока - IsCurrentFirst bool `json:"is_current_first"` } // Создаем новую игру @@ -41,16 +39,15 @@ func NewGame(board b.Board, reader *bufio.Reader, saver s.IGameSaver, } return &Game{ - Board: &board, - Player: player1, - Player2: player2, - CurrentPlayer: player1, - Reader: reader, - State: playing, - Saver: saver, - Mode: mode, - Difficulty: difficulty, - IsCurrentFirst: true, + Board: &board, + Player: player1, + Player2: player2, + CurrentPlayer: player1, + Reader: reader, + State: playing, + Saver: saver, + Mode: mode, + Difficulty: difficulty, } } diff --git a/part_5/tic_tac_toe_v4/game/game_serialization.go b/part_5/tic_tac_toe_v4/game/game_serialization.go index 1378f64..73018d4 100644 --- a/part_5/tic_tac_toe_v4/game/game_serialization.go +++ b/part_5/tic_tac_toe_v4/game/game_serialization.go @@ -9,22 +9,14 @@ import ( s "tic-tac-toe/storage" ) -// Подготавливаем игру к сохранению -func (g *Game) prepareForSave() { - // Устанавливаем флаг текущего игрока - g.IsCurrentFirst = (g.CurrentPlayer == g.Player) -} - // Возвращаем снапшот игровой сессии func (g *Game) gameSnapshot() *m.GameSnapshot { - g.prepareForSave() return &m.GameSnapshot{ - Board: g.Board, - PlayerFigure: g.Player.GetFigure(), - State: int(g.State), - Mode: int(g.Mode), - Difficulty: g.Difficulty, - IsCurrentFirst: g.IsCurrentFirst, + Board: g.Board, + PlayerFigure: g.Player.GetFigure(), + State: int(g.State), + Mode: int(g.Mode), + Difficulty: g.Difficulty, } } @@ -38,7 +30,6 @@ func (g *Game) RestoreFromSnapshot( g.State = GameState(snapshot.State) g.Mode = GameMode(snapshot.Mode) g.Difficulty = snapshot.Difficulty - g.IsCurrentFirst = snapshot.IsCurrentFirst // Создаем объекты игроков g.Player = &p.HumanPlayer{Figure: snapshot.PlayerFigure} @@ -76,9 +67,5 @@ func (g *Game) recreatePlayersAfterLoad(reader *bufio.Reader) { } // Восстанавливаем указатель на текущего игрока - if g.IsCurrentFirst { - g.CurrentPlayer = g.Player - } else { - g.CurrentPlayer = g.Player2 - } + g.CurrentPlayer = g.Player } diff --git a/part_5/tic_tac_toe_v4/model/game_snapshot.go b/part_5/tic_tac_toe_v4/model/game_snapshot.go index a3bc1c1..85f7204 100644 --- a/part_5/tic_tac_toe_v4/model/game_snapshot.go +++ b/part_5/tic_tac_toe_v4/model/game_snapshot.go @@ -7,10 +7,9 @@ import ( // Структура для сериализации/десериализации игры type GameSnapshot struct { - Board *b.Board `json:"board"` - PlayerFigure b.BoardField `json:"player_figure"` - State int `json:"state"` - Mode int `json:"mode"` - Difficulty p.Difficulty `json:"difficulty,omitempty"` - IsCurrentFirst bool `json:"is_current_first"` + Board *b.Board `json:"board"` + PlayerFigure b.BoardField `json:"player_figure"` + State int `json:"state"` + Mode int `json:"mode"` + Difficulty p.Difficulty `json:"difficulty,omitempty"` } diff --git a/part_5/tic_tac_toe_v4/rr.json b/part_5/tic_tac_toe_v4/rr.json new file mode 100644 index 0000000..7a86a00 --- /dev/null +++ b/part_5/tic_tac_toe_v4/rr.json @@ -0,0 +1 @@ +{"board":{"board":[[1,0,0],[0,1,2],[0,2,0]],"size":3},"player_figure":1,"state":0,"mode":1} diff --git a/part_6/tic_tac_toe_v5/database/crud.go b/part_6/tic_tac_toe_v5/database/crud.go index fcd0e93..2d0eb6c 100644 --- a/part_6/tic_tac_toe_v5/database/crud.go +++ b/part_6/tic_tac_toe_v5/database/crud.go @@ -49,7 +49,6 @@ func (r *SQLiteRepository) SaveSnapshot( State: int(snapshot.State), Mode: int(snapshot.Mode), Difficulty: int(snapshot.Difficulty), - IsCurrentFirst: snapshot.IsCurrentFirst, PlayerNickName: player.NickName, }).Error } diff --git a/part_6/tic_tac_toe_v5/database/database.go b/part_6/tic_tac_toe_v5/database/database.go index 16af663..8bc6ae9 100644 --- a/part_6/tic_tac_toe_v5/database/database.go +++ b/part_6/tic_tac_toe_v5/database/database.go @@ -6,6 +6,7 @@ import ( "gorm.io/driver/sqlite" "gorm.io/gorm" + "gorm.io/gorm/logger" ) type SQLiteRepository struct { @@ -23,7 +24,11 @@ func NewSQLiteRepository() (*SQLiteRepository, error) { } // Открываем соединение с базой данных - db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{}) + db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{ + // Отключаем вывод логов SQL-запросов. Если хоитите видеть их, + // то закомментируйте следующую строку + Logger: logger.Default.LogMode(logger.Silent), + }) if err != nil { return nil, fmt.Errorf("failed to connect to database: %w", err) } diff --git a/part_6/tic_tac_toe_v5/database/models.go b/part_6/tic_tac_toe_v5/database/models.go index fbf376d..7909092 100644 --- a/part_6/tic_tac_toe_v5/database/models.go +++ b/part_6/tic_tac_toe_v5/database/models.go @@ -30,7 +30,6 @@ type GameSnapshot struct { State int `gorm:"not null"` Mode int `gorm:"not null"` Difficulty int `gorm:"not null"` - IsCurrentFirst bool `gorm:"not null"` PlayerNickName string `gorm:"not null"` Player *Player `gorm:"foreignKey:PlayerNickName;references:NickName"` } diff --git a/part_6/tic_tac_toe_v5/database/utils.go b/part_6/tic_tac_toe_v5/database/utils.go index ffcc00a..1cf5497 100644 --- a/part_6/tic_tac_toe_v5/database/utils.go +++ b/part_6/tic_tac_toe_v5/database/utils.go @@ -49,12 +49,11 @@ func (gs *GameSnapshot) ToModel() (*m.GameSnapshot, error) { } return &m.GameSnapshot{ - Board: &board, - PlayerFigure: b.BoardField(gs.PlayerFigure), - State: gs.State, - Mode: gs.Mode, - Difficulty: p.Difficulty(gs.Difficulty), - IsCurrentFirst: gs.IsCurrentFirst, - SnapshotName: gs.SnapshotName, + Board: &board, + PlayerFigure: b.BoardField(gs.PlayerFigure), + State: gs.State, + Mode: gs.Mode, + Difficulty: p.Difficulty(gs.Difficulty), + SnapshotName: gs.SnapshotName, }, nil } diff --git a/part_6/tic_tac_toe_v5/game/game_core.go b/part_6/tic_tac_toe_v5/game/game_core.go index f6818ca..7cdc2c2 100644 --- a/part_6/tic_tac_toe_v5/game/game_core.go +++ b/part_6/tic_tac_toe_v5/game/game_core.go @@ -21,8 +21,6 @@ type Game struct { Mode GameMode `json:"mode"` // Уровень сложности компьютера (только для PvC) Difficulty p.Difficulty `json:"difficulty,omitempty"` - // Флаг для определения текущего игрока - IsCurrentFirst bool `json:"is_current_first"` } // Создаем новую игру @@ -44,16 +42,15 @@ func NewGame( } return &Game{ - Board: &board, - Player: player1, - Player2: player2, - CurrentPlayer: player1, - Reader: reader, - State: playing, - repository: repository, - Mode: mode, - Difficulty: difficulty, - IsCurrentFirst: true, + Board: &board, + Player: player1, + Player2: player2, + CurrentPlayer: player1, + Reader: reader, + State: playing, + repository: repository, + Mode: mode, + Difficulty: difficulty, } } diff --git a/part_6/tic_tac_toe_v5/game/game_serialization.go b/part_6/tic_tac_toe_v5/game/game_serialization.go index fecfaf0..17b73d9 100644 --- a/part_6/tic_tac_toe_v5/game/game_serialization.go +++ b/part_6/tic_tac_toe_v5/game/game_serialization.go @@ -9,22 +9,14 @@ import ( p "tic-tac-toe/player" ) -// Подготавливаем игру к сохранению -func (g *Game) prepareForSave() { - // Устанавливаем флаг текущего игрока - g.IsCurrentFirst = (g.CurrentPlayer == g.Player) -} - // Возвращаем снапшот игровой сессии func (g *Game) gameSnapshot() *m.GameSnapshot { - g.prepareForSave() return &m.GameSnapshot{ - Board: g.Board, - PlayerFigure: g.Player.GetFigure(), - State: int(g.State), - Mode: int(g.Mode), - Difficulty: g.Difficulty, - IsCurrentFirst: g.IsCurrentFirst, + Board: g.Board, + PlayerFigure: g.Player.GetFigure(), + State: int(g.State), + Mode: int(g.Mode), + Difficulty: g.Difficulty, } } @@ -38,7 +30,6 @@ func (g *Game) RestoreFromSnapshot( g.State = GameState(snapshot.State) g.Mode = GameMode(snapshot.Mode) g.Difficulty = p.Difficulty(snapshot.Difficulty) - g.IsCurrentFirst = snapshot.IsCurrentFirst // Создаем объекты игроков g.Player = &p.HumanPlayer{Figure: snapshot.PlayerFigure} @@ -76,9 +67,5 @@ func (g *Game) recreatePlayersAfterLoad(reader *bufio.Reader) { } // Восстанавливаем указатель на текущего игрока - if g.IsCurrentFirst { - g.CurrentPlayer = g.Player - } else { - g.CurrentPlayer = g.Player2 - } + g.CurrentPlayer = g.Player } diff --git a/part_6/tic_tac_toe_v5/model/game_snapshot.go b/part_6/tic_tac_toe_v5/model/game_snapshot.go index fbe3184..f41d58e 100644 --- a/part_6/tic_tac_toe_v5/model/game_snapshot.go +++ b/part_6/tic_tac_toe_v5/model/game_snapshot.go @@ -7,11 +7,10 @@ import ( // Структура для сериализации/десериализации игры type GameSnapshot struct { - SnapshotName string `json:"snapshot_name"` - Board *b.Board `json:"board"` - PlayerFigure b.BoardField `json:"player_figure"` - State int `json:"state"` - Mode int `json:"mode"` - Difficulty p.Difficulty `json:"difficulty,omitempty"` - IsCurrentFirst bool `json:"is_current_first"` + SnapshotName string `json:"snapshot_name"` + Board *b.Board `json:"board"` + PlayerFigure b.BoardField `json:"player_figure"` + State int `json:"state"` + Mode int `json:"mode"` + Difficulty p.Difficulty `json:"difficulty,omitempty"` } diff --git a/part_6/tic_tac_toe_v5/tic_tac_toe.db b/part_6/tic_tac_toe_v5/tic_tac_toe.db index 2ae4b46..6a9f56d 100644 Binary files a/part_6/tic_tac_toe_v5/tic_tac_toe.db and b/part_6/tic_tac_toe_v5/tic_tac_toe.db differ diff --git a/part_7/tic_tac_toe_v6/database/crud.go b/part_7/tic_tac_toe_v6/database/crud.go index fcd0e93..2d0eb6c 100644 --- a/part_7/tic_tac_toe_v6/database/crud.go +++ b/part_7/tic_tac_toe_v6/database/crud.go @@ -49,7 +49,6 @@ func (r *SQLiteRepository) SaveSnapshot( State: int(snapshot.State), Mode: int(snapshot.Mode), Difficulty: int(snapshot.Difficulty), - IsCurrentFirst: snapshot.IsCurrentFirst, PlayerNickName: player.NickName, }).Error } diff --git a/part_7/tic_tac_toe_v6/database/database.go b/part_7/tic_tac_toe_v6/database/database.go index 16af663..8bc6ae9 100644 --- a/part_7/tic_tac_toe_v6/database/database.go +++ b/part_7/tic_tac_toe_v6/database/database.go @@ -6,6 +6,7 @@ import ( "gorm.io/driver/sqlite" "gorm.io/gorm" + "gorm.io/gorm/logger" ) type SQLiteRepository struct { @@ -23,7 +24,11 @@ func NewSQLiteRepository() (*SQLiteRepository, error) { } // Открываем соединение с базой данных - db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{}) + db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{ + // Отключаем вывод логов SQL-запросов. Если хоитите видеть их, + // то закомментируйте следующую строку + Logger: logger.Default.LogMode(logger.Silent), + }) if err != nil { return nil, fmt.Errorf("failed to connect to database: %w", err) } diff --git a/part_7/tic_tac_toe_v6/database/models.go b/part_7/tic_tac_toe_v6/database/models.go index fbf376d..7909092 100644 --- a/part_7/tic_tac_toe_v6/database/models.go +++ b/part_7/tic_tac_toe_v6/database/models.go @@ -30,7 +30,6 @@ type GameSnapshot struct { State int `gorm:"not null"` Mode int `gorm:"not null"` Difficulty int `gorm:"not null"` - IsCurrentFirst bool `gorm:"not null"` PlayerNickName string `gorm:"not null"` Player *Player `gorm:"foreignKey:PlayerNickName;references:NickName"` } diff --git a/part_7/tic_tac_toe_v6/database/utils.go b/part_7/tic_tac_toe_v6/database/utils.go index ffcc00a..1cf5497 100644 --- a/part_7/tic_tac_toe_v6/database/utils.go +++ b/part_7/tic_tac_toe_v6/database/utils.go @@ -49,12 +49,11 @@ func (gs *GameSnapshot) ToModel() (*m.GameSnapshot, error) { } return &m.GameSnapshot{ - Board: &board, - PlayerFigure: b.BoardField(gs.PlayerFigure), - State: gs.State, - Mode: gs.Mode, - Difficulty: p.Difficulty(gs.Difficulty), - IsCurrentFirst: gs.IsCurrentFirst, - SnapshotName: gs.SnapshotName, + Board: &board, + PlayerFigure: b.BoardField(gs.PlayerFigure), + State: gs.State, + Mode: gs.Mode, + Difficulty: p.Difficulty(gs.Difficulty), + SnapshotName: gs.SnapshotName, }, nil } diff --git a/part_7/tic_tac_toe_v6/game/game_core.go b/part_7/tic_tac_toe_v6/game/game_core.go index f6818ca..7cdc2c2 100644 --- a/part_7/tic_tac_toe_v6/game/game_core.go +++ b/part_7/tic_tac_toe_v6/game/game_core.go @@ -21,8 +21,6 @@ type Game struct { Mode GameMode `json:"mode"` // Уровень сложности компьютера (только для PvC) Difficulty p.Difficulty `json:"difficulty,omitempty"` - // Флаг для определения текущего игрока - IsCurrentFirst bool `json:"is_current_first"` } // Создаем новую игру @@ -44,16 +42,15 @@ func NewGame( } return &Game{ - Board: &board, - Player: player1, - Player2: player2, - CurrentPlayer: player1, - Reader: reader, - State: playing, - repository: repository, - Mode: mode, - Difficulty: difficulty, - IsCurrentFirst: true, + Board: &board, + Player: player1, + Player2: player2, + CurrentPlayer: player1, + Reader: reader, + State: playing, + repository: repository, + Mode: mode, + Difficulty: difficulty, } } diff --git a/part_7/tic_tac_toe_v6/game/game_serialization.go b/part_7/tic_tac_toe_v6/game/game_serialization.go index fecfaf0..17b73d9 100644 --- a/part_7/tic_tac_toe_v6/game/game_serialization.go +++ b/part_7/tic_tac_toe_v6/game/game_serialization.go @@ -9,22 +9,14 @@ import ( p "tic-tac-toe/player" ) -// Подготавливаем игру к сохранению -func (g *Game) prepareForSave() { - // Устанавливаем флаг текущего игрока - g.IsCurrentFirst = (g.CurrentPlayer == g.Player) -} - // Возвращаем снапшот игровой сессии func (g *Game) gameSnapshot() *m.GameSnapshot { - g.prepareForSave() return &m.GameSnapshot{ - Board: g.Board, - PlayerFigure: g.Player.GetFigure(), - State: int(g.State), - Mode: int(g.Mode), - Difficulty: g.Difficulty, - IsCurrentFirst: g.IsCurrentFirst, + Board: g.Board, + PlayerFigure: g.Player.GetFigure(), + State: int(g.State), + Mode: int(g.Mode), + Difficulty: g.Difficulty, } } @@ -38,7 +30,6 @@ func (g *Game) RestoreFromSnapshot( g.State = GameState(snapshot.State) g.Mode = GameMode(snapshot.Mode) g.Difficulty = p.Difficulty(snapshot.Difficulty) - g.IsCurrentFirst = snapshot.IsCurrentFirst // Создаем объекты игроков g.Player = &p.HumanPlayer{Figure: snapshot.PlayerFigure} @@ -76,9 +67,5 @@ func (g *Game) recreatePlayersAfterLoad(reader *bufio.Reader) { } // Восстанавливаем указатель на текущего игрока - if g.IsCurrentFirst { - g.CurrentPlayer = g.Player - } else { - g.CurrentPlayer = g.Player2 - } + g.CurrentPlayer = g.Player } diff --git a/part_7/tic_tac_toe_v6/model/game_snapshot.go b/part_7/tic_tac_toe_v6/model/game_snapshot.go index fbe3184..f41d58e 100644 --- a/part_7/tic_tac_toe_v6/model/game_snapshot.go +++ b/part_7/tic_tac_toe_v6/model/game_snapshot.go @@ -7,11 +7,10 @@ import ( // Структура для сериализации/десериализации игры type GameSnapshot struct { - SnapshotName string `json:"snapshot_name"` - Board *b.Board `json:"board"` - PlayerFigure b.BoardField `json:"player_figure"` - State int `json:"state"` - Mode int `json:"mode"` - Difficulty p.Difficulty `json:"difficulty,omitempty"` - IsCurrentFirst bool `json:"is_current_first"` + SnapshotName string `json:"snapshot_name"` + Board *b.Board `json:"board"` + PlayerFigure b.BoardField `json:"player_figure"` + State int `json:"state"` + Mode int `json:"mode"` + Difficulty p.Difficulty `json:"difficulty,omitempty"` } diff --git a/part_7/tic_tac_toe_v6/tic_tac_toe.db b/part_7/tic_tac_toe_v6/tic_tac_toe.db index fc58eaf..b9f3296 100644 Binary files a/part_7/tic_tac_toe_v6/tic_tac_toe.db and b/part_7/tic_tac_toe_v6/tic_tac_toe.db differ diff --git a/part_8/tic_tac_toe_v7/.vscode/launch.json b/part_8/tic_tac_toe_v7/.vscode/launch.json new file mode 100644 index 0000000..edd87ae --- /dev/null +++ b/part_8/tic_tac_toe_v7/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + "version": "0.2.0", + "configurations": [ + + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${fileDirname}", // <- ставим запятую + "console": "integratedTerminal" + } + ] +} diff --git a/part_8/tic_tac_toe_v7/board/board.go b/part_8/tic_tac_toe_v7/board/board.go new file mode 100644 index 0000000..5097b15 --- /dev/null +++ b/part_8/tic_tac_toe_v7/board/board.go @@ -0,0 +1,215 @@ +package board + +import ( + "fmt" + "sync" +) + +const ( + BoardDefaultSize int = 3 + BoardMinSize int = 3 + BoardMaxSize int = 9 +) + +type Board struct { + Board [][]BoardField `json:"board"` + Size int `json:"size"` +} + +func NewBoard(size int) *Board { + board := make([][]BoardField, size) + for i := range board { + board[i] = make([]BoardField, size) + } + return &Board{Board: board, Size: size} +} + +func (b *Board) IsEmpty() bool { + for i := range b.Size { + for j := range b.Size { + if b.Board[i][j] != Empty { + return false + } + } + } + return true +} + +// Отображение игрового поля +// ToStringSlice converts the board to a slice of strings for serialization. +func (b *Board) ToStringSlice() []string { + slice := make([]string, 0, b.Size*b.Size) + for i := 0; i < b.Size; i++ { + for j := 0; j < b.Size; j++ { + slice = append(slice, b.Board[i][j].String()) + } + } + return slice +} + +// Отображение игрового поля +func (b *Board) PrintBoard() { + fmt.Print(" ") + for i := range b.Size { + fmt.Printf("%d ", i+1) + } + fmt.Println() + for i := range b.Size { + fmt.Printf("%d ", i+1) + for j := range b.Size { + switch b.Board[i][j] { + case Empty: + fmt.Print(". ") + case Cross: + fmt.Print("X ") + case Nought: + fmt.Print("O ") + } + } + fmt.Println() + } +} + +// Проверка возможности и выполнения хода +func (b *Board) makeMove(x, y int) bool { + return b.Board[x][y] == Empty +} + +func (b *Board) SetSymbol(x, y int, player BoardField) bool { + if b.makeMove(x, y) { + b.Board[x][y] = player + return true + } + return false +} + +// Проверка выигрыша +func (b *Board) CheckWin(player BoardField) bool { + if b.Size <= 4 { + // Для маленьких досок используем обычную проверку + return b.checkWinSequential(player) + } + + // Для больших досок используем параллельную проверку + + // 3 направления проверок: строки/столбцы, 2 диагонали + resultChan := make(chan bool, 3) + var wg sync.WaitGroup + wg.Add(3) + + // Параллельная проверка строк и столбцов + go func() { + defer wg.Done() + for i := range b.Size { + rowWin, colWin := true, true + for j := 0; j < b.Size; j++ { + if b.Board[i][j] != player { + rowWin = false + } + if b.Board[j][i] != player { + colWin = false + } + } + if rowWin || colWin { + resultChan <- true + return // Нашли выигрыш, выходим из горутины + } + } + resultChan <- false + }() + + // Параллельная проверка главной диагонали + go func() { + defer wg.Done() + mainDiag := true + for i := range b.Size { + if b.Board[i][i] != player { + mainDiag = false + break + } + } + resultChan <- mainDiag + }() + + // Параллельная проверка побочной диагонали + go func() { + defer wg.Done() + antiDiag := true + for i := range b.Size { + if b.Board[i][b.Size-i-1] != player { + antiDiag = false + break + } + } + resultChan <- antiDiag + }() + + // Запускаем горутину, которая закроет канал после завершения всех проверок + go func() { + wg.Wait() + close(resultChan) + }() + + // Получаем результаты проверок с помощью for range. + // Этот цикл будет ждать, пока канал не будет закрыт. + for result := range resultChan { + if result { + return true // Найден выигрыш. + } + } + + return false +} + +// Оригинальный алгоритм проверки выигрыша для малых досок +func (b *Board) checkWinSequential(player BoardField) bool { + // Проверка строк и столбцов + for i := range b.Size { + rowWin, colWin := true, true + for j := range b.Size { + if b.Board[i][j] != player { + rowWin = false + } + if b.Board[j][i] != player { + colWin = false + } + } + if rowWin || colWin { + return true + } + } + + // Главная диагональ + mainDiag := true + for i := range b.Size { + if b.Board[i][i] != player { + mainDiag = false + break + } + } + if mainDiag { + return true + } + + // Побочная диагональ + antiDiag := true + for i := range b.Size { + if b.Board[i][b.Size-i-1] != player { + antiDiag = false + break + } + } + return antiDiag +} + +// Проверка на ничью +func (b *Board) CheckDraw() bool { + for i := range b.Size { + for j := range b.Size { + if b.Board[i][j] == Empty { + return false + } + } + } + return true +} diff --git a/part_8/tic_tac_toe_v7/board/board_cell_type.go b/part_8/tic_tac_toe_v7/board/board_cell_type.go new file mode 100644 index 0000000..fcc17ec --- /dev/null +++ b/part_8/tic_tac_toe_v7/board/board_cell_type.go @@ -0,0 +1,23 @@ +package board + +type BoardField int + +// фигуры в клетке поля +const ( + Empty BoardField = iota + Cross + Nought +) + +func (bf BoardField) String() string { + switch bf { + case Empty: + return "." + case Cross: + return "X" + case Nought: + return "O" + default: + return "?" + } +} diff --git a/part_8/tic_tac_toe_v7/client/client.go b/part_8/tic_tac_toe_v7/client/client.go new file mode 100644 index 0000000..0746e7b --- /dev/null +++ b/part_8/tic_tac_toe_v7/client/client.go @@ -0,0 +1,114 @@ +package client + +import ( + "encoding/json" + "fmt" + "log" + "net" + "sync" + "time" + + b "tic-tac-toe/board" + "tic-tac-toe/network" +) + +// Client represents the client-side application. +type Client struct { + conn net.Conn + board *b.Board + mySymbol b.BoardField + currentPlayer b.BoardField + playerName string + roomName string + state State + mutex sync.RWMutex + lastMsgTime time.Time +} + +// NewClient creates a new client and connects to the server. +func NewClient(addr string) (*Client, error) { + conn, err := net.Dial("tcp", addr) + if err != nil { + return nil, err + } + + return &Client{ + conn: conn, + state: waitNickNameConfirm, + mySymbol: b.Empty, // Will be set upon joining a room + }, nil +} + +func (c *Client) setNickname(nickname string) { + c.playerName = nickname +} + +func (c *Client) getState() State { + c.mutex.RLock() + defer c.mutex.RUnlock() + return c.state +} + +func (c *Client) setState(state State) { + c.mutex.Lock() + defer c.mutex.Unlock() + + // Display a message only when transitioning to opponentMove + if state == opponentMove && c.state != opponentMove { + fmt.Println("\nWaiting for opponent's move...") + } else if state == waitingOpponentInRoom && c.state != waitingOpponentInRoom { + fmt.Println("\nWaiting for opponent to join...") + } + + c.state = state +} + +// Start begins the client's main loop for sending and receiving messages. +func (c *Client) Start() { + defer c.conn.Close() + + fmt.Println("Connected to server. ") + go c.readFromServer() + c.menu() +} + +// readFromServer continuously reads messages from the server and handles them. +func (c *Client) readFromServer() { + decoder := json.NewDecoder(c.conn) + for { + var msg network.Message + if err := decoder.Decode(&msg); err != nil { + log.Printf("Disconnected from server: %v", err) + return // Exit goroutine if connection is lost + } + + switch msg.Cmd { + case network.CmdRoomJoinResponse: + c.handleRoomJoinResponse(msg.Payload) + case network.CmdInitGame: + c.handleInitGame(msg.Payload) + case network.CmdUpdateState: + c.handleUpdateState(msg.Payload) + case network.CmdEndGame: + c.handleEndGame(msg.Payload) + case network.CmdError: + c.handleError(msg.Payload) + case network.CmdRoomListResponse: + c.handleRoomListResponse(msg.Payload) + case network.CmdNickNameResponse: + c.handleNickNameResponse(msg.Payload) + case network.CmdOpponentLeft: + c.handleOpponentLeft(msg.Payload) + case network.CmdFinishedGamesResponse: + c.handleFinishedGamesResponse(msg.Payload) + case network.CmdFinishedGameResponse: + c.handleFinishedGameResponse(msg.Payload) + default: + log.Printf( + "Received unhandled message type '%s' "+ + "from server. Payload: %s\n> ", + msg.Cmd, string(msg.Payload), + ) + } + } +} diff --git a/part_8/tic_tac_toe_v7/client/client_state.go b/part_8/tic_tac_toe_v7/client/client_state.go new file mode 100644 index 0000000..d13fff8 --- /dev/null +++ b/part_8/tic_tac_toe_v7/client/client_state.go @@ -0,0 +1,15 @@ +package client + +type State int + +const ( + waitNickNameConfirm State = iota + mainMenu + waitRoomJoin + playing + playerMove + opponentMove + endGame + waitingOpponentInRoom + waitResponseFromServer +) diff --git a/part_8/tic_tac_toe_v7/client/menu.go b/part_8/tic_tac_toe_v7/client/menu.go new file mode 100644 index 0000000..200bae0 --- /dev/null +++ b/part_8/tic_tac_toe_v7/client/menu.go @@ -0,0 +1,158 @@ +package client + +import ( + "bufio" + "encoding/json" + "fmt" + "log" + "os" + "strconv" + "strings" + "tic-tac-toe/network" + "time" +) + +func (c *Client) menu() { + reader := bufio.NewReader(os.Stdin) + encoder := json.NewEncoder(c.conn) + + fmt.Print("Enter your nickname: ") + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(input) + c.playerName = input + var msg network.Message + + msg.Cmd = network.CmdNickname + payloadData := network.NicknameRequest{Nickname: c.playerName} + jsonPayload, err := json.Marshal(payloadData) + if err != nil { + log.Printf("Error marshalling payload for command %s: %v", msg.Cmd, err) + return + } + msg.Payload = jsonPayload + if err := encoder.Encode(msg); err != nil { + log.Printf("Failed to send message to server: %v. Disconnecting.", err) + return // Exit if we can't send to server + } + + for { + switch c.getState() { + case waitNickNameConfirm: + time.Sleep(100 * time.Millisecond) + continue + case mainMenu: + c.mainMenu(reader, encoder) + case playerMove: + c.playing(reader, encoder) + case opponentMove: + // Just wait silently for opponent's move + time.Sleep(1000 * time.Millisecond) + continue + case endGame: + fmt.Println("\nGame has ended. Restarting in 10 seconds...") + time.Sleep(10 * time.Second) + continue + case waitResponseFromServer: + time.Sleep(100 * time.Millisecond) + continue + case waitingOpponentInRoom: + // Rate-limit messages to once every 3 seconds + now := time.Now() + if now.Sub(c.lastMsgTime) > 3*time.Second { + c.lastMsgTime = now + fmt.Println("\nWaiting for opponent to join...") + fmt.Println("Press 'q' and Enter to return to main menu") + fmt.Print("> ") + } + + // Poll for input every cycle but don't block + var buffer [1]byte + n, _ := os.Stdin.Read(buffer[:]) + if n > 0 && (buffer[0] == 'q' || buffer[0] == 'Q') { + fmt.Println("Leaving room...") + var msg network.Message + msg.Cmd = network.CmdLeaveRoomRequest + payload := network.LeaveRoomRequest{ + RoomName: c.roomName, + PlayerName: c.playerName, + } + jsonPayload, _ := json.Marshal(payload) + msg.Payload = jsonPayload + encoder.Encode(msg) + c.setState(mainMenu) + continue + } + + // Short sleep to avoid CPU spinning + time.Sleep(100 * time.Millisecond) + continue + } + } +} + +func (c *Client) mainMenu(reader *bufio.Reader, encoder *json.Encoder) { + var msg network.Message + + fmt.Println("Enter command:") + fmt.Println("1 - Get room list") + fmt.Println("2 - Join room") + fmt.Println("3 - Get finished games") + fmt.Println("4 - Get finished game by id") + fmt.Println("5 - Exit") + fmt.Print("> ") + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(input) + + command, err := strconv.Atoi(input) + if err != nil { + fmt.Println("Invalid command.") + return + } + + switch command { + case 1: + msg.Cmd = network.CmdListRoomsRequest + encoder.Encode(msg) + c.setState(waitResponseFromServer) + case 2: + fmt.Print("Enter room name: ") + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(input) + c.roomName = input + msg.Cmd = network.CmdJoinRoomRequest + payload := network.JoinRoomRequest{ + RoomName: c.roomName, + PlayerName: c.playerName, + } + jsonPayload, _ := json.Marshal(payload) + msg.Payload = jsonPayload + encoder.Encode(msg) + c.setState(waitResponseFromServer) + case 3: + msg.Cmd = network.CmdFinishedGamesRequest + encoder.Encode(msg) + c.setState(waitResponseFromServer) + case 4: + fmt.Print("Enter game id: ") + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(input) + + gameId, err := strconv.Atoi(input) + if err != nil { + fmt.Println("Invalid game id.") + return + } + + msg.Cmd = network.CmdFinishedGameByIdRequest + payload := network.GetFinishedGameByIdRequest{GameID: gameId} + jsonPayload, _ := json.Marshal(payload) + msg.Payload = jsonPayload + encoder.Encode(msg) + c.setState(waitResponseFromServer) + case 5: + os.Exit(0) + default: + fmt.Println("Unknown command.") + return + } +} diff --git a/part_8/tic_tac_toe_v7/client/playing.go b/part_8/tic_tac_toe_v7/client/playing.go new file mode 100644 index 0000000..56b2952 --- /dev/null +++ b/part_8/tic_tac_toe_v7/client/playing.go @@ -0,0 +1,60 @@ +package client + +import ( + "bufio" + "encoding/json" + "fmt" + "strconv" + "strings" + "tic-tac-toe/network" +) + +func (c *Client) playing(reader *bufio.Reader, encoder *json.Encoder) { + fmt.Printf("\nEnter command: or q for exit to main menu\n> ") + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(input) + + if input == "q" { + var msg network.Message + msg.Cmd = network.CmdLeaveRoomRequest + payload := network.LeaveRoomRequest{ + RoomName: c.roomName, + PlayerName: c.playerName, + } + jsonPayload, _ := json.Marshal(payload) + msg.Payload = jsonPayload + encoder.Encode(msg) + c.setState(mainMenu) + return + } + + parts := strings.Fields(input) + if len(parts) != 2 { + fmt.Println("Usage: ") + return + } + + var msg network.Message + row, err1 := strconv.Atoi(parts[0]) + col, err2 := strconv.Atoi(parts[1]) + if err1 != nil || err2 != nil { + fmt.Println("Row and column must be numbers.") + return + } + + if !c.validateMove(row, col) { + return // validateMove prints the error + } + + msg.Cmd = network.CmdMakeMoveRequest + payload := network.MakeMoveRequest{ + RoomName: c.roomName, + PlayerName: c.playerName, + PositionRow: row - 1, + PositionCol: col - 1, + } + jsonPayload, _ := json.Marshal(payload) + msg.Payload = jsonPayload + encoder.Encode(msg) + c.setState(waitResponseFromServer) +} diff --git a/part_8/tic_tac_toe_v7/client/server_message_handlers.go b/part_8/tic_tac_toe_v7/client/server_message_handlers.go new file mode 100644 index 0000000..121ccc8 --- /dev/null +++ b/part_8/tic_tac_toe_v7/client/server_message_handlers.go @@ -0,0 +1,211 @@ +package client + +import ( + "encoding/json" + "fmt" + "log" + + b "tic-tac-toe/board" + g "tic-tac-toe/game" // Added for g.GameMode + "tic-tac-toe/network" +) + +// handleRoomJoinResponse processes the RoomJoinResponse message from the server. +func (c *Client) handleRoomJoinResponse(payload json.RawMessage) { + var res network.RoomJoinResponse + if err := json.Unmarshal(payload, &res); err == nil { + c.mySymbol = res.PlayerSymbol + c.roomName = res.RoomName + if res.Board.Size > 0 { // Check if board is valid + c.board = &res.Board + fmt.Printf("\nSuccessfully joined room '%s' as %s.\n", res.RoomName, res.PlayerSymbol) + c.board.PrintBoard() + } else { + fmt.Printf("\nSuccessfully joined room '%s' as %s. Waiting for game to start...\n", res.RoomName, res.PlayerSymbol) + } + } else { + log.Printf("Error unmarshalling RoomJoinResponse: %v", err) + } + c.setState(waitingOpponentInRoom) +} + +// handleInitGame processes the InitGameResponse message from the server. +func (c *Client) handleInitGame(payload json.RawMessage) { + var res network.InitGameResponse + if err := json.Unmarshal(payload, &res); err == nil { + c.board = &res.Board + c.currentPlayer = res.CurrentPlayer + fmt.Println("\n--- Game Started ---") + c.board.PrintBoard() + c.printTurnInfo() + if res.CurrentPlayer == c.mySymbol { + c.setState(playerMove) + } else { + c.setState(opponentMove) + } + } else { + log.Printf("Error unmarshalling InitGameResponse: %v", err) + } +} + +// handleUpdateState processes the GameStateUpdate message from the server. +func (c *Client) handleUpdateState(payload json.RawMessage) { + var res network.GameStateUpdate + if err := json.Unmarshal(payload, &res); err == nil { + c.board = &res.Board + c.currentPlayer = res.CurrentPlayer + fmt.Println("\n--- Game State Update ---") + c.board.PrintBoard() + c.printTurnInfo() + if res.CurrentPlayer == c.mySymbol { + c.setState(playerMove) + } else { + c.setState(opponentMove) + } + } else { + log.Printf("Error unmarshalling GameStateUpdate: %v", err) + } +} + +// handleEndGame processes the EndGameResponse message from the server. +func (c *Client) handleEndGame(payload json.RawMessage) { + var res network.EndGameResponse + if err := json.Unmarshal(payload, &res); err == nil { + c.board = &res.Board + fmt.Println("\n--- Game Over ---") + c.board.PrintBoard() + if res.CurrentPlayer == b.Empty { + fmt.Println("It's a Draw!") + } else { + fmt.Printf("Player %s wins!\n", res.CurrentPlayer) + } + c.setState(endGame) + fmt.Print("> ") + } else { + log.Printf("Error unmarshalling EndGameResponse: %v", err) + } +} + +// handleError processes the ErrorResponse message from the server. +func (c *Client) handleError(payload json.RawMessage) { + var errPayload network.ErrorResponse + if err := json.Unmarshal(payload, &errPayload); err == nil { + fmt.Printf("\nServer Error: %s\n> ", errPayload.Message) + } else { + log.Printf("Error unmarshalling ErrorResponse: %v", err) + } + c.setState(mainMenu) +} + +// gameModeToString converts GameMode to a string representation. +func gameModeToString(mode g.GameMode) string { + switch mode { + case g.PvP: + return "PvP" + case g.PvC: + return "PvC" + default: + return "Unknown" + } +} + +func difficultyToString(difficulty g.Difficulty) string { + switch difficulty { + case g.Easy: + return "Easy" + case g.Medium: + return "Medium" + case g.Hard: + return "Hard" + default: + return "" + } +} + +// handleRoomListResponse processes the RoomListResponse message from the server. +func (c *Client) handleRoomListResponse(payload json.RawMessage) { + var roomList network.RoomListResponse + if err := json.Unmarshal(payload, &roomList); err == nil { + fmt.Println("\nAvailable rooms:") + if len(roomList.Rooms) == 0 { + fmt.Println("No rooms available.") + } else { + for _, room := range roomList.Rooms { + fmt.Printf("- %s (Board Size: %dx%d, Full: %t, Mode: %s, Difficulty: %s)\n", + room.Name, + room.BoardSize, room.BoardSize, + room.IsFull, + gameModeToString(room.GameMode), + difficultyToString(room.Difficult), + ) + } + } + } else { + log.Printf("Error unmarshalling RoomListResponse: %v", err) + } + c.setState(mainMenu) +} + +// handleNickNameResponse processes the NickNameResponse message from the server. +func (c *Client) handleNickNameResponse(payload json.RawMessage) { + var res network.NickNameResponse + if err := json.Unmarshal(payload, &res); err == nil { + fmt.Printf("\nWelcome, %s!\n> ", res.Nickname) + c.setNickname(res.Nickname) + c.setState(mainMenu) + } else { + log.Printf("Error unmarshalling NickNameResponse: %v", err) + } +} + +// handleOpponentLeft processes the OpponentLeft message from the server. +func (c *Client) handleOpponentLeft(payload json.RawMessage) { + var res network.OpponentLeft + if err := json.Unmarshal(payload, &res); err == nil { + fmt.Printf("\nPlayer '%s' has left the game.\n> ", res.Nickname) + } else { + log.Printf("Error unmarshalling OpponentLeft: %v", err) + } + c.setState(waitingOpponentInRoom) +} + +// handleFinishedGamesResponse processes the FinishedGamesResponse message from the server. +func (c *Client) handleFinishedGamesResponse(payload json.RawMessage) { + var res network.FinishedGamesResponse + if err := json.Unmarshal(payload, &res); err == nil { + fmt.Println("\nFinished games:") + if res.Games == nil || len(*res.Games) == 0 { + fmt.Println("No finished games.") + } else { + for _, game := range *res.Games { + fmt.Printf("- Game #%d: %s vs %s (Winner: %s) at %v\n", + game.ID, game.WinnerName, + game.AnotherPlayerName, game.WinnerName, + game.Time.Format("2006-01-02 15:04:05"), + ) + } + } + } else { + log.Printf("Error unmarshalling FinishedGamesResponse: %v", err) + } + c.setState(mainMenu) +} + +// handleFinishedGameResponse processes the FinishedGameResponse message from the server. +func (c *Client) handleFinishedGameResponse(payload json.RawMessage) { + var res network.FinishedGameResponse + if err := json.Unmarshal(payload, &res); err == nil { + fmt.Println("\nFinished game:") + fmt.Printf("- Game #%d: %s vs %s (Winner: %s) at %v\n", + res.Game.ID, res.Game.WinnerName, + res.Game.AnotherPlayerName, res.Game.WinnerName, + res.Game.Time.Format("2006-01-02 15:04:05"), + ) + c.board = res.Game.Board + c.board.PrintBoard() + fmt.Println() + } else { + log.Printf("Error unmarshalling FinishedGameResponse: %v", err) + } + c.setState(mainMenu) +} diff --git a/part_8/tic_tac_toe_v7/client/utils.go b/part_8/tic_tac_toe_v7/client/utils.go new file mode 100644 index 0000000..279ca0d --- /dev/null +++ b/part_8/tic_tac_toe_v7/client/utils.go @@ -0,0 +1,38 @@ +package client + +import ( + "fmt" + b "tic-tac-toe/board" +) + +func (c *Client) printTurnInfo() { + if c.board == nil { + return + } + if c.currentPlayer == c.mySymbol { + fmt.Println("It's your turn.") + } else if c.currentPlayer != b.Empty { + fmt.Printf("It's player %s's turn.\n", c.currentPlayer) + } else { + // Game might be over or in an intermediate state + } + fmt.Print("> ") +} + +// validateMove checks if a move is valid based on the local board state. +func (c *Client) validateMove(row, col int) bool { + if c.board == nil { + fmt.Println("Game has not started yet.") + return false + } + if row < 1 || row > c.board.Size || col < 1 || col > c.board.Size { + fmt.Printf("Invalid move. Row and column must be between 1 and %d.\n", c.board.Size) + return false + } + // Convert to 0-indexed for board access + if c.board.Board[row-1][col-1] != b.Empty { + fmt.Println("Invalid move. Cell is already occupied.") + return false + } + return true +} diff --git a/part_8/tic_tac_toe_v7/database/crud.go b/part_8/tic_tac_toe_v7/database/crud.go new file mode 100644 index 0000000..6a7fdef --- /dev/null +++ b/part_8/tic_tac_toe_v7/database/crud.go @@ -0,0 +1,47 @@ +package database + +import ( + "encoding/json" + + m "tic-tac-toe/model" +) + +func (r *SQLiteRepository) SaveFinishedGame( + snapshot *m.FinishGameSnapshot) error { + boardJSON, err := json.Marshal(snapshot.Board) + if err != nil { + return err + } + + return r.db.Create(&PlayerFinishGame{ + BoardJSON: boardJSON, + PlayerFigure: int(snapshot.PlayerFigure), + WinnerName: snapshot.WinnerName, + AnotherPlayerName: snapshot.AnotherPlayerName, + Time: snapshot.Time, + }).Error +} + +func (r *SQLiteRepository) GetAllFinishedGames() (*[]m.FinishGameSnapshot, error) { + var playerFinishGames []PlayerFinishGame + if err := r.db.Find(&playerFinishGames).Error; err != nil { + return nil, err + } + var finishGameSnapshots []m.FinishGameSnapshot + for _, playerFinishGame := range playerFinishGames { + temp, err := playerFinishGame.ToModel() + if err != nil { + return nil, err + } + finishGameSnapshots = append(finishGameSnapshots, *temp) + } + return &finishGameSnapshots, nil +} + +func (r *SQLiteRepository) GetFinishedGameById(id int) (*m.FinishGameSnapshot, error) { + var playerFinishGame PlayerFinishGame + if err := r.db.Where("id = ?", id).First(&playerFinishGame).Error; err != nil { + return nil, err + } + return playerFinishGame.ToModel() +} diff --git a/part_8/tic_tac_toe_v7/database/database.go b/part_8/tic_tac_toe_v7/database/database.go new file mode 100644 index 0000000..88be6a7 --- /dev/null +++ b/part_8/tic_tac_toe_v7/database/database.go @@ -0,0 +1,45 @@ +package database + +import ( + "fmt" + "os" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +type SQLiteRepository struct { + db *gorm.DB // заменили на *gorm.DB +} + +func NewSQLiteRepository() (*SQLiteRepository, error) { + // Создаем репозиторий + repository := &SQLiteRepository{} + + // Проверяем существование файла базы данных + dbExists := true + if _, err := os.Stat(dbName); os.IsNotExist(err) { + dbExists = false + } + + // Открываем соединение с базой данных + db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{}) + if err != nil { + return nil, fmt.Errorf("failed to connect to database: %w", err) + } + + // Сохраняем соединение в репозитории + repository.db = db + + // Если база данных только что создана, выполняем миграцию + if !dbExists { + fmt.Println("Creating new database schema") + if err := db.AutoMigrate(&PlayerFinishGame{}); err != nil { + return nil, fmt.Errorf("failed to migrate database: %w", err) + } + } else { + fmt.Println("Using existing database") + } + + return repository, nil +} diff --git a/part_8/tic_tac_toe_v7/database/db_definition.go b/part_8/tic_tac_toe_v7/database/db_definition.go new file mode 100644 index 0000000..475aea3 --- /dev/null +++ b/part_8/tic_tac_toe_v7/database/db_definition.go @@ -0,0 +1,12 @@ +package database + +import "errors" + +const dbName = "tic_tac_toe.db" + +var ( + ErrDuplicate = errors.New("record already exists") + ErrNotExists = errors.New("row not exists") + ErrUpdateFailed = errors.New("update failed") + ErrDeleteFailed = errors.New("delete failed") +) diff --git a/part_8/tic_tac_toe_v7/database/i_repository.go b/part_8/tic_tac_toe_v7/database/i_repository.go new file mode 100644 index 0000000..55b5d8d --- /dev/null +++ b/part_8/tic_tac_toe_v7/database/i_repository.go @@ -0,0 +1,13 @@ +package database + +import "tic-tac-toe/model" + +// Интерфейс для работы с базой данных +type IRepository interface { + // Сохраняет информацию о завершенной игре + SaveFinishedGame(snapshot *model.FinishGameSnapshot) error + // Получает все завершенные игры для указанного игрока + GetAllFinishedGames() (*[]model.FinishGameSnapshot, error) + // Получает конкретную завершенную игру по ID + GetFinishedGameById(id int) (*model.FinishGameSnapshot, error) +} diff --git a/part_8/tic_tac_toe_v7/database/models.go b/part_8/tic_tac_toe_v7/database/models.go new file mode 100644 index 0000000..901f260 --- /dev/null +++ b/part_8/tic_tac_toe_v7/database/models.go @@ -0,0 +1,14 @@ +package database + +import "time" + +// PlayerFinishGame представляет модель таблицы +// для хранения завершенной игры в БД +type PlayerFinishGame struct { + ID int `gorm:"primary_key;autoIncrement;not null"` + WinnerName string `gorm:"not null"` + AnotherPlayerName string `gorm:"not null"` + BoardJSON []byte `gorm:"type:json;not null"` + PlayerFigure int `gorm:"not null"` + Time time.Time `gorm:"not null"` +} diff --git a/part_8/tic_tac_toe_v7/database/utils.go b/part_8/tic_tac_toe_v7/database/utils.go new file mode 100644 index 0000000..7c4af0b --- /dev/null +++ b/part_8/tic_tac_toe_v7/database/utils.go @@ -0,0 +1,31 @@ +package database + +import ( + "encoding/json" + + b "tic-tac-toe/board" + m "tic-tac-toe/model" +) + +// Задаем имя таблицы для структуры PlayerFinishGame +func (pfg *PlayerFinishGame) TableName() string { + return "player_finish_games" +} + +// Преобразуем таблицу PlayerFinishGame в модель PlayerFinishGame +// из пакета model +func (f *PlayerFinishGame) ToModel() (*m.FinishGameSnapshot, error) { + var board b.Board + if err := json.Unmarshal(f.BoardJSON, &board); err != nil { + return nil, err + } + + return &m.FinishGameSnapshot{ + ID: f.ID, + Board: &board, + PlayerFigure: b.BoardField(f.PlayerFigure), + WinnerName: f.WinnerName, + AnotherPlayerName: f.AnotherPlayerName, + Time: f.Time, + }, nil +} diff --git a/part_8/tic_tac_toe_v7/game/game_state.go b/part_8/tic_tac_toe_v7/game/game_state.go new file mode 100644 index 0000000..24468ee --- /dev/null +++ b/part_8/tic_tac_toe_v7/game/game_state.go @@ -0,0 +1,31 @@ +package game + +type GameState int + +// состояние игрового процесса +const ( + WaitingOpponent GameState = iota + Draw + CrossWin + NoughtWin + CrossStep + NoughtStep +) + +// Режим игры +type GameMode int + +const ( + PvP GameMode = iota + PvC +) + +// Уровни сложности компьютера +type Difficulty int + +const ( + None Difficulty = iota + Easy + Medium + Hard +) diff --git a/part_8/tic_tac_toe_v7/go.mod b/part_8/tic_tac_toe_v7/go.mod new file mode 100644 index 0000000..ed636a3 --- /dev/null +++ b/part_8/tic_tac_toe_v7/go.mod @@ -0,0 +1,16 @@ +module tic-tac-toe + +go 1.24.0 + +require github.com/mattn/go-sqlite3 v1.14.28 // indirect + +require ( + gorm.io/driver/sqlite v1.6.0 + gorm.io/gorm v1.30.0 +) + +require ( + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + golang.org/x/text v0.26.0 // indirect +) diff --git a/part_8/tic_tac_toe_v7/go.sum b/part_8/tic_tac_toe_v7/go.sum new file mode 100644 index 0000000..2c20abd --- /dev/null +++ b/part_8/tic_tac_toe_v7/go.sum @@ -0,0 +1,14 @@ +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= +gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= diff --git a/part_8/tic_tac_toe_v7/main.go b/part_8/tic_tac_toe_v7/main.go new file mode 100644 index 0000000..027d86f --- /dev/null +++ b/part_8/tic_tac_toe_v7/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "flag" + "log" + + "tic-tac-toe/client" + "tic-tac-toe/database" + "tic-tac-toe/server" +) + +func main() { + mode := flag.String( + "mode", "server", + "start in 'server' or 'client' mode", + ) + addr := flag.String("addr", ":8088", "address to run on") + flag.Parse() + + switch *mode { + case "server": + repository, err := database.NewSQLiteRepository() + if err != nil { + log.Fatalf("Failed to create repository: %v", err) + } + srv, err := server.NewServer(*addr, repository) + if err != nil { + log.Fatalf("Failed to create server: %v", err) + } + srv.Start() + case "client": + cli, err := client.NewClient(*addr) + if err != nil { + log.Fatalf("Failed to connect to server: %v", err) + } + cli.Start() + default: + log.Fatalf( + "Unknown mode: %s. Use 'server' or 'client'.", *mode, + ) + } +} diff --git a/part_8/tic_tac_toe_v7/model/finish_game_shapshot.go b/part_8/tic_tac_toe_v7/model/finish_game_shapshot.go new file mode 100644 index 0000000..ac739b1 --- /dev/null +++ b/part_8/tic_tac_toe_v7/model/finish_game_shapshot.go @@ -0,0 +1,15 @@ +package model + +import ( + "tic-tac-toe/board" + "time" +) + +type FinishGameSnapshot struct { + ID int `json:"id"` + Board *board.Board `json:"board"` + PlayerFigure board.BoardField `json:"player_figure"` + WinnerName string `json:"winner_name"` + AnotherPlayerName string `json:"another_player_name"` + Time time.Time `json:"time"` +} diff --git a/part_8/tic_tac_toe_v7/network/client_to_server.go b/part_8/tic_tac_toe_v7/network/client_to_server.go new file mode 100644 index 0000000..42595e6 --- /dev/null +++ b/part_8/tic_tac_toe_v7/network/client_to_server.go @@ -0,0 +1,52 @@ +package network + +const ( + // Client to Server Commands + CmdNickname Command = "nickname" + CmdJoinRoomRequest Command = "join_room" + CmdLeaveRoomRequest Command = "leave_room" + CmdListRoomsRequest Command = "list_rooms" + CmdMakeMoveRequest Command = "make_move" + CmdFinishedGamesRequest Command = "get_finished_games" + CmdFinishedGameByIdRequest Command = "get_finished_game_by_id" +) + +// LoginRequest отправляется клиентом для входа в систему. +type NicknameRequest struct { + Nickname string `json:"nickname"` +} + +// JoinRoomRequest отправляется клиентом для подключения к существующей комнате. +type JoinRoomRequest struct { + RoomName string `json:"room_name"` + PlayerName string `json:"player_name"` +} + +// LeaveRoomRequest отправляется клиентом для выхода из текущей комнаты. +type LeaveRoomRequest struct { + RoomName string `json:"room_name"` + PlayerName string `json:"player_name"` +} + +// ListRoomsRequest отправляется клиентом для получения списка доступных комнат. +// Обычно для этого запроса не требуется специальная полезная нагрузка. +type ListRoomsRequest struct { +} + +// MakeMoveRequest отправляется клиентом для совершения хода в игре. +type MakeMoveRequest struct { + RoomName string `json:"room_name"` + PlayerName string `json:"player_name"` + PositionRow int `json:"position_row"` + PositionCol int `json:"position_col"` +} + +// GetFinishedGamesRequest отправляется клиентом для получения списка завершенных игр. +// Обычно для этого запроса не требуется специальная полезная нагрузка, если запрашиваются все игры для пользователя. +type GetFinishedGamesRequest struct { +} + +// GetFinishedGameByIdRequest отправляется клиентом для получения конкретной завершенной игры. +type GetFinishedGameByIdRequest struct { + GameID int `json:"game_id"` +} diff --git a/part_8/tic_tac_toe_v7/network/protocol.go b/part_8/tic_tac_toe_v7/network/protocol.go new file mode 100644 index 0000000..d6e7888 --- /dev/null +++ b/part_8/tic_tac_toe_v7/network/protocol.go @@ -0,0 +1,10 @@ +package network + +import "encoding/json" + +type Command string + +type Message struct { + Cmd Command `json:"command"` + Payload json.RawMessage `json:"payload,omitempty"` +} diff --git a/part_8/tic_tac_toe_v7/network/server_to_client.go b/part_8/tic_tac_toe_v7/network/server_to_client.go new file mode 100644 index 0000000..e4e113e --- /dev/null +++ b/part_8/tic_tac_toe_v7/network/server_to_client.go @@ -0,0 +1,94 @@ +package network + +import ( + b "tic-tac-toe/board" + g "tic-tac-toe/game" + "tic-tac-toe/model" +) + +const ( + // Server to Client Commands + CmdUpdateState Command = "update_state" + CmdError Command = "error" + CmdNickNameResponse Command = "nick_name_response" + CmdRoomCreated Command = "room_created" + CmdRoomJoinResponse Command = "room_join_response" + CmdRoomListResponse Command = "room_list_response" + CmdInitGame Command = "init_game" + CmdOpponentLeft Command = "opponent_left" + CmdEndGame Command = "end_game" + CmdFinishedGamesResponse Command = "finished_games_response" + CmdFinishedGameResponse Command = "finished_game_response" +) + +// сообщение о том, что противник покинул игру +// инициализирующее сообщение в начале партии + +// InitGameResponse отправляется сервером при инициализации игры. +type InitGameResponse struct { + Board b.Board `json:"board"` + CurrentPlayer b.BoardField `json:"current_player"` +} + +// EndGameResponse отправляется сервером при завершении игры. +type EndGameResponse struct { + Board b.Board `json:"board"` + CurrentPlayer b.BoardField `json:"current_player"` +} + +type OpponentLeft struct { + Nickname string `json:"nickname"` +} + +// RoomInfo содержит информацию о комнате. +type RoomInfo struct { + Name string `json:"name"` + BoardSize int `json:"board_size"` + IsFull bool `json:"is_full"` + GameMode g.GameMode `json:"game_mode"` + Difficult g.Difficulty `json:"difficult"` +} + +// RoomListсодержит список доступных комнат. +type RoomListResponse struct { + Rooms []RoomInfo `json:"rooms"` +} + +// GameStateUpdate содержит информацию об обновлении состояния игры. +type GameStateUpdate struct { + Board b.Board `json:"board"` + CurrentPlayer b.BoardField `json:"current_player"` +} + +// ErrorResponse отправляется сервером при возникновении ошибки. +type ErrorResponse struct { + Message string `json:"message"` +} + +// NickNameResponse отправляется сервером при успешном входе клиента. +type NickNameResponse struct { + Nickname string `json:"nickname"` +} + +// RoomCreatedResponse отправляется сервером после успешного создания комнаты. +type RoomCreatedResponse struct { + RoomID string `json:"room_id"` + RoomName string `json:"room_name"` +} + +// RoomJoinResponse отправляется сервером, когда клиент успешно присоединился к комнате. +type RoomJoinResponse struct { + RoomName string `json:"room_name"` + PlayerSymbol b.BoardField `json:"player_symbol"` + Board b.Board `json:"board"` +} + +// FinishedGamesResponse отправляется сервером со списком завершенных игр. +type FinishedGamesResponse struct { + Games *[]model.FinishGameSnapshot `json:"games"` +} + +// FinishedGameResponse отправляется сервером с информацией о конкретной завершенной игре. +type FinishedGameResponse struct { + Game *model.FinishGameSnapshot `json:"game"` +} diff --git a/part_8/tic_tac_toe_v7/player/computer_player.go b/part_8/tic_tac_toe_v7/player/computer_player.go new file mode 100644 index 0000000..66f14bb --- /dev/null +++ b/part_8/tic_tac_toe_v7/player/computer_player.go @@ -0,0 +1,342 @@ +package player + +import ( + "fmt" + "math/rand" + "net" + b "tic-tac-toe/board" + g "tic-tac-toe/game" + "tic-tac-toe/network" + "time" +) + +// Структура для представления игрока-компьютера +type ComputerPlayer struct { + Figure b.BoardField `json:"figure"` + Difficulty g.Difficulty `json:"difficulty"` + rand *rand.Rand +} + +// Создаем нового игрока-компьютера с заданным уровнем сложности +func NewComputerPlayer( + figure b.BoardField, + difficulty g.Difficulty, +) *ComputerPlayer { + source := rand.NewSource(time.Now().UnixNano()) + return &ComputerPlayer{ + Figure: figure, + Difficulty: difficulty, + rand: rand.New(source), + } +} + +func (p *ComputerPlayer) GetSymbol() string { + if p.Figure == b.Cross { + return "X" + } + return "O" +} + +func (p *ComputerPlayer) SendMessage(msg *network.Message) { + +} + +func (p *ComputerPlayer) GetNickname() string { + return "Computer" +} + +func (p *ComputerPlayer) SwitchPlayer() { + if p.Figure == b.Cross { + p.Figure = b.Nought + } else { + p.Figure = b.Cross + } +} + +func (p *ComputerPlayer) GetFigure() b.BoardField { + return p.Figure +} + +func (p *ComputerPlayer) IsComputer() bool { + return true +} + +// Реализуем ход компьютера в зависимости от выбранной сложности +func (p *ComputerPlayer) MakeMove(board *b.Board) (int, int, bool) { + fmt.Printf("%s (Computer) making move... ", p.GetSymbol()) + + var row, col int + switch p.Difficulty { + case g.Easy: + row, col = p.makeEasyMove(board) + case g.Medium: + row, col = p.makeMediumMove(board) + case g.Hard: + row, col = p.makeHardMove(board) + } + + fmt.Printf("Move made (%d, %d)\n", row+1, col+1) + return row, col, true +} + +// Легкий уровень: случайный ход на свободную клетку +func (p *ComputerPlayer) makeEasyMove(board *b.Board) (int, int) { + emptyCells := p.getEmptyCells(board) + if len(emptyCells) == 0 { + return -1, -1 + } + + // Выбираем случайную свободную клетку + randomIndex := p.rand.Intn(len(emptyCells)) + return emptyCells[randomIndex][0], emptyCells[randomIndex][1] +} + +func (p *ComputerPlayer) CheckSocket(conn net.Conn) bool { + return false +} + +// Средний уровень: проверяет возможность выигрыша +// или блокировки выигрыша противника +func (p *ComputerPlayer) makeMediumMove(board *b.Board) (int, int) { + // Проверяем, можем ли мы выиграть за один ход + if move := p.findWinningMove(board, p.Figure); move != nil { + return move[0], move[1] + } + + // Проверяем, нужно ли блокировать победу противника + opponentFigure := b.Cross + if p.Figure == b.Cross { + opponentFigure = b.Nought + } + + if move := p.findWinningMove(board, opponentFigure); move != nil { + return move[0], move[1] + } + + // Занимаем центр, если свободен (хорошая стратегия) + center := board.Size / 2 + if board.Board[center][center] == b.Empty { + return center, center + } + + // Занимаем угол, если свободен + corners := [][]int{ + {0, 0}, + {0, board.Size - 1}, + {board.Size - 1, 0}, + {board.Size - 1, board.Size - 1}, + } + + for _, corner := range corners { + if board.Board[corner[0]][corner[1]] == b.Empty { + return corner[0], corner[1] + } + } + + // Если нет лучшего хода, делаем случайный ход + return p.makeEasyMove(board) +} + +// Сложный уровень: использует алгоритм минимакс для оптимального хода +func (p *ComputerPlayer) makeHardMove(board *b.Board) (int, int) { + // Если доска пустая, ходим в центр или угол (оптимальный первый ход) + emptyCells := p.getEmptyCells(board) + if len(emptyCells) == board.Size*board.Size { + // Первый ход - центр или угол + center := board.Size / 2 + return center, center + } + + // Используем минимакс для доски 3x3 + // Для больших досок это слишком ресурсоемко + if board.Size <= 3 { + bestScore := -1000 + bestMove := []int{-1, -1} + + // Создаем канал для результатов + type moveResult struct { + move []int + score int + } + resultChan := make(chan moveResult, len(emptyCells)) + + // Запускаем горутину для каждого возможного хода + for _, cell := range emptyCells { + go func(cell []int) { + row, col := cell[0], cell[1] + // Копируем доску чтобы избежать гонок данных + boardCopy := p.copyBoard(board) + + // Пробуем сделать ход + boardCopy.Board[row][col] = p.Figure + + // Вычисляем оценку хода через минимакс + score := p.minimax(boardCopy, 0, false) + + // Отправляем результат в канал + resultChan <- moveResult{ + move: []int{row, col}, + score: score, + } + }(cell) + } + + // Собираем результаты всех горутин + for i := 0; i < len(emptyCells); i++ { + result := <-resultChan + if result.score > bestScore { + bestScore = result.score + bestMove = result.move + } + } + + return bestMove[0], bestMove[1] + } + + // Для больших досок выбираем случайно одну из трех параллельных стратегий + strategyChoice := p.rand.Intn(3) + switch strategyChoice { + case 0: + fmt.Println("Using limited-depth parallel minimax strategy") + return p.makeLimitedDepthMinimax(board) + case 1: + fmt.Println("Using parallel heuristic evaluation strategy") + return p.makeParallelHeuristicMove(board) + case 2: + fmt.Println("Using zone-based parallel analysis strategy") + return p.makeZoneBasedMove(board) + default: + //В случае ошибки используем стратегию среднего уровня + return p.makeMediumMove(board) + } +} + +// Алгоритм минимакс для определения оптимального хода +func (p *ComputerPlayer) minimax( + board *b.Board, + depth int, isMaximizing bool, +) int { + opponentFigure := b.Cross + if p.Figure == b.Cross { + opponentFigure = b.Nought + } + + // Проверяем терминальное состояние + if board.CheckWin(p.Figure) { + return 10 - depth // Выигрыш, чем быстрее, тем лучше + } else if board.CheckWin(opponentFigure) { + return depth - 10 // Проигрыш, чем дольше, тем лучше + } else if board.CheckDraw() { + return 0 // Ничья + } + + emptyCells := p.getEmptyCells(board) + + if isMaximizing { + bestScore := -1000 + + // Проходим по всем свободным клеткам + for _, cell := range emptyCells { + row, col := cell[0], cell[1] + + // Делаем ход + board.Board[row][col] = p.Figure + + // Рекурсивно оцениваем ход + score := p.minimax(board, depth+1, false) + + // Отменяем ход + board.Board[row][col] = b.Empty + + bestScore = max(score, bestScore) + } + + return bestScore + } else { + bestScore := 1000 + + // Проходим по всем свободным клеткам + for _, cell := range emptyCells { + row, col := cell[0], cell[1] + + // Делаем ход противника + board.Board[row][col] = opponentFigure + + // Рекурсивно оцениваем ход + score := p.minimax(board, depth+1, true) + + // Отменяем ход + board.Board[row][col] = b.Empty + + bestScore = min(score, bestScore) + } + + return bestScore + } +} + +// Вспомогательная функция для поиска хода, приводящего к выигрышу +func (p *ComputerPlayer) findWinningMove( + board *b.Board, + figure b.BoardField, +) []int { + for _, cell := range p.getEmptyCells(board) { + row, col := cell[0], cell[1] + + // Пробуем сделать ход + board.Board[row][col] = figure + + // Проверяем, приведет ли этот ход к выигрышу + if board.CheckWin(figure) { + // Отменяем ход и возвращаем координаты + board.Board[row][col] = b.Empty + return []int{row, col} + } + + // Отменяем ход + board.Board[row][col] = b.Empty + } + + return nil // Нет выигрышного хода +} + +// Получение списка пустых клеток +func (p *ComputerPlayer) getEmptyCells(board *b.Board) [][]int { + var emptyCells [][]int + + for i := 0; i < board.Size; i++ { + for j := 0; j < board.Size; j++ { + if board.Board[i][j] == b.Empty { + emptyCells = append(emptyCells, []int{i, j}) + } + } + } + + return emptyCells +} + +// Вспомогательные функции max и min +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +// Копирование доски для избежания гонок данных при параллельном вычислении +func (p *ComputerPlayer) copyBoard(board *b.Board) *b.Board { + newBoard := b.NewBoard(board.Size) + for i := 0; i < board.Size; i++ { + for j := 0; j < board.Size; j++ { + newBoard.Board[i][j] = board.Board[i][j] + } + } + return newBoard +} diff --git a/part_8/tic_tac_toe_v7/player/i_player.go b/part_8/tic_tac_toe_v7/player/i_player.go new file mode 100644 index 0000000..a98111e --- /dev/null +++ b/part_8/tic_tac_toe_v7/player/i_player.go @@ -0,0 +1,32 @@ +package player + +import ( + "net" + b "tic-tac-toe/board" + "tic-tac-toe/network" +) + +// Интерфейс для любого игрока, будь то человек или компьютер +type IPlayer interface { + // Получение символа игрока (X или O) + GetSymbol() string + + // Переключение хода на другого игрока + SwitchPlayer() + + SendMessage(msg *network.Message) + + GetNickname() string + + // Получение текущей фигуры игрока + GetFigure() b.BoardField + + // Выполнение хода игрока + // Возвращает координаты хода (x, y) и признак успешности + MakeMove(board *b.Board) (int, int, bool) + + // Проверка, является ли игрок компьютером + IsComputer() bool + + CheckSocket(conn net.Conn) bool +} diff --git a/part_8/tic_tac_toe_v7/player/player.go b/part_8/tic_tac_toe_v7/player/player.go new file mode 100644 index 0000000..8b7934d --- /dev/null +++ b/part_8/tic_tac_toe_v7/player/player.go @@ -0,0 +1,95 @@ +package player + +import ( + "encoding/json" + "fmt" + "net" + "strconv" + "strings" + b "tic-tac-toe/board" + "tic-tac-toe/network" +) + +// Структура для представления игрока-человека +type HumanPlayer struct { + Figure b.BoardField `json:"figure"` + Nickname string `json:"nickname"` + Conn *net.Conn `json:"-"` +} + +func NewHumanPlayer( + nickname string, conn *net.Conn, +) *HumanPlayer { + return &HumanPlayer{Figure: b.Cross, Nickname: nickname, Conn: conn} +} + +func (p *HumanPlayer) CheckSocket(conn net.Conn) bool { + return *p.Conn == conn +} + +// Возвращаем символ игрока +func (p *HumanPlayer) GetSymbol() string { + if p.Figure == b.Cross { + return "X" + } + return "O" +} + +func (p *HumanPlayer) SendMessage(msg *network.Message) { + json.NewEncoder(*p.Conn).Encode(msg) +} + +func (p *HumanPlayer) GetNickname() string { + return p.Nickname +} + +// Изменяем фигуру текущего игрока +func (p *HumanPlayer) SwitchPlayer() { + if p.Figure == b.Cross { + p.Figure = b.Nought + } else { + p.Figure = b.Cross + } +} + +// Возвращаем текущую фигуру игрока +func (p *HumanPlayer) GetFigure() b.BoardField { + return p.Figure +} + +// Метод-заглушка, т.к. ввод игрока осуществляется на +// уровне пакета game, где нужно еще отрабатывать +// команду на выход и сохранение игровой сессии +func (p *HumanPlayer) MakeMove(board *b.Board) (int, int, bool) { + return -1, -1, false +} + +// Обрабатываем строку ввода и +// преобразуем ее в координаты хода +func (p *HumanPlayer) ParseMove( + input string, + board *b.Board, +) (int, int, bool) { + parts := strings.Fields(input) + if len(parts) != 2 { + fmt.Println("Invalid input. Please try again.") + return -1, -1, false + } + + row, err1 := strconv.Atoi(parts[0]) + col, err2 := strconv.Atoi(parts[1]) + if err1 != nil || err2 != nil || + row < 1 || col < 1 || row > board.Size || + col > board.Size { + fmt.Println("Invalid input. Please try again.") + return -1, -1, false + } + + // Преобразуем введенные координаты (начиная с 1) + // в индексы массива (начиная с 0) + return row - 1, col - 1, true +} + +func (p *HumanPlayer) IsComputer() bool { + return false +} diff --git a/part_8/tic_tac_toe_v7/player/strategy_heuristic.go b/part_8/tic_tac_toe_v7/player/strategy_heuristic.go new file mode 100644 index 0000000..52cbaa0 --- /dev/null +++ b/part_8/tic_tac_toe_v7/player/strategy_heuristic.go @@ -0,0 +1,169 @@ +package player + +import ( + "sync" + b "tic-tac-toe/board" +) + +// Метод запуска параллельной эвристической оценки ходов +func (p *ComputerPlayer) makeParallelHeuristicMove(board *b.Board) (int, int) { + bestScore := -100000 + var bestMove []int + emptyCells := p.getEmptyCells(board) + + if len(emptyCells) == 0 { + return -1, -1 + } + if len(emptyCells) == 1 { + return emptyCells[0][0], emptyCells[0][1] + } + + // Создаем канал для результатов + type moveResult struct { + move []int + score int + } + resultChan := make(chan moveResult, len(emptyCells)) + var wg sync.WaitGroup + + // Запускаем горутины для каждого возможного хода + for _, cell := range emptyCells { + wg.Add(1) + go func(r, c int) { + defer wg.Done() + boardCopy := p.copyBoard(board) + boardCopy.Board[r][c] = p.Figure + score := p.evaluateBoardHeuristic(boardCopy, p.Figure) + resultChan <- moveResult{move: []int{r, c}, score: score} + }(cell[0], cell[1]) + } + + wg.Wait() + close(resultChan) + + // Определяем лучший ход + for result := range resultChan { + if result.score > bestScore { + bestScore = result.score + bestMove = result.move + } + } + + if bestMove == nil { + // Если по какой-то причине лучший ход не найден (маловероятно) + // переходи на стратегию поведения среднего уровня сложности + return p.makeMediumMove(board) + } + + return bestMove[0], bestMove[1] +} + +// Эвристическая оценка доски +// Количество рядов, столбцов или диагоналей, где у игрока есть N фигур +// и остальные клетки пусты. Также учитываем блокировку противника. +func (p *ComputerPlayer) evaluateBoardHeuristic( + board *b.Board, player b.BoardField, +) int { + score := 0 + opponent := b.Cross + if player == b.Cross { + opponent = b.Nought + } + + // Оценка за почти выигрышные линии для игрока + // Почти выигрыш + score += p.countPotentialLines(board, player, board.Size-1) * 100 + // Две фигуры в ряд (для Size > 2) + score += p.countPotentialLines(board, player, board.Size-2) * 10 + + // Штраф за почти выигрышные линии для оппонента (блокировка) + // Блокировка почти выигрыша оппонента + score -= p.countPotentialLines(board, opponent, board.Size-1) * 90 + // Блокировка двух фигур оппонента + score -= p.countPotentialLines(board, opponent, board.Size-2) * 5 + + // Бонус за занятие центра (особенно на нечетных досках) + if board.Size%2 == 1 { + center := board.Size / 2 + if board.Board[center][center] == player { + score += 5 + } else if board.Board[center][center] == opponent { + score -= 5 + } + } + return score +} + +// Вспомогательная функция для подсчета потенциальных линий +func (p *ComputerPlayer) countPotentialLines( + board *b.Board, player b.BoardField, numPlayerSymbols int, +) int { + count := 0 + lineSize := board.Size + + // Проверка строк + for r := 0; r < lineSize; r++ { + playerSymbols := 0 + emptySymbols := 0 + for c := 0; c < lineSize; c++ { + if board.Board[r][c] == player { + playerSymbols++ + } else if board.Board[r][c] == b.Empty { + emptySymbols++ + } + } + if playerSymbols == numPlayerSymbols && + (playerSymbols+emptySymbols) == lineSize { + count++ + } + } + + // Проверка столбцов + for c := 0; c < lineSize; c++ { + playerSymbols := 0 + emptySymbols := 0 + for r := 0; r < lineSize; r++ { + if board.Board[r][c] == player { + playerSymbols++ + } else if board.Board[r][c] == b.Empty { + emptySymbols++ + } + } + if playerSymbols == numPlayerSymbols && + (playerSymbols+emptySymbols) == lineSize { + count++ + } + } + + // Проверка главной диагонали + playerSymbolsDiag1 := 0 + emptySymbolsDiag1 := 0 + for i := 0; i < lineSize; i++ { + if board.Board[i][i] == player { + playerSymbolsDiag1++ + } else if board.Board[i][i] == b.Empty { + emptySymbolsDiag1++ + } + } + if playerSymbolsDiag1 == numPlayerSymbols && + (playerSymbolsDiag1+emptySymbolsDiag1) == lineSize { + count++ + } + + // Проверка побочной диагонали + playerSymbolsDiag2 := 0 + emptySymbolsDiag2 := 0 + for i := 0; i < lineSize; i++ { + if board.Board[i][lineSize-1-i] == player { + playerSymbolsDiag2++ + } else if board.Board[i][lineSize-1-i] == b.Empty { + emptySymbolsDiag2++ + } + } + if playerSymbolsDiag2 == numPlayerSymbols && + (playerSymbolsDiag2+emptySymbolsDiag2) == lineSize { + count++ + } + + return count +} diff --git a/part_8/tic_tac_toe_v7/player/strategy_limited_minimax.go b/part_8/tic_tac_toe_v7/player/strategy_limited_minimax.go new file mode 100644 index 0000000..b7acf10 --- /dev/null +++ b/part_8/tic_tac_toe_v7/player/strategy_limited_minimax.go @@ -0,0 +1,114 @@ +package player + +import ( + "sync" + b "tic-tac-toe/board" +) + +const maxDepth = 2 // Ограничение глубины для минимакса + +// Метод запуска стратегии с ограничением глубины для минимакса +func (p *ComputerPlayer) makeLimitedDepthMinimax(board *b.Board) (int, int) { + bestScore := -100000 + var bestMove []int + emptyCells := p.getEmptyCells(board) + + if len(emptyCells) == 0 { + return -1, -1 // Нет доступных ходов + } + if len(emptyCells) == 1 { + return emptyCells[0][0], emptyCells[0][1] // Единственный возможный ход + } + + // Создаем канал для результатов + type moveResult struct { + move []int + score int + } + resultChan := make(chan moveResult, len(emptyCells)) + var wg sync.WaitGroup + + // Запускаем горутины для каждого возможного хода + for _, cell := range emptyCells { + wg.Add(1) + go func(r, c int) { + defer wg.Done() + boardCopy := p.copyBoard(board) + boardCopy.Board[r][c] = p.Figure + score := p.minimaxRecursive(boardCopy, 0, false, maxDepth) + resultChan <- moveResult{move: []int{r, c}, score: score} + }(cell[0], cell[1]) + } + + wg.Wait() // Ждем завершения всех горутин + close(resultChan) // Закрываем канал + + // Определяем лучший ход + for result := range resultChan { + if result.score > bestScore { + bestScore = result.score + bestMove = result.move + } + } + + if bestMove == nil { + // Если по какой-то причине лучший ход не найден (маловероятно) + // переходи на стратегию поведения среднего уровня сложности + return p.makeMediumMove(board) + } + + return bestMove[0], bestMove[1] +} + +// Рекурсивная часть минимакса с ограничением глубины +func (p *ComputerPlayer) minimaxRecursive( + board *b.Board, depth int, isMaximizing bool, + maxDepthLimit int, +) int { + opponentFigure := b.Cross + if p.Figure == b.Cross { + opponentFigure = b.Nought + } + + if board.CheckWin(p.Figure) { + return 10 - depth // Выигрыш текущего игрока + } + if board.CheckWin(opponentFigure) { + return depth - 10 // Проигрыш текущего игрока (выигрыш оппонента) + } + if board.CheckDraw() { + return 0 // Ничья + } + + if depth >= maxDepthLimit { // Ограничение глубины + // Если достигнута максимальная глубина, используем эвристическую оценку + return p.evaluateBoardHeuristic(board, p.Figure) + } + + emptyCells := p.getEmptyCells(board) + + if isMaximizing { + bestScore := -100000 + for _, cell := range emptyCells { + boardCopy := p.copyBoard(board) + boardCopy.Board[cell[0]][cell[1]] = p.Figure + score := p.minimaxRecursive( + boardCopy, depth+1, false, maxDepthLimit, + ) + bestScore = max(bestScore, score) + } + return bestScore + } else { + bestScore := 100000 + // opponentFigure уже определен выше + for _, cell := range emptyCells { + boardCopy := p.copyBoard(board) + boardCopy.Board[cell[0]][cell[1]] = opponentFigure + score := p.minimaxRecursive( + boardCopy, depth+1, true, maxDepthLimit, + ) + bestScore = min(bestScore, score) + } + return bestScore + } +} diff --git a/part_8/tic_tac_toe_v7/player/strategy_zonebased.go b/part_8/tic_tac_toe_v7/player/strategy_zonebased.go new file mode 100644 index 0000000..e65b8a0 --- /dev/null +++ b/part_8/tic_tac_toe_v7/player/strategy_zonebased.go @@ -0,0 +1,121 @@ +package player + +import ( + b "tic-tac-toe/board" +) + +// Параллельный анализ на основе зон +func (p *ComputerPlayer) makeZoneBasedMove(board *b.Board) (int, int) { + // Если доска не очень большая, используем эвристику + if board.Size <= 5 { // Пороговое значение, можно настроить + return p.makeParallelHeuristicMove(board) + } + + bestScore := -100000 + var bestMove []int + emptyCells := p.getEmptyCells(board) + if len(emptyCells) == 0 { + return -1, -1 + } + if len(emptyCells) == 1 { + return emptyCells[0][0], emptyCells[0][1] + } + + // Определяем размер зоны (например, 3x3) + zoneSize := 3 + if board.Size < zoneSize { + zoneSize = board.Size // Если доска меньше зоны, зона равна доске + } + + type moveResult struct { + move []int + score int + } + // Используем буферизированный канал, чтобы не блокировать горутины, + // если основная горутина не успевает обрабатывать результаты + + // Размер канала равен количеству пустых клеток, + // т.к. для каждой может быть запущена горутина + resultChan := make(chan moveResult, len(emptyCells)) + numZonesToProcess := 0 // Счетчик для корректного ожидания + + for _, cell := range emptyCells { + numZonesToProcess++ + // Запускаем горутину для каждой пустой клетки + go func(centerCell []int) { + localBestScore := -100000 + var localBestMove []int + + // Определяем границы зоны вокруг centerCell + minRow := max(0, centerCell[0]-zoneSize/2) + maxRow := min(board.Size-1, centerCell[0]+zoneSize/2) + minCol := max(0, centerCell[1]-zoneSize/2) + maxCol := min(board.Size-1, centerCell[1]+zoneSize/2) + + // Ищем ходы в зоне + foundMoveInZone := false + for r := minRow; r <= maxRow; r++ { + for c := minCol; c <= maxCol; c++ { + // Если найден пустая клетка в зоне + if board.Board[r][c] == b.Empty { + foundMoveInZone = true + boardCopy := p.copyBoard(board) + boardCopy.Board[r][c] = p.Figure + // Оцениваем ход испо + score := p.evaluateBoardHeuristic(boardCopy, p.Figure) + + // Если найден лучший ход + if score > localBestScore { + localBestScore = score + localBestMove = []int{r, c} + } + } + } + } + + // Если найден лучший ход в зоне + if foundMoveInZone && localBestMove != nil { + resultChan <- moveResult{ + move: localBestMove, score: localBestScore, + } + } else if !foundMoveInZone && + board.Board[centerCell[0]][centerCell[1]] == b.Empty { + // Если зона вокруг centerCell не содержит других + // пустых клеток, но сама centerCell пуста – + // оцениваем ход в centerCell + boardCopy := p.copyBoard(board) + boardCopy.Board[centerCell[0]][centerCell[1]] = p.Figure + score := p.evaluateBoardHeuristic(boardCopy, p.Figure) + resultChan <- moveResult{move: centerCell, score: score} + } else { + // Если не найдено ходов в зоне или centerCell не пуста + // (не должно случиться, если итерируем по emptyCells), + // отправляем фиктивный результат, + // чтобы не блокировать ожидание. + // Этого не должно происходить в нормальном потоке. + resultChan <- moveResult{move: nil, score: -200000} + } + }(cell) + } + + // Ожидаем завершения всех горутин + processedGoroutines := 0 // Счетчик для корректного ожидания + for processedGoroutines < numZonesToProcess { + result := <-resultChan + processedGoroutines++ + // Если найден лучший ход + if result.move != nil && result.score > bestScore { + bestScore = result.score + bestMove = result.move + } + } + + if bestMove == nil { + // Если по какой-то причине лучший ход не найден (маловероятно) + // переходи на стратегию поведения среднего уровня сложности + return p.makeMediumMove(board) + } + + // Возвращаем лучший ход + return bestMove[0], bestMove[1] +} diff --git a/part_8/tic_tac_toe_v7/room/room.go b/part_8/tic_tac_toe_v7/room/room.go new file mode 100644 index 0000000..658dc07 --- /dev/null +++ b/part_8/tic_tac_toe_v7/room/room.go @@ -0,0 +1,255 @@ +package room + +import ( + "encoding/json" + "log" + "math/rand" + b "tic-tac-toe/board" + db "tic-tac-toe/database" + g "tic-tac-toe/game" + "tic-tac-toe/model" + n "tic-tac-toe/network" + p "tic-tac-toe/player" + "time" +) + +// Room manages the state of a single game room. +type Room struct { + Name string + Board *b.Board + Player1 p.IPlayer + Player2 p.IPlayer + CurrentPlayer p.IPlayer + State g.GameState + repository db.IRepository + Mode g.GameMode + // Уровень сложности компьютера (только для PvC) + Difficulty g.Difficulty +} + +// NewRoom creates a new game room. +func NewRoom( + name string, repository db.IRepository, boardSize int, + gameMode g.GameMode, difficulty g.Difficulty, +) *Room { + room := &Room{ + Name: name, + repository: repository, + Mode: gameMode, + Difficulty: difficulty, + Board: b.NewBoard(boardSize), + State: g.WaitingOpponent, + } + if gameMode == g.PvC { + room.Player2 = p.NewComputerPlayer(b.Nought, difficulty) + } + return room +} + +func (r *Room) IsFull() bool { + return r.Player1 != nil && r.Player2 != nil +} + +func (r *Room) PlayersAmount() int { + if r.Player1 != nil && r.Player2 != nil { + return 2 + } + return 1 +} + +func (r *Room) BoardSize() int { + return r.Board.Size +} + +func (r *Room) AddPlayer(player p.IPlayer) { + if r.Player1 == nil { + r.Player1 = player + if r.Player1.GetSymbol() != "X" { + r.Player1.SwitchPlayer() + } + } else if r.Player2 == nil { + r.Player2 = player + if r.Player2.GetSymbol() != "O" { + r.Player2.SwitchPlayer() + } + } +} + +func (r *Room) RemovePlayer(player p.IPlayer) { + if r.Player1 == player { + r.Player1 = nil + if r.Player2 != nil && !r.Player2.IsComputer() { + opponentLeft := &n.OpponentLeft{Nickname: player.GetNickname()} + payloadBytes, err := json.Marshal(opponentLeft) + if err != nil { + log.Printf("Error marshaling OpponentLeft: %v", err) + return + } + msg := &n.Message{ + Cmd: n.CmdOpponentLeft, + Payload: payloadBytes, + } + r.Player2.SendMessage(msg) + } + } else if r.Player2 == player { + r.Player2 = nil + if r.Player1 != nil && !r.Player1.IsComputer() { + opponentLeft := &n.OpponentLeft{Nickname: player.GetNickname()} + payloadBytes, err := json.Marshal(opponentLeft) + if err != nil { + log.Printf("Error marshaling OpponentLeft: %v", err) + return + } + msg := &n.Message{ + Cmd: n.CmdOpponentLeft, + Payload: payloadBytes, + } + r.Player1.SendMessage(msg) + } + } +} + +func (r *Room) InitGame() { + if !r.IsFull() { + return + } + + randomPlayer := []b.BoardField{b.Cross, b.Nought} + if !r.Board.IsEmpty() { + r.Board = b.NewBoard(r.Board.Size) + } + + msg := &n.Message{Cmd: n.CmdInitGame} + initGamePayload := &n.InitGameResponse{ + Board: *r.Board, + } + // Select a random starting symbol + starterSymbol := randomPlayer[rand.Intn(len(randomPlayer))] + switch starterSymbol { + case b.Cross: + r.State = g.CrossStep + initGamePayload.CurrentPlayer = b.Cross + case b.Nought: + r.State = g.NoughtStep + initGamePayload.CurrentPlayer = b.Nought + } + + // Set the current player based on game mode and starter symbol + if r.Mode == g.PvC { + // In PvC mode, Player1 is always the human player + if r.State == g.CrossStep { + r.CurrentPlayer = r.Player1 + } else if r.State == g.NoughtStep { + r.CurrentPlayer = r.Player2 + } + } else { + // In PvP mode, set the current player based on who has the starter symbol + if (r.State == g.CrossStep && r.Player1.GetFigure() == b.Cross) || + (r.State == g.NoughtStep && r.Player1.GetFigure() == b.Nought) { + r.CurrentPlayer = r.Player1 + } else { + r.CurrentPlayer = r.Player2 + } + } + + payloadBytes, err := json.Marshal(initGamePayload) + if err != nil { + log.Printf("Error marshaling InitGameResponse for Player1 after Player2 left: %v", err) + return + } + msg.Payload = payloadBytes + r.Player1.SendMessage(msg) + r.Player2.SendMessage(msg) + + if r.CurrentPlayer.IsComputer() { + row, col, _ := r.CurrentPlayer.MakeMove(r.Board) + r.PlayerStep(r.CurrentPlayer, row, col) + } +} + +// Переключаем активного игрока +func (r *Room) switchCurrentPlayer() { + if r.CurrentPlayer == r.Player1 { + r.CurrentPlayer = r.Player2 + } else { + r.CurrentPlayer = r.Player1 + } +} + +func (r *Room) PlayerStep(player p.IPlayer, row, col int) { + msg := &n.Message{} + if r.State != g.CrossStep && r.State != g.NoughtStep { + return + } + // проверяем, что ход делает текущий игрок + if player != r.CurrentPlayer { + return + } + + r.Board.SetSymbol(row, col, r.CurrentPlayer.GetFigure()) + if r.Board.CheckWin(r.CurrentPlayer.GetFigure()) { + if r.CurrentPlayer.GetFigure() == b.Cross { + r.State = g.CrossWin + } else { + r.State = g.NoughtWin + } + msg.Cmd = n.CmdEndGame + endGamePayload := &n.EndGameResponse{ + Board: *r.Board, + CurrentPlayer: r.CurrentPlayer.GetFigure(), + } + msg.Payload, _ = json.Marshal(endGamePayload) + + figureWinner := r.CurrentPlayer.GetFigure() + winnerNickName := r.CurrentPlayer.GetNickname() + + var anotherPlayerNickName string + if r.CurrentPlayer == r.Player1 { + anotherPlayerNickName = r.Player2.GetNickname() + } else { + anotherPlayerNickName = r.Player1.GetNickname() + } + r.repository.SaveFinishedGame(&model.FinishGameSnapshot{ + Board: r.Board, + PlayerFigure: figureWinner, + WinnerName: winnerNickName, + AnotherPlayerName: anotherPlayerNickName, + Time: time.Now(), + }) + } else if r.Board.CheckDraw() { + r.State = g.Draw + msg.Cmd = n.CmdEndGame + endGamePayload := &n.EndGameResponse{ + Board: *r.Board, + CurrentPlayer: b.Empty, + } + msg.Payload, _ = json.Marshal(endGamePayload) + } else { + if r.CurrentPlayer.GetFigure() == b.Cross { + r.State = g.NoughtStep + } else { + r.State = g.CrossStep + } + r.switchCurrentPlayer() + msg.Cmd = n.CmdUpdateState + stateUpdatePayload := &n.GameStateUpdate{ + Board: *r.Board, + CurrentPlayer: r.CurrentPlayer.GetFigure(), + } + msg.Payload, _ = json.Marshal(stateUpdatePayload) + } + + r.Player1.SendMessage(msg) + r.Player2.SendMessage(msg) + + if r.State == g.CrossWin || r.State == g.NoughtWin || r.State == g.Draw { + time.Sleep(10 * time.Second) + r.InitGame() + return + } + + if r.CurrentPlayer.IsComputer() { + row, col, _ := r.CurrentPlayer.MakeMove(r.Board) + r.PlayerStep(r.CurrentPlayer, row, col) + } +} diff --git a/part_8/tic_tac_toe_v7/server/client_message_handlers.go b/part_8/tic_tac_toe_v7/server/client_message_handlers.go new file mode 100644 index 0000000..ac6e75b --- /dev/null +++ b/part_8/tic_tac_toe_v7/server/client_message_handlers.go @@ -0,0 +1,201 @@ +package server + +import ( + "encoding/json" + "log" + "net" + "strconv" + "tic-tac-toe/network" + p "tic-tac-toe/player" +) + +var defaultPlayerCounts int = 0 + +func (s *Server) handleCommand(client net.Conn, msg *network.Message) { + log.Printf( + "Received command '%s' from %s", + msg.Cmd, client.RemoteAddr(), + ) + + switch msg.Cmd { + case network.CmdNickname: + s.nickNameHandler(client, msg) + case network.CmdMakeMoveRequest: + s.makeMoveHandler(client, msg) + case network.CmdListRoomsRequest: + s.listRoomsHandler(client, msg) + case network.CmdJoinRoomRequest: + s.joinRoomHandler(client, msg) + case network.CmdLeaveRoomRequest: + s.leaveRoomHandler(client, msg) + case network.CmdFinishedGamesRequest: + s.getFinishedGamesHandler(client, msg) + case network.CmdFinishedGameByIdRequest: + s.getFinishedGameByIdHandler(client, msg) + default: + log.Printf("Unknown command: %s", msg.Cmd) + } +} + +func (s *Server) nickNameHandler(client net.Conn, msg *network.Message) { + nicknameRequest := &network.NicknameRequest{} + if err := json.Unmarshal(msg.Payload, nicknameRequest); err != nil { + log.Printf("Error unmarshaling NicknameRequest: %v", err) + return + } + if s.players[nicknameRequest.Nickname] != nil { + nicknameRequest.Nickname = nicknameRequest.Nickname + + "_" + strconv.Itoa(defaultPlayerCounts) + defaultPlayerCounts++ + } + s.players[nicknameRequest.Nickname] = p.NewHumanPlayer( + nicknameRequest.Nickname, &client, + ) + response := &network.NickNameResponse{ + Nickname: nicknameRequest.Nickname, + } + msg.Payload, _ = json.Marshal(response) + msg.Cmd = network.CmdNickNameResponse + json.NewEncoder(client).Encode(msg) +} + +func (s *Server) joinRoomHandler(client net.Conn, msg *network.Message) { + joinRoomRequest := &network.JoinRoomRequest{} + if err := json.Unmarshal(msg.Payload, joinRoomRequest); err != nil { + log.Printf("Error unmarshaling JoinRoomRequest: %v", err) + return + } + room, okRoom := s.rooms[joinRoomRequest.RoomName] + player, okPlayer := s.players[joinRoomRequest.PlayerName] + if !okRoom || !okPlayer { + response := &network.ErrorResponse{Message: "Room not found"} + msg.Cmd = network.CmdError + msg.Payload, _ = json.Marshal(response) + json.NewEncoder(client).Encode(msg) + return + } + s.mutex.Lock() + if room.IsFull() { + s.mutex.Unlock() + response := &network.ErrorResponse{Message: "Room is full"} + msg.Cmd = network.CmdError + msg.Payload, _ = json.Marshal(response) + json.NewEncoder(client).Encode(msg) + return + } + room.AddPlayer(player) + s.mutex.Unlock() + response := &network.RoomJoinResponse{ + RoomName: joinRoomRequest.RoomName, + PlayerSymbol: player.GetFigure(), + Board: *room.Board, + } + msg.Payload, _ = json.Marshal(response) + msg.Cmd = network.CmdRoomJoinResponse + json.NewEncoder(client).Encode(msg) + room.InitGame() +} + +func (s *Server) leaveRoomHandler(client net.Conn, msg *network.Message) { + leaveRoomRequest := &network.LeaveRoomRequest{} + if err := json.Unmarshal(msg.Payload, leaveRoomRequest); err != nil { + log.Printf("Error unmarshaling LeaveRoomRequest: %v", err) + return + } + room, okRoom := s.rooms[leaveRoomRequest.RoomName] + player, okPlayer := s.players[leaveRoomRequest.PlayerName] + if !okRoom || !okPlayer { + response := &network.ErrorResponse{Message: "Room not found"} + msg.Cmd = network.CmdError + msg.Payload, _ = json.Marshal(response) + json.NewEncoder(client).Encode(msg) + return + } + s.mutex.Lock() + room.RemovePlayer(player) + s.mutex.Unlock() +} + +func (s *Server) listRoomsHandler(client net.Conn, msg *network.Message) { + s.mutex.Lock() + defer s.mutex.Unlock() + var roomInfos []network.RoomInfo + for _, room := range s.rooms { + roomInfos = append(roomInfos, network.RoomInfo{ + Name: room.Name, + BoardSize: room.BoardSize(), + IsFull: room.IsFull(), + GameMode: room.Mode, + Difficult: room.Difficulty, + }) + } + + response := &network.RoomListResponse{ + Rooms: roomInfos, + } + msg.Cmd = network.CmdRoomListResponse + msg.Payload, _ = json.Marshal(response) + json.NewEncoder(client).Encode(msg) +} + +func (s *Server) getFinishedGamesHandler(client net.Conn, msg *network.Message) { + // получаем данные из БД + finishedGames, err := s.repository.GetAllFinishedGames() + if err != nil { + response := &network.ErrorResponse{Message: "Error getting finished games"} + msg.Cmd = network.CmdError + msg.Payload, _ = json.Marshal(response) + json.NewEncoder(client).Encode(msg) + return + } + response := &network.FinishedGamesResponse{ + Games: finishedGames, + } + msg.Cmd = network.CmdFinishedGamesResponse + msg.Payload, _ = json.Marshal(response) + json.NewEncoder(client).Encode(msg) +} + +func (s *Server) getFinishedGameByIdHandler(client net.Conn, msg *network.Message) { + getFinishedGameByIdRequest := &network.GetFinishedGameByIdRequest{} + if err := json.Unmarshal(msg.Payload, getFinishedGameByIdRequest); err != nil { + log.Printf("Error unmarshaling GetFinishedGameByIdRequest: %v", err) + return + } + finishedGame, err := s.repository.GetFinishedGameById(getFinishedGameByIdRequest.GameID) + if err != nil { + response := &network.ErrorResponse{Message: "Error getting finished game by id"} + msg.Cmd = network.CmdError + msg.Payload, _ = json.Marshal(response) + json.NewEncoder(client).Encode(msg) + return + } + response := &network.FinishedGameResponse{ + Game: finishedGame, + } + msg.Cmd = network.CmdFinishedGameResponse + msg.Payload, _ = json.Marshal(response) + json.NewEncoder(client).Encode(msg) +} + +func (s *Server) makeMoveHandler(client net.Conn, msg *network.Message) { + makeMoveRequest := &network.MakeMoveRequest{} + if err := json.Unmarshal(msg.Payload, makeMoveRequest); err != nil { + log.Printf("Error unmarshaling MakeMoveRequest: %v", err) + return + } + room, okRoom := s.rooms[makeMoveRequest.RoomName] + player, okPlayer := s.players[makeMoveRequest.PlayerName] + if !okRoom || !okPlayer { + response := &network.ErrorResponse{Message: "Room not found"} + msg.Cmd = network.CmdError + msg.Payload, _ = json.Marshal(response) + json.NewEncoder(client).Encode(msg) + return + } + room.PlayerStep( + player, + makeMoveRequest.PositionRow, + makeMoveRequest.PositionCol, + ) +} diff --git a/part_8/tic_tac_toe_v7/server/server.go b/part_8/tic_tac_toe_v7/server/server.go new file mode 100644 index 0000000..07f50b3 --- /dev/null +++ b/part_8/tic_tac_toe_v7/server/server.go @@ -0,0 +1,116 @@ +package server + +import ( + "encoding/json" + "log" + "net" + "sync" + + db "tic-tac-toe/database" + g "tic-tac-toe/game" + "tic-tac-toe/network" + "tic-tac-toe/player" + "tic-tac-toe/room" +) + +// Server manages client connections and game rooms. +type Server struct { + listener net.Listener + repository db.IRepository + rooms map[string]*room.Room + players map[string]player.IPlayer + mutex sync.RWMutex +} + +// NewServer creates and returns a new server instance. +func NewServer(addr string, repository db.IRepository) (*Server, error) { + listener, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + server := &Server{ + listener: listener, + repository: repository, + rooms: make(map[string]*room.Room), + players: make(map[string]player.IPlayer), + } + + server.rooms["room1"] = room.NewRoom( + "room1", server.repository, 3, g.PvP, g.None, + ) + server.rooms["room2"] = room.NewRoom( + "room2", server.repository, 3, g.PvC, g.Easy, + ) + server.rooms["room3"] = room.NewRoom( + "room3", server.repository, 5, g.PvC, g.Medium, + ) + server.rooms["room4"] = room.NewRoom( + "room4", server.repository, 6, g.PvC, g.Hard, + ) + + return server, nil +} + +// Start begins listening for and handling client connections. +func (s *Server) Start() { + log.Printf("Server started, listening on %s", s.listener.Addr()) + defer s.listener.Close() + + for { + conn, err := s.listener.Accept() + if err != nil { + log.Printf("Error accepting connection: %v", err) + continue + } + + go s.handleConnection(conn) + } +} + +// handleConnection manages a single client connection. +func (s *Server) handleConnection(conn net.Conn) { + log.Printf("New client connected: %s", conn.RemoteAddr()) + defer conn.Close() + + decoder := json.NewDecoder(conn) + for { + var msg network.Message + if err := decoder.Decode(&msg); err != nil { + log.Printf("Client %s disconnected: %v", conn.RemoteAddr(), err) + s.disconnectedClientHandler(conn) + return + } + + s.handleCommand(conn, &msg) + } +} + +func (s *Server) disconnectedClientHandler(conn net.Conn) { + var player player.IPlayer + for _, room := range s.rooms { + if room.Player1 != nil { + if room.Player1.CheckSocket(conn) { + player = room.Player1 + room.RemovePlayer(room.Player1) + break + } + } + if room.Player2 != nil { + if room.Player2.CheckSocket(conn) { + player = room.Player2 + room.RemovePlayer(room.Player2) + break + } + } + } + if player == nil { + log.Printf( + "Client %s disconnected: player not found", + conn.RemoteAddr(), + ) + return + } + s.mutex.Lock() + delete(s.players, player.GetNickname()) + s.mutex.Unlock() +} diff --git a/part_8/tic_tac_toe_v7/tic_tac_toe.db b/part_8/tic_tac_toe_v7/tic_tac_toe.db new file mode 100644 index 0000000..f8c4e50 Binary files /dev/null and b/part_8/tic_tac_toe_v7/tic_tac_toe.db differ diff --git a/part_8/tic_tac_toe_v8/.vscode/launch.json b/part_8/tic_tac_toe_v8/.vscode/launch.json new file mode 100644 index 0000000..edd87ae --- /dev/null +++ b/part_8/tic_tac_toe_v8/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + "version": "0.2.0", + "configurations": [ + + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${fileDirname}", // <- ставим запятую + "console": "integratedTerminal" + } + ] +} diff --git a/part_8/tic_tac_toe_v8/board/board.go b/part_8/tic_tac_toe_v8/board/board.go new file mode 100644 index 0000000..a5bc053 --- /dev/null +++ b/part_8/tic_tac_toe_v8/board/board.go @@ -0,0 +1,203 @@ +package board + +import ( + "fmt" + "sync" +) + +const ( + BoardDefaultSize int = 3 + BoardMinSize int = 3 + BoardMaxSize int = 9 +) + +type Board struct { + Board [][]BoardField `json:"board"` + Size int `json:"size"` +} + +func NewBoard(size int) *Board { + board := make([][]BoardField, size) + for i := range board { + board[i] = make([]BoardField, size) + } + return &Board{Board: board, Size: size} +} + +func (b *Board) IsEmpty() bool { + for i := range b.Size { + for j := range b.Size { + if b.Board[i][j] != Empty { + return false + } + } + } + return true +} + +// Отображение игрового поля +func (b *Board) PrintBoard() { + fmt.Print(" ") + for i := range b.Size { + fmt.Printf("%d ", i+1) + } + fmt.Println() + for i := range b.Size { + fmt.Printf("%d ", i+1) + for j := range b.Size { + switch b.Board[i][j] { + case Empty: + fmt.Print(". ") + case Cross: + fmt.Print("X ") + case Nought: + fmt.Print("O ") + } + } + fmt.Println() + } +} + +// Проверка возможности и выполнения хода +func (b *Board) makeMove(x, y int) bool { + return b.Board[x][y] == Empty +} + +func (b *Board) SetSymbol(x, y int, player BoardField) bool { + if b.makeMove(x, y) { + b.Board[x][y] = player + return true + } + return false +} + +// Проверка выигрыша +func (b *Board) CheckWin(player BoardField) bool { + if b.Size <= 4 { + // Для маленьких досок используем обычную проверку + return b.checkWinSequential(player) + } + + // Для больших досок используем параллельную проверку + + // 3 направления проверок: строки/столбцы, 2 диагонали + resultChan := make(chan bool, 3) + var wg sync.WaitGroup + wg.Add(3) + + // Параллельная проверка строк и столбцов + go func() { + defer wg.Done() + for i := range b.Size { + rowWin, colWin := true, true + for j := 0; j < b.Size; j++ { + if b.Board[i][j] != player { + rowWin = false + } + if b.Board[j][i] != player { + colWin = false + } + } + if rowWin || colWin { + resultChan <- true + return // Нашли выигрыш, выходим из горутины + } + } + resultChan <- false + }() + + // Параллельная проверка главной диагонали + go func() { + defer wg.Done() + mainDiag := true + for i := range b.Size { + if b.Board[i][i] != player { + mainDiag = false + break + } + } + resultChan <- mainDiag + }() + + // Параллельная проверка побочной диагонали + go func() { + defer wg.Done() + antiDiag := true + for i := range b.Size { + if b.Board[i][b.Size-i-1] != player { + antiDiag = false + break + } + } + resultChan <- antiDiag + }() + + // Запускаем горутину, которая закроет канал после завершения всех проверок + go func() { + wg.Wait() + close(resultChan) + }() + + // Получаем результаты проверок с помощью for range. + // Этот цикл будет ждать, пока канал не будет закрыт. + for result := range resultChan { + if result { + return true // Найден выигрыш. + } + } + + return false +} + +// Оригинальный алгоритм проверки выигрыша для малых досок +func (b *Board) checkWinSequential(player BoardField) bool { + // Проверка строк и столбцов + for i := range b.Size { + rowWin, colWin := true, true + for j := range b.Size { + if b.Board[i][j] != player { + rowWin = false + } + if b.Board[j][i] != player { + colWin = false + } + } + if rowWin || colWin { + return true + } + } + + // Главная диагональ + mainDiag := true + for i := range b.Size { + if b.Board[i][i] != player { + mainDiag = false + break + } + } + if mainDiag { + return true + } + + // Побочная диагональ + antiDiag := true + for i := range b.Size { + if b.Board[i][b.Size-i-1] != player { + antiDiag = false + break + } + } + return antiDiag +} + +// Проверка на ничью +func (b *Board) CheckDraw() bool { + for i := range b.Size { + for j := range b.Size { + if b.Board[i][j] == Empty { + return false + } + } + } + return true +} diff --git a/part_8/tic_tac_toe_v8/board/board_cell_type.go b/part_8/tic_tac_toe_v8/board/board_cell_type.go new file mode 100644 index 0000000..fcc17ec --- /dev/null +++ b/part_8/tic_tac_toe_v8/board/board_cell_type.go @@ -0,0 +1,23 @@ +package board + +type BoardField int + +// фигуры в клетке поля +const ( + Empty BoardField = iota + Cross + Nought +) + +func (bf BoardField) String() string { + switch bf { + case Empty: + return "." + case Cross: + return "X" + case Nought: + return "O" + default: + return "?" + } +} diff --git a/part_8/tic_tac_toe_v8/client/client.go b/part_8/tic_tac_toe_v8/client/client.go new file mode 100644 index 0000000..0746e7b --- /dev/null +++ b/part_8/tic_tac_toe_v8/client/client.go @@ -0,0 +1,114 @@ +package client + +import ( + "encoding/json" + "fmt" + "log" + "net" + "sync" + "time" + + b "tic-tac-toe/board" + "tic-tac-toe/network" +) + +// Client represents the client-side application. +type Client struct { + conn net.Conn + board *b.Board + mySymbol b.BoardField + currentPlayer b.BoardField + playerName string + roomName string + state State + mutex sync.RWMutex + lastMsgTime time.Time +} + +// NewClient creates a new client and connects to the server. +func NewClient(addr string) (*Client, error) { + conn, err := net.Dial("tcp", addr) + if err != nil { + return nil, err + } + + return &Client{ + conn: conn, + state: waitNickNameConfirm, + mySymbol: b.Empty, // Will be set upon joining a room + }, nil +} + +func (c *Client) setNickname(nickname string) { + c.playerName = nickname +} + +func (c *Client) getState() State { + c.mutex.RLock() + defer c.mutex.RUnlock() + return c.state +} + +func (c *Client) setState(state State) { + c.mutex.Lock() + defer c.mutex.Unlock() + + // Display a message only when transitioning to opponentMove + if state == opponentMove && c.state != opponentMove { + fmt.Println("\nWaiting for opponent's move...") + } else if state == waitingOpponentInRoom && c.state != waitingOpponentInRoom { + fmt.Println("\nWaiting for opponent to join...") + } + + c.state = state +} + +// Start begins the client's main loop for sending and receiving messages. +func (c *Client) Start() { + defer c.conn.Close() + + fmt.Println("Connected to server. ") + go c.readFromServer() + c.menu() +} + +// readFromServer continuously reads messages from the server and handles them. +func (c *Client) readFromServer() { + decoder := json.NewDecoder(c.conn) + for { + var msg network.Message + if err := decoder.Decode(&msg); err != nil { + log.Printf("Disconnected from server: %v", err) + return // Exit goroutine if connection is lost + } + + switch msg.Cmd { + case network.CmdRoomJoinResponse: + c.handleRoomJoinResponse(msg.Payload) + case network.CmdInitGame: + c.handleInitGame(msg.Payload) + case network.CmdUpdateState: + c.handleUpdateState(msg.Payload) + case network.CmdEndGame: + c.handleEndGame(msg.Payload) + case network.CmdError: + c.handleError(msg.Payload) + case network.CmdRoomListResponse: + c.handleRoomListResponse(msg.Payload) + case network.CmdNickNameResponse: + c.handleNickNameResponse(msg.Payload) + case network.CmdOpponentLeft: + c.handleOpponentLeft(msg.Payload) + case network.CmdFinishedGamesResponse: + c.handleFinishedGamesResponse(msg.Payload) + case network.CmdFinishedGameResponse: + c.handleFinishedGameResponse(msg.Payload) + default: + log.Printf( + "Received unhandled message type '%s' "+ + "from server. Payload: %s\n> ", + msg.Cmd, string(msg.Payload), + ) + } + } +} diff --git a/part_8/tic_tac_toe_v8/client/client_state.go b/part_8/tic_tac_toe_v8/client/client_state.go new file mode 100644 index 0000000..d13fff8 --- /dev/null +++ b/part_8/tic_tac_toe_v8/client/client_state.go @@ -0,0 +1,15 @@ +package client + +type State int + +const ( + waitNickNameConfirm State = iota + mainMenu + waitRoomJoin + playing + playerMove + opponentMove + endGame + waitingOpponentInRoom + waitResponseFromServer +) diff --git a/part_8/tic_tac_toe_v8/client/menu.go b/part_8/tic_tac_toe_v8/client/menu.go new file mode 100644 index 0000000..200bae0 --- /dev/null +++ b/part_8/tic_tac_toe_v8/client/menu.go @@ -0,0 +1,158 @@ +package client + +import ( + "bufio" + "encoding/json" + "fmt" + "log" + "os" + "strconv" + "strings" + "tic-tac-toe/network" + "time" +) + +func (c *Client) menu() { + reader := bufio.NewReader(os.Stdin) + encoder := json.NewEncoder(c.conn) + + fmt.Print("Enter your nickname: ") + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(input) + c.playerName = input + var msg network.Message + + msg.Cmd = network.CmdNickname + payloadData := network.NicknameRequest{Nickname: c.playerName} + jsonPayload, err := json.Marshal(payloadData) + if err != nil { + log.Printf("Error marshalling payload for command %s: %v", msg.Cmd, err) + return + } + msg.Payload = jsonPayload + if err := encoder.Encode(msg); err != nil { + log.Printf("Failed to send message to server: %v. Disconnecting.", err) + return // Exit if we can't send to server + } + + for { + switch c.getState() { + case waitNickNameConfirm: + time.Sleep(100 * time.Millisecond) + continue + case mainMenu: + c.mainMenu(reader, encoder) + case playerMove: + c.playing(reader, encoder) + case opponentMove: + // Just wait silently for opponent's move + time.Sleep(1000 * time.Millisecond) + continue + case endGame: + fmt.Println("\nGame has ended. Restarting in 10 seconds...") + time.Sleep(10 * time.Second) + continue + case waitResponseFromServer: + time.Sleep(100 * time.Millisecond) + continue + case waitingOpponentInRoom: + // Rate-limit messages to once every 3 seconds + now := time.Now() + if now.Sub(c.lastMsgTime) > 3*time.Second { + c.lastMsgTime = now + fmt.Println("\nWaiting for opponent to join...") + fmt.Println("Press 'q' and Enter to return to main menu") + fmt.Print("> ") + } + + // Poll for input every cycle but don't block + var buffer [1]byte + n, _ := os.Stdin.Read(buffer[:]) + if n > 0 && (buffer[0] == 'q' || buffer[0] == 'Q') { + fmt.Println("Leaving room...") + var msg network.Message + msg.Cmd = network.CmdLeaveRoomRequest + payload := network.LeaveRoomRequest{ + RoomName: c.roomName, + PlayerName: c.playerName, + } + jsonPayload, _ := json.Marshal(payload) + msg.Payload = jsonPayload + encoder.Encode(msg) + c.setState(mainMenu) + continue + } + + // Short sleep to avoid CPU spinning + time.Sleep(100 * time.Millisecond) + continue + } + } +} + +func (c *Client) mainMenu(reader *bufio.Reader, encoder *json.Encoder) { + var msg network.Message + + fmt.Println("Enter command:") + fmt.Println("1 - Get room list") + fmt.Println("2 - Join room") + fmt.Println("3 - Get finished games") + fmt.Println("4 - Get finished game by id") + fmt.Println("5 - Exit") + fmt.Print("> ") + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(input) + + command, err := strconv.Atoi(input) + if err != nil { + fmt.Println("Invalid command.") + return + } + + switch command { + case 1: + msg.Cmd = network.CmdListRoomsRequest + encoder.Encode(msg) + c.setState(waitResponseFromServer) + case 2: + fmt.Print("Enter room name: ") + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(input) + c.roomName = input + msg.Cmd = network.CmdJoinRoomRequest + payload := network.JoinRoomRequest{ + RoomName: c.roomName, + PlayerName: c.playerName, + } + jsonPayload, _ := json.Marshal(payload) + msg.Payload = jsonPayload + encoder.Encode(msg) + c.setState(waitResponseFromServer) + case 3: + msg.Cmd = network.CmdFinishedGamesRequest + encoder.Encode(msg) + c.setState(waitResponseFromServer) + case 4: + fmt.Print("Enter game id: ") + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(input) + + gameId, err := strconv.Atoi(input) + if err != nil { + fmt.Println("Invalid game id.") + return + } + + msg.Cmd = network.CmdFinishedGameByIdRequest + payload := network.GetFinishedGameByIdRequest{GameID: gameId} + jsonPayload, _ := json.Marshal(payload) + msg.Payload = jsonPayload + encoder.Encode(msg) + c.setState(waitResponseFromServer) + case 5: + os.Exit(0) + default: + fmt.Println("Unknown command.") + return + } +} diff --git a/part_8/tic_tac_toe_v8/client/playing.go b/part_8/tic_tac_toe_v8/client/playing.go new file mode 100644 index 0000000..56b2952 --- /dev/null +++ b/part_8/tic_tac_toe_v8/client/playing.go @@ -0,0 +1,60 @@ +package client + +import ( + "bufio" + "encoding/json" + "fmt" + "strconv" + "strings" + "tic-tac-toe/network" +) + +func (c *Client) playing(reader *bufio.Reader, encoder *json.Encoder) { + fmt.Printf("\nEnter command: or q for exit to main menu\n> ") + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(input) + + if input == "q" { + var msg network.Message + msg.Cmd = network.CmdLeaveRoomRequest + payload := network.LeaveRoomRequest{ + RoomName: c.roomName, + PlayerName: c.playerName, + } + jsonPayload, _ := json.Marshal(payload) + msg.Payload = jsonPayload + encoder.Encode(msg) + c.setState(mainMenu) + return + } + + parts := strings.Fields(input) + if len(parts) != 2 { + fmt.Println("Usage: ") + return + } + + var msg network.Message + row, err1 := strconv.Atoi(parts[0]) + col, err2 := strconv.Atoi(parts[1]) + if err1 != nil || err2 != nil { + fmt.Println("Row and column must be numbers.") + return + } + + if !c.validateMove(row, col) { + return // validateMove prints the error + } + + msg.Cmd = network.CmdMakeMoveRequest + payload := network.MakeMoveRequest{ + RoomName: c.roomName, + PlayerName: c.playerName, + PositionRow: row - 1, + PositionCol: col - 1, + } + jsonPayload, _ := json.Marshal(payload) + msg.Payload = jsonPayload + encoder.Encode(msg) + c.setState(waitResponseFromServer) +} diff --git a/part_8/tic_tac_toe_v8/client/server_message_handlers.go b/part_8/tic_tac_toe_v8/client/server_message_handlers.go new file mode 100644 index 0000000..69e036b --- /dev/null +++ b/part_8/tic_tac_toe_v8/client/server_message_handlers.go @@ -0,0 +1,210 @@ +package client + +import ( + "encoding/json" + "fmt" + "log" + + b "tic-tac-toe/board" + g "tic-tac-toe/game" + "tic-tac-toe/network" +) + +// handleRoomJoinResponse processes the RoomJoinResponse message from the server. +func (c *Client) handleRoomJoinResponse(payload json.RawMessage) { + var res network.RoomJoinResponse + if err := json.Unmarshal(payload, &res); err == nil { + c.mySymbol = res.PlayerSymbol + c.roomName = res.RoomName + if res.Board.Size > 0 { // Check if board is valid + c.board = &res.Board + fmt.Printf("\nSuccessfully joined room '%s' as %s.\n", res.RoomName, res.PlayerSymbol) + c.board.PrintBoard() + } else { + fmt.Printf("\nSuccessfully joined room '%s' as %s. Waiting for game to start...\n", res.RoomName, res.PlayerSymbol) + } + } else { + log.Printf("Error unmarshalling RoomJoinResponse: %v", err) + } + c.setState(waitingOpponentInRoom) +} + +// handleInitGame processes the InitGameResponse message from the server. +func (c *Client) handleInitGame(payload json.RawMessage) { + var res network.InitGameResponse + if err := json.Unmarshal(payload, &res); err == nil { + c.board = &res.Board + c.currentPlayer = res.CurrentPlayer + fmt.Println("\n--- Game Started ---") + c.board.PrintBoard() + c.printTurnInfo() + if res.CurrentPlayer == c.mySymbol { + c.setState(playerMove) + } else { + c.setState(opponentMove) + } + } else { + log.Printf("Error unmarshalling InitGameResponse: %v", err) + } +} + +// handleUpdateState processes the GameStateUpdate message from the server. +func (c *Client) handleUpdateState(payload json.RawMessage) { + var res network.GameStateUpdate + if err := json.Unmarshal(payload, &res); err == nil { + c.board = &res.Board + c.currentPlayer = res.CurrentPlayer + fmt.Println("\n--- Game State Update ---") + c.board.PrintBoard() + c.printTurnInfo() + if res.CurrentPlayer == c.mySymbol { + c.setState(playerMove) + } else { + c.setState(opponentMove) + } + } else { + log.Printf("Error unmarshalling GameStateUpdate: %v", err) + } +} + +// handleEndGame processes the EndGameResponse message from the server. +func (c *Client) handleEndGame(payload json.RawMessage) { + var res network.EndGameResponse + if err := json.Unmarshal(payload, &res); err == nil { + c.board = &res.Board + fmt.Println("\n--- Game Over ---") + c.board.PrintBoard() + if res.CurrentPlayer == b.Empty { + fmt.Println("It's a Draw!") + } else { + fmt.Printf("Player %s wins!\n", res.CurrentPlayer) + } + c.setState(endGame) + fmt.Print("> ") + } else { + log.Printf("Error unmarshalling EndGameResponse: %v", err) + } +} + +// handleError processes the ErrorResponse message from the server. +func (c *Client) handleError(payload json.RawMessage) { + var errPayload network.ErrorResponse + if err := json.Unmarshal(payload, &errPayload); err == nil { + fmt.Printf("\nServer Error: %s\n> ", errPayload.Message) + } else { + log.Printf("Error unmarshalling ErrorResponse: %v", err) + } + c.setState(mainMenu) +} + +// gameModeToString converts GameMode to a string representation. +func gameModeToString(mode g.GameMode) string { + switch mode { + case g.PvP: + return "PvP" + case g.PvC: + return "PvC" + default: + return "Unknown" + } +} + +func difficultyToString(difficulty g.Difficulty) string { + switch difficulty { + case g.Easy: + return "Easy" + case g.Medium: + return "Medium" + case g.Hard: + return "Hard" + default: + return "" + } +} + +// handleFinishedGamesResponse processes the FinishedGamesResponse message from the server. +func (c *Client) handleFinishedGamesResponse(payload json.RawMessage) { + var res network.FinishedGamesResponse + if err := json.Unmarshal(payload, &res); err == nil { + fmt.Println("\nFinished games:") + if res.Games == nil || len(*res.Games) == 0 { + fmt.Println("No finished games.") + } else { + for _, game := range *res.Games { + fmt.Printf("- Game #%d: %s vs %s (Winner: %s) at %v\n", + game.ID, game.WinnerName, + game.AnotherPlayerName, game.WinnerName, + game.Time.Format("2006-01-02 15:04:05"), + ) + } + } + } else { + log.Printf("Error unmarshalling FinishedGamesResponse: %v", err) + } + c.setState(mainMenu) +} + +// handleRoomListResponse processes the RoomListResponse message from the server. +func (c *Client) handleRoomListResponse(payload json.RawMessage) { + var roomList network.RoomListResponse + if err := json.Unmarshal(payload, &roomList); err == nil { + fmt.Println("\nAvailable rooms:") + if len(roomList.Rooms) == 0 { + fmt.Println("No rooms available.") + } else { + for _, room := range roomList.Rooms { + fmt.Printf("- %s (Board Size: %dx%d, Full: %t, Mode: %s, Difficulty: %s)\n", + room.Name, + room.BoardSize, room.BoardSize, + room.IsFull, + gameModeToString(room.GameMode), + difficultyToString(room.Difficult), + ) + } + } + } else { + log.Printf("Error unmarshalling RoomListResponse: %v", err) + } + c.setState(mainMenu) +} + +// handleNickNameResponse processes the NickNameResponse message from the server. +func (c *Client) handleNickNameResponse(payload json.RawMessage) { + var res network.NickNameResponse + if err := json.Unmarshal(payload, &res); err == nil { + fmt.Printf("\nWelcome, %s!\n> ", res.Nickname) + c.setNickname(res.Nickname) + c.setState(mainMenu) + } else { + log.Printf("Error unmarshalling NickNameResponse: %v", err) + } +} + +// handleOpponentLeft processes the OpponentLeft message from the server. +func (c *Client) handleOpponentLeft(payload json.RawMessage) { + var res network.OpponentLeft + if err := json.Unmarshal(payload, &res); err == nil { + fmt.Printf("\nPlayer '%s' has left the game.\n> ", res.Nickname) + } else { + log.Printf("Error unmarshalling OpponentLeft: %v", err) + } + c.setState(waitingOpponentInRoom) +} + +// handleFinishedGameResponse processes the FinishedGameResponse message from the server. +func (c *Client) handleFinishedGameResponse(payload json.RawMessage) { + var res network.FinishedGameResponse + if err := json.Unmarshal(payload, &res); err == nil { + fmt.Println("\nFinished game:") + fmt.Printf("- Game #%d: %s vs %s (Winner: %s) at %v\n", + res.Game.ID, res.Game.WinnerName, + res.Game.AnotherPlayerName, res.Game.WinnerName, + res.Game.Time.Format("2006-01-02 15:04:05"), + ) + c.board = res.Game.Board + c.board.PrintBoard() + } else { + log.Printf("Error unmarshalling FinishedGameResponse: %v", err) + } + c.setState(mainMenu) +} diff --git a/part_8/tic_tac_toe_v8/client/utils.go b/part_8/tic_tac_toe_v8/client/utils.go new file mode 100644 index 0000000..279ca0d --- /dev/null +++ b/part_8/tic_tac_toe_v8/client/utils.go @@ -0,0 +1,38 @@ +package client + +import ( + "fmt" + b "tic-tac-toe/board" +) + +func (c *Client) printTurnInfo() { + if c.board == nil { + return + } + if c.currentPlayer == c.mySymbol { + fmt.Println("It's your turn.") + } else if c.currentPlayer != b.Empty { + fmt.Printf("It's player %s's turn.\n", c.currentPlayer) + } else { + // Game might be over or in an intermediate state + } + fmt.Print("> ") +} + +// validateMove checks if a move is valid based on the local board state. +func (c *Client) validateMove(row, col int) bool { + if c.board == nil { + fmt.Println("Game has not started yet.") + return false + } + if row < 1 || row > c.board.Size || col < 1 || col > c.board.Size { + fmt.Printf("Invalid move. Row and column must be between 1 and %d.\n", c.board.Size) + return false + } + // Convert to 0-indexed for board access + if c.board.Board[row-1][col-1] != b.Empty { + fmt.Println("Invalid move. Cell is already occupied.") + return false + } + return true +} diff --git a/part_8/tic_tac_toe_v8/database/crud.go b/part_8/tic_tac_toe_v8/database/crud.go new file mode 100644 index 0000000..55bfb0a --- /dev/null +++ b/part_8/tic_tac_toe_v8/database/crud.go @@ -0,0 +1,74 @@ +package database + +import ( + "encoding/json" + + m "tic-tac-toe/model" +) + +func (r *SQLiteRepository) createPlayer( + nickName string, +) (*Player, error) { + player := &Player{NickName: nickName} + if err := r.db.Create(player).Error; err != nil { + return nil, err + } + return player, nil +} + +func (r *SQLiteRepository) getPlayer( + nickName string, +) (*Player, error) { + var player Player + if err := r.db.Where( + "nick_name = ?", nickName, + ).First(&player).Error; err != nil { + return nil, err + } + return &player, nil +} + +func (r *SQLiteRepository) SaveFinishedGame( + snapshot *m.FinishGameSnapshot) error { + boardJSON, err := json.Marshal(snapshot.Board) + if err != nil { + return err + } + + player, _ := r.getPlayer(snapshot.WinnerName) + if player == nil { + player, _ = r.createPlayer(snapshot.WinnerName) + } + + return r.db.Create(&PlayerFinishGame{ + BoardJSON: boardJSON, + PlayerFigure: int(snapshot.PlayerFigure), + WinnerName: snapshot.WinnerName, + PlayerNickName: player.NickName, + Time: snapshot.Time, + }).Error +} + +func (r *SQLiteRepository) GetAllFinishedGames() (*[]m.FinishGameSnapshot, error) { + var playerFinishGames []PlayerFinishGame + if err := r.db.Find(&playerFinishGames).Error; err != nil { + return nil, err + } + var finishGameSnapshots []m.FinishGameSnapshot + for _, playerFinishGame := range playerFinishGames { + temp, err := playerFinishGame.ToModel() + if err != nil { + return nil, err + } + finishGameSnapshots = append(finishGameSnapshots, *temp) + } + return &finishGameSnapshots, nil +} + +func (r *SQLiteRepository) GetFinishedGameById(id int) (*m.FinishGameSnapshot, error) { + var playerFinishGame PlayerFinishGame + if err := r.db.Where("id = ?", id).First(&playerFinishGame).Error; err != nil { + return nil, err + } + return playerFinishGame.ToModel() +} diff --git a/part_8/tic_tac_toe_v8/database/database.go b/part_8/tic_tac_toe_v8/database/database.go new file mode 100644 index 0000000..8068d05 --- /dev/null +++ b/part_8/tic_tac_toe_v8/database/database.go @@ -0,0 +1,45 @@ +package database + +import ( + "fmt" + "os" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +type SQLiteRepository struct { + db *gorm.DB // заменили на *gorm.DB +} + +func NewSQLiteRepository() (*SQLiteRepository, error) { + // Создаем репозиторий + repository := &SQLiteRepository{} + + // Проверяем существование файла базы данных + dbExists := true + if _, err := os.Stat(dbName); os.IsNotExist(err) { + dbExists = false + } + + // Открываем соединение с базой данных + db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{}) + if err != nil { + return nil, fmt.Errorf("failed to connect to database: %w", err) + } + + // Сохраняем соединение в репозитории + repository.db = db + + // Если база данных только что создана, выполняем миграцию + if !dbExists { + fmt.Println("Creating new database schema") + if err := db.AutoMigrate(&Player{}, &PlayerFinishGame{}); err != nil { + return nil, fmt.Errorf("failed to migrate database: %w", err) + } + } else { + fmt.Println("Using existing database") + } + + return repository, nil +} diff --git a/part_8/tic_tac_toe_v8/database/db_definition.go b/part_8/tic_tac_toe_v8/database/db_definition.go new file mode 100644 index 0000000..475aea3 --- /dev/null +++ b/part_8/tic_tac_toe_v8/database/db_definition.go @@ -0,0 +1,12 @@ +package database + +import "errors" + +const dbName = "tic_tac_toe.db" + +var ( + ErrDuplicate = errors.New("record already exists") + ErrNotExists = errors.New("row not exists") + ErrUpdateFailed = errors.New("update failed") + ErrDeleteFailed = errors.New("delete failed") +) diff --git a/part_8/tic_tac_toe_v8/database/i_repository.go b/part_8/tic_tac_toe_v8/database/i_repository.go new file mode 100644 index 0000000..55b5d8d --- /dev/null +++ b/part_8/tic_tac_toe_v8/database/i_repository.go @@ -0,0 +1,13 @@ +package database + +import "tic-tac-toe/model" + +// Интерфейс для работы с базой данных +type IRepository interface { + // Сохраняет информацию о завершенной игре + SaveFinishedGame(snapshot *model.FinishGameSnapshot) error + // Получает все завершенные игры для указанного игрока + GetAllFinishedGames() (*[]model.FinishGameSnapshot, error) + // Получает конкретную завершенную игру по ID + GetFinishedGameById(id int) (*model.FinishGameSnapshot, error) +} diff --git a/part_8/tic_tac_toe_v8/database/models.go b/part_8/tic_tac_toe_v8/database/models.go new file mode 100644 index 0000000..cff1e13 --- /dev/null +++ b/part_8/tic_tac_toe_v8/database/models.go @@ -0,0 +1,21 @@ +package database + +import "time" + +// Player представляет модель таблицы +// для хранения профилей игроков в БД +type Player struct { + NickName string `gorm:"primary_key;not null"` +} + +// PlayerFinishGame представляет модель таблицы +// для хранения завершенной игры в БД +type PlayerFinishGame struct { + ID int `gorm:"primary_key;autoIncrement;not null"` + WinnerName string `gorm:"not null"` + BoardJSON []byte `gorm:"type:json;not null"` + PlayerFigure int `gorm:"not null"` + Time time.Time `gorm:"not null"` + PlayerNickName string `gorm:"not null"` + Player *Player `gorm:"foreignKey:PlayerNickName;references:NickName"` +} diff --git a/part_8/tic_tac_toe_v8/database/utils.go b/part_8/tic_tac_toe_v8/database/utils.go new file mode 100644 index 0000000..41061a1 --- /dev/null +++ b/part_8/tic_tac_toe_v8/database/utils.go @@ -0,0 +1,36 @@ +package database + +import ( + "encoding/json" + + b "tic-tac-toe/board" + m "tic-tac-toe/model" +) + +// Задаем имя таблицы для структуры Player +func (p *Player) TableName() string { + return "players" +} + +// Задаем имя таблицы для структуры PlayerFinishGame +func (pfg *PlayerFinishGame) TableName() string { + return "player_finish_games" +} + +// Преобразуем таблицу PlayerFinishGame в модель PlayerFinishGame +// из пакета model +func (f *PlayerFinishGame) ToModel() (*m.FinishGameSnapshot, error) { + var board b.Board + if err := json.Unmarshal(f.BoardJSON, &board); err != nil { + return nil, err + } + + return &m.FinishGameSnapshot{ + ID: f.ID, + Board: &board, + PlayerFigure: b.BoardField(f.PlayerFigure), + WinnerName: f.WinnerName, + AnotherPlayerName: f.PlayerNickName, + Time: f.Time, + }, nil +} diff --git a/part_8/tic_tac_toe_v8/game/game_state.go b/part_8/tic_tac_toe_v8/game/game_state.go new file mode 100644 index 0000000..24468ee --- /dev/null +++ b/part_8/tic_tac_toe_v8/game/game_state.go @@ -0,0 +1,31 @@ +package game + +type GameState int + +// состояние игрового процесса +const ( + WaitingOpponent GameState = iota + Draw + CrossWin + NoughtWin + CrossStep + NoughtStep +) + +// Режим игры +type GameMode int + +const ( + PvP GameMode = iota + PvC +) + +// Уровни сложности компьютера +type Difficulty int + +const ( + None Difficulty = iota + Easy + Medium + Hard +) diff --git a/part_8/tic_tac_toe_v8/go.mod b/part_8/tic_tac_toe_v8/go.mod new file mode 100644 index 0000000..ed636a3 --- /dev/null +++ b/part_8/tic_tac_toe_v8/go.mod @@ -0,0 +1,16 @@ +module tic-tac-toe + +go 1.24.0 + +require github.com/mattn/go-sqlite3 v1.14.28 // indirect + +require ( + gorm.io/driver/sqlite v1.6.0 + gorm.io/gorm v1.30.0 +) + +require ( + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + golang.org/x/text v0.26.0 // indirect +) diff --git a/part_8/tic_tac_toe_v8/go.sum b/part_8/tic_tac_toe_v8/go.sum new file mode 100644 index 0000000..2c20abd --- /dev/null +++ b/part_8/tic_tac_toe_v8/go.sum @@ -0,0 +1,14 @@ +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= +gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= diff --git a/part_8/tic_tac_toe_v8/main.go b/part_8/tic_tac_toe_v8/main.go new file mode 100644 index 0000000..3731dc2 --- /dev/null +++ b/part_8/tic_tac_toe_v8/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "flag" + "log" + + "tic-tac-toe/client" + "tic-tac-toe/database" + "tic-tac-toe/server" +) + +func main() { + mode := flag.String("mode", "client", "start in 'server' or 'client' mode") + addr := flag.String("addr", ":8088", "address to run on") + flag.Parse() + repository, err := database.NewSQLiteRepository() + if err != nil { + log.Fatalf("Failed to create repository: %v", err) + } + + switch *mode { + case "server": + srv, err := server.NewServer(*addr, repository) + if err != nil { + log.Fatalf("Failed to create server: %v", err) + } + srv.Start() + case "client": + cli, err := client.NewClient(*addr) + if err != nil { + log.Fatalf("Failed to connect to server: %v", err) + } + cli.Start() + default: + log.Fatalf("Unknown mode: %s. Use 'server' or 'client'.", *mode) + } +} diff --git a/part_8/tic_tac_toe_v8/model/finish_game_shapshot.go b/part_8/tic_tac_toe_v8/model/finish_game_shapshot.go new file mode 100644 index 0000000..ac739b1 --- /dev/null +++ b/part_8/tic_tac_toe_v8/model/finish_game_shapshot.go @@ -0,0 +1,15 @@ +package model + +import ( + "tic-tac-toe/board" + "time" +) + +type FinishGameSnapshot struct { + ID int `json:"id"` + Board *board.Board `json:"board"` + PlayerFigure board.BoardField `json:"player_figure"` + WinnerName string `json:"winner_name"` + AnotherPlayerName string `json:"another_player_name"` + Time time.Time `json:"time"` +} diff --git a/part_8/tic_tac_toe_v8/network/client_to_server.go b/part_8/tic_tac_toe_v8/network/client_to_server.go new file mode 100644 index 0000000..42595e6 --- /dev/null +++ b/part_8/tic_tac_toe_v8/network/client_to_server.go @@ -0,0 +1,52 @@ +package network + +const ( + // Client to Server Commands + CmdNickname Command = "nickname" + CmdJoinRoomRequest Command = "join_room" + CmdLeaveRoomRequest Command = "leave_room" + CmdListRoomsRequest Command = "list_rooms" + CmdMakeMoveRequest Command = "make_move" + CmdFinishedGamesRequest Command = "get_finished_games" + CmdFinishedGameByIdRequest Command = "get_finished_game_by_id" +) + +// LoginRequest отправляется клиентом для входа в систему. +type NicknameRequest struct { + Nickname string `json:"nickname"` +} + +// JoinRoomRequest отправляется клиентом для подключения к существующей комнате. +type JoinRoomRequest struct { + RoomName string `json:"room_name"` + PlayerName string `json:"player_name"` +} + +// LeaveRoomRequest отправляется клиентом для выхода из текущей комнаты. +type LeaveRoomRequest struct { + RoomName string `json:"room_name"` + PlayerName string `json:"player_name"` +} + +// ListRoomsRequest отправляется клиентом для получения списка доступных комнат. +// Обычно для этого запроса не требуется специальная полезная нагрузка. +type ListRoomsRequest struct { +} + +// MakeMoveRequest отправляется клиентом для совершения хода в игре. +type MakeMoveRequest struct { + RoomName string `json:"room_name"` + PlayerName string `json:"player_name"` + PositionRow int `json:"position_row"` + PositionCol int `json:"position_col"` +} + +// GetFinishedGamesRequest отправляется клиентом для получения списка завершенных игр. +// Обычно для этого запроса не требуется специальная полезная нагрузка, если запрашиваются все игры для пользователя. +type GetFinishedGamesRequest struct { +} + +// GetFinishedGameByIdRequest отправляется клиентом для получения конкретной завершенной игры. +type GetFinishedGameByIdRequest struct { + GameID int `json:"game_id"` +} diff --git a/part_8/tic_tac_toe_v8/network/protocol.go b/part_8/tic_tac_toe_v8/network/protocol.go new file mode 100644 index 0000000..d6e7888 --- /dev/null +++ b/part_8/tic_tac_toe_v8/network/protocol.go @@ -0,0 +1,10 @@ +package network + +import "encoding/json" + +type Command string + +type Message struct { + Cmd Command `json:"command"` + Payload json.RawMessage `json:"payload,omitempty"` +} diff --git a/part_8/tic_tac_toe_v8/network/server_to_client.go b/part_8/tic_tac_toe_v8/network/server_to_client.go new file mode 100644 index 0000000..e4e113e --- /dev/null +++ b/part_8/tic_tac_toe_v8/network/server_to_client.go @@ -0,0 +1,94 @@ +package network + +import ( + b "tic-tac-toe/board" + g "tic-tac-toe/game" + "tic-tac-toe/model" +) + +const ( + // Server to Client Commands + CmdUpdateState Command = "update_state" + CmdError Command = "error" + CmdNickNameResponse Command = "nick_name_response" + CmdRoomCreated Command = "room_created" + CmdRoomJoinResponse Command = "room_join_response" + CmdRoomListResponse Command = "room_list_response" + CmdInitGame Command = "init_game" + CmdOpponentLeft Command = "opponent_left" + CmdEndGame Command = "end_game" + CmdFinishedGamesResponse Command = "finished_games_response" + CmdFinishedGameResponse Command = "finished_game_response" +) + +// сообщение о том, что противник покинул игру +// инициализирующее сообщение в начале партии + +// InitGameResponse отправляется сервером при инициализации игры. +type InitGameResponse struct { + Board b.Board `json:"board"` + CurrentPlayer b.BoardField `json:"current_player"` +} + +// EndGameResponse отправляется сервером при завершении игры. +type EndGameResponse struct { + Board b.Board `json:"board"` + CurrentPlayer b.BoardField `json:"current_player"` +} + +type OpponentLeft struct { + Nickname string `json:"nickname"` +} + +// RoomInfo содержит информацию о комнате. +type RoomInfo struct { + Name string `json:"name"` + BoardSize int `json:"board_size"` + IsFull bool `json:"is_full"` + GameMode g.GameMode `json:"game_mode"` + Difficult g.Difficulty `json:"difficult"` +} + +// RoomListсодержит список доступных комнат. +type RoomListResponse struct { + Rooms []RoomInfo `json:"rooms"` +} + +// GameStateUpdate содержит информацию об обновлении состояния игры. +type GameStateUpdate struct { + Board b.Board `json:"board"` + CurrentPlayer b.BoardField `json:"current_player"` +} + +// ErrorResponse отправляется сервером при возникновении ошибки. +type ErrorResponse struct { + Message string `json:"message"` +} + +// NickNameResponse отправляется сервером при успешном входе клиента. +type NickNameResponse struct { + Nickname string `json:"nickname"` +} + +// RoomCreatedResponse отправляется сервером после успешного создания комнаты. +type RoomCreatedResponse struct { + RoomID string `json:"room_id"` + RoomName string `json:"room_name"` +} + +// RoomJoinResponse отправляется сервером, когда клиент успешно присоединился к комнате. +type RoomJoinResponse struct { + RoomName string `json:"room_name"` + PlayerSymbol b.BoardField `json:"player_symbol"` + Board b.Board `json:"board"` +} + +// FinishedGamesResponse отправляется сервером со списком завершенных игр. +type FinishedGamesResponse struct { + Games *[]model.FinishGameSnapshot `json:"games"` +} + +// FinishedGameResponse отправляется сервером с информацией о конкретной завершенной игре. +type FinishedGameResponse struct { + Game *model.FinishGameSnapshot `json:"game"` +} diff --git a/part_8/tic_tac_toe_v8/player/computer_player.go b/part_8/tic_tac_toe_v8/player/computer_player.go new file mode 100644 index 0000000..66f14bb --- /dev/null +++ b/part_8/tic_tac_toe_v8/player/computer_player.go @@ -0,0 +1,342 @@ +package player + +import ( + "fmt" + "math/rand" + "net" + b "tic-tac-toe/board" + g "tic-tac-toe/game" + "tic-tac-toe/network" + "time" +) + +// Структура для представления игрока-компьютера +type ComputerPlayer struct { + Figure b.BoardField `json:"figure"` + Difficulty g.Difficulty `json:"difficulty"` + rand *rand.Rand +} + +// Создаем нового игрока-компьютера с заданным уровнем сложности +func NewComputerPlayer( + figure b.BoardField, + difficulty g.Difficulty, +) *ComputerPlayer { + source := rand.NewSource(time.Now().UnixNano()) + return &ComputerPlayer{ + Figure: figure, + Difficulty: difficulty, + rand: rand.New(source), + } +} + +func (p *ComputerPlayer) GetSymbol() string { + if p.Figure == b.Cross { + return "X" + } + return "O" +} + +func (p *ComputerPlayer) SendMessage(msg *network.Message) { + +} + +func (p *ComputerPlayer) GetNickname() string { + return "Computer" +} + +func (p *ComputerPlayer) SwitchPlayer() { + if p.Figure == b.Cross { + p.Figure = b.Nought + } else { + p.Figure = b.Cross + } +} + +func (p *ComputerPlayer) GetFigure() b.BoardField { + return p.Figure +} + +func (p *ComputerPlayer) IsComputer() bool { + return true +} + +// Реализуем ход компьютера в зависимости от выбранной сложности +func (p *ComputerPlayer) MakeMove(board *b.Board) (int, int, bool) { + fmt.Printf("%s (Computer) making move... ", p.GetSymbol()) + + var row, col int + switch p.Difficulty { + case g.Easy: + row, col = p.makeEasyMove(board) + case g.Medium: + row, col = p.makeMediumMove(board) + case g.Hard: + row, col = p.makeHardMove(board) + } + + fmt.Printf("Move made (%d, %d)\n", row+1, col+1) + return row, col, true +} + +// Легкий уровень: случайный ход на свободную клетку +func (p *ComputerPlayer) makeEasyMove(board *b.Board) (int, int) { + emptyCells := p.getEmptyCells(board) + if len(emptyCells) == 0 { + return -1, -1 + } + + // Выбираем случайную свободную клетку + randomIndex := p.rand.Intn(len(emptyCells)) + return emptyCells[randomIndex][0], emptyCells[randomIndex][1] +} + +func (p *ComputerPlayer) CheckSocket(conn net.Conn) bool { + return false +} + +// Средний уровень: проверяет возможность выигрыша +// или блокировки выигрыша противника +func (p *ComputerPlayer) makeMediumMove(board *b.Board) (int, int) { + // Проверяем, можем ли мы выиграть за один ход + if move := p.findWinningMove(board, p.Figure); move != nil { + return move[0], move[1] + } + + // Проверяем, нужно ли блокировать победу противника + opponentFigure := b.Cross + if p.Figure == b.Cross { + opponentFigure = b.Nought + } + + if move := p.findWinningMove(board, opponentFigure); move != nil { + return move[0], move[1] + } + + // Занимаем центр, если свободен (хорошая стратегия) + center := board.Size / 2 + if board.Board[center][center] == b.Empty { + return center, center + } + + // Занимаем угол, если свободен + corners := [][]int{ + {0, 0}, + {0, board.Size - 1}, + {board.Size - 1, 0}, + {board.Size - 1, board.Size - 1}, + } + + for _, corner := range corners { + if board.Board[corner[0]][corner[1]] == b.Empty { + return corner[0], corner[1] + } + } + + // Если нет лучшего хода, делаем случайный ход + return p.makeEasyMove(board) +} + +// Сложный уровень: использует алгоритм минимакс для оптимального хода +func (p *ComputerPlayer) makeHardMove(board *b.Board) (int, int) { + // Если доска пустая, ходим в центр или угол (оптимальный первый ход) + emptyCells := p.getEmptyCells(board) + if len(emptyCells) == board.Size*board.Size { + // Первый ход - центр или угол + center := board.Size / 2 + return center, center + } + + // Используем минимакс для доски 3x3 + // Для больших досок это слишком ресурсоемко + if board.Size <= 3 { + bestScore := -1000 + bestMove := []int{-1, -1} + + // Создаем канал для результатов + type moveResult struct { + move []int + score int + } + resultChan := make(chan moveResult, len(emptyCells)) + + // Запускаем горутину для каждого возможного хода + for _, cell := range emptyCells { + go func(cell []int) { + row, col := cell[0], cell[1] + // Копируем доску чтобы избежать гонок данных + boardCopy := p.copyBoard(board) + + // Пробуем сделать ход + boardCopy.Board[row][col] = p.Figure + + // Вычисляем оценку хода через минимакс + score := p.minimax(boardCopy, 0, false) + + // Отправляем результат в канал + resultChan <- moveResult{ + move: []int{row, col}, + score: score, + } + }(cell) + } + + // Собираем результаты всех горутин + for i := 0; i < len(emptyCells); i++ { + result := <-resultChan + if result.score > bestScore { + bestScore = result.score + bestMove = result.move + } + } + + return bestMove[0], bestMove[1] + } + + // Для больших досок выбираем случайно одну из трех параллельных стратегий + strategyChoice := p.rand.Intn(3) + switch strategyChoice { + case 0: + fmt.Println("Using limited-depth parallel minimax strategy") + return p.makeLimitedDepthMinimax(board) + case 1: + fmt.Println("Using parallel heuristic evaluation strategy") + return p.makeParallelHeuristicMove(board) + case 2: + fmt.Println("Using zone-based parallel analysis strategy") + return p.makeZoneBasedMove(board) + default: + //В случае ошибки используем стратегию среднего уровня + return p.makeMediumMove(board) + } +} + +// Алгоритм минимакс для определения оптимального хода +func (p *ComputerPlayer) minimax( + board *b.Board, + depth int, isMaximizing bool, +) int { + opponentFigure := b.Cross + if p.Figure == b.Cross { + opponentFigure = b.Nought + } + + // Проверяем терминальное состояние + if board.CheckWin(p.Figure) { + return 10 - depth // Выигрыш, чем быстрее, тем лучше + } else if board.CheckWin(opponentFigure) { + return depth - 10 // Проигрыш, чем дольше, тем лучше + } else if board.CheckDraw() { + return 0 // Ничья + } + + emptyCells := p.getEmptyCells(board) + + if isMaximizing { + bestScore := -1000 + + // Проходим по всем свободным клеткам + for _, cell := range emptyCells { + row, col := cell[0], cell[1] + + // Делаем ход + board.Board[row][col] = p.Figure + + // Рекурсивно оцениваем ход + score := p.minimax(board, depth+1, false) + + // Отменяем ход + board.Board[row][col] = b.Empty + + bestScore = max(score, bestScore) + } + + return bestScore + } else { + bestScore := 1000 + + // Проходим по всем свободным клеткам + for _, cell := range emptyCells { + row, col := cell[0], cell[1] + + // Делаем ход противника + board.Board[row][col] = opponentFigure + + // Рекурсивно оцениваем ход + score := p.minimax(board, depth+1, true) + + // Отменяем ход + board.Board[row][col] = b.Empty + + bestScore = min(score, bestScore) + } + + return bestScore + } +} + +// Вспомогательная функция для поиска хода, приводящего к выигрышу +func (p *ComputerPlayer) findWinningMove( + board *b.Board, + figure b.BoardField, +) []int { + for _, cell := range p.getEmptyCells(board) { + row, col := cell[0], cell[1] + + // Пробуем сделать ход + board.Board[row][col] = figure + + // Проверяем, приведет ли этот ход к выигрышу + if board.CheckWin(figure) { + // Отменяем ход и возвращаем координаты + board.Board[row][col] = b.Empty + return []int{row, col} + } + + // Отменяем ход + board.Board[row][col] = b.Empty + } + + return nil // Нет выигрышного хода +} + +// Получение списка пустых клеток +func (p *ComputerPlayer) getEmptyCells(board *b.Board) [][]int { + var emptyCells [][]int + + for i := 0; i < board.Size; i++ { + for j := 0; j < board.Size; j++ { + if board.Board[i][j] == b.Empty { + emptyCells = append(emptyCells, []int{i, j}) + } + } + } + + return emptyCells +} + +// Вспомогательные функции max и min +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +// Копирование доски для избежания гонок данных при параллельном вычислении +func (p *ComputerPlayer) copyBoard(board *b.Board) *b.Board { + newBoard := b.NewBoard(board.Size) + for i := 0; i < board.Size; i++ { + for j := 0; j < board.Size; j++ { + newBoard.Board[i][j] = board.Board[i][j] + } + } + return newBoard +} diff --git a/part_8/tic_tac_toe_v8/player/i_player.go b/part_8/tic_tac_toe_v8/player/i_player.go new file mode 100644 index 0000000..a98111e --- /dev/null +++ b/part_8/tic_tac_toe_v8/player/i_player.go @@ -0,0 +1,32 @@ +package player + +import ( + "net" + b "tic-tac-toe/board" + "tic-tac-toe/network" +) + +// Интерфейс для любого игрока, будь то человек или компьютер +type IPlayer interface { + // Получение символа игрока (X или O) + GetSymbol() string + + // Переключение хода на другого игрока + SwitchPlayer() + + SendMessage(msg *network.Message) + + GetNickname() string + + // Получение текущей фигуры игрока + GetFigure() b.BoardField + + // Выполнение хода игрока + // Возвращает координаты хода (x, y) и признак успешности + MakeMove(board *b.Board) (int, int, bool) + + // Проверка, является ли игрок компьютером + IsComputer() bool + + CheckSocket(conn net.Conn) bool +} diff --git a/part_8/tic_tac_toe_v8/player/player.go b/part_8/tic_tac_toe_v8/player/player.go new file mode 100644 index 0000000..8b7934d --- /dev/null +++ b/part_8/tic_tac_toe_v8/player/player.go @@ -0,0 +1,95 @@ +package player + +import ( + "encoding/json" + "fmt" + "net" + "strconv" + "strings" + b "tic-tac-toe/board" + "tic-tac-toe/network" +) + +// Структура для представления игрока-человека +type HumanPlayer struct { + Figure b.BoardField `json:"figure"` + Nickname string `json:"nickname"` + Conn *net.Conn `json:"-"` +} + +func NewHumanPlayer( + nickname string, conn *net.Conn, +) *HumanPlayer { + return &HumanPlayer{Figure: b.Cross, Nickname: nickname, Conn: conn} +} + +func (p *HumanPlayer) CheckSocket(conn net.Conn) bool { + return *p.Conn == conn +} + +// Возвращаем символ игрока +func (p *HumanPlayer) GetSymbol() string { + if p.Figure == b.Cross { + return "X" + } + return "O" +} + +func (p *HumanPlayer) SendMessage(msg *network.Message) { + json.NewEncoder(*p.Conn).Encode(msg) +} + +func (p *HumanPlayer) GetNickname() string { + return p.Nickname +} + +// Изменяем фигуру текущего игрока +func (p *HumanPlayer) SwitchPlayer() { + if p.Figure == b.Cross { + p.Figure = b.Nought + } else { + p.Figure = b.Cross + } +} + +// Возвращаем текущую фигуру игрока +func (p *HumanPlayer) GetFigure() b.BoardField { + return p.Figure +} + +// Метод-заглушка, т.к. ввод игрока осуществляется на +// уровне пакета game, где нужно еще отрабатывать +// команду на выход и сохранение игровой сессии +func (p *HumanPlayer) MakeMove(board *b.Board) (int, int, bool) { + return -1, -1, false +} + +// Обрабатываем строку ввода и +// преобразуем ее в координаты хода +func (p *HumanPlayer) ParseMove( + input string, + board *b.Board, +) (int, int, bool) { + parts := strings.Fields(input) + if len(parts) != 2 { + fmt.Println("Invalid input. Please try again.") + return -1, -1, false + } + + row, err1 := strconv.Atoi(parts[0]) + col, err2 := strconv.Atoi(parts[1]) + if err1 != nil || err2 != nil || + row < 1 || col < 1 || row > board.Size || + col > board.Size { + fmt.Println("Invalid input. Please try again.") + return -1, -1, false + } + + // Преобразуем введенные координаты (начиная с 1) + // в индексы массива (начиная с 0) + return row - 1, col - 1, true +} + +func (p *HumanPlayer) IsComputer() bool { + return false +} diff --git a/part_8/tic_tac_toe_v8/player/strategy_heuristic.go b/part_8/tic_tac_toe_v8/player/strategy_heuristic.go new file mode 100644 index 0000000..52cbaa0 --- /dev/null +++ b/part_8/tic_tac_toe_v8/player/strategy_heuristic.go @@ -0,0 +1,169 @@ +package player + +import ( + "sync" + b "tic-tac-toe/board" +) + +// Метод запуска параллельной эвристической оценки ходов +func (p *ComputerPlayer) makeParallelHeuristicMove(board *b.Board) (int, int) { + bestScore := -100000 + var bestMove []int + emptyCells := p.getEmptyCells(board) + + if len(emptyCells) == 0 { + return -1, -1 + } + if len(emptyCells) == 1 { + return emptyCells[0][0], emptyCells[0][1] + } + + // Создаем канал для результатов + type moveResult struct { + move []int + score int + } + resultChan := make(chan moveResult, len(emptyCells)) + var wg sync.WaitGroup + + // Запускаем горутины для каждого возможного хода + for _, cell := range emptyCells { + wg.Add(1) + go func(r, c int) { + defer wg.Done() + boardCopy := p.copyBoard(board) + boardCopy.Board[r][c] = p.Figure + score := p.evaluateBoardHeuristic(boardCopy, p.Figure) + resultChan <- moveResult{move: []int{r, c}, score: score} + }(cell[0], cell[1]) + } + + wg.Wait() + close(resultChan) + + // Определяем лучший ход + for result := range resultChan { + if result.score > bestScore { + bestScore = result.score + bestMove = result.move + } + } + + if bestMove == nil { + // Если по какой-то причине лучший ход не найден (маловероятно) + // переходи на стратегию поведения среднего уровня сложности + return p.makeMediumMove(board) + } + + return bestMove[0], bestMove[1] +} + +// Эвристическая оценка доски +// Количество рядов, столбцов или диагоналей, где у игрока есть N фигур +// и остальные клетки пусты. Также учитываем блокировку противника. +func (p *ComputerPlayer) evaluateBoardHeuristic( + board *b.Board, player b.BoardField, +) int { + score := 0 + opponent := b.Cross + if player == b.Cross { + opponent = b.Nought + } + + // Оценка за почти выигрышные линии для игрока + // Почти выигрыш + score += p.countPotentialLines(board, player, board.Size-1) * 100 + // Две фигуры в ряд (для Size > 2) + score += p.countPotentialLines(board, player, board.Size-2) * 10 + + // Штраф за почти выигрышные линии для оппонента (блокировка) + // Блокировка почти выигрыша оппонента + score -= p.countPotentialLines(board, opponent, board.Size-1) * 90 + // Блокировка двух фигур оппонента + score -= p.countPotentialLines(board, opponent, board.Size-2) * 5 + + // Бонус за занятие центра (особенно на нечетных досках) + if board.Size%2 == 1 { + center := board.Size / 2 + if board.Board[center][center] == player { + score += 5 + } else if board.Board[center][center] == opponent { + score -= 5 + } + } + return score +} + +// Вспомогательная функция для подсчета потенциальных линий +func (p *ComputerPlayer) countPotentialLines( + board *b.Board, player b.BoardField, numPlayerSymbols int, +) int { + count := 0 + lineSize := board.Size + + // Проверка строк + for r := 0; r < lineSize; r++ { + playerSymbols := 0 + emptySymbols := 0 + for c := 0; c < lineSize; c++ { + if board.Board[r][c] == player { + playerSymbols++ + } else if board.Board[r][c] == b.Empty { + emptySymbols++ + } + } + if playerSymbols == numPlayerSymbols && + (playerSymbols+emptySymbols) == lineSize { + count++ + } + } + + // Проверка столбцов + for c := 0; c < lineSize; c++ { + playerSymbols := 0 + emptySymbols := 0 + for r := 0; r < lineSize; r++ { + if board.Board[r][c] == player { + playerSymbols++ + } else if board.Board[r][c] == b.Empty { + emptySymbols++ + } + } + if playerSymbols == numPlayerSymbols && + (playerSymbols+emptySymbols) == lineSize { + count++ + } + } + + // Проверка главной диагонали + playerSymbolsDiag1 := 0 + emptySymbolsDiag1 := 0 + for i := 0; i < lineSize; i++ { + if board.Board[i][i] == player { + playerSymbolsDiag1++ + } else if board.Board[i][i] == b.Empty { + emptySymbolsDiag1++ + } + } + if playerSymbolsDiag1 == numPlayerSymbols && + (playerSymbolsDiag1+emptySymbolsDiag1) == lineSize { + count++ + } + + // Проверка побочной диагонали + playerSymbolsDiag2 := 0 + emptySymbolsDiag2 := 0 + for i := 0; i < lineSize; i++ { + if board.Board[i][lineSize-1-i] == player { + playerSymbolsDiag2++ + } else if board.Board[i][lineSize-1-i] == b.Empty { + emptySymbolsDiag2++ + } + } + if playerSymbolsDiag2 == numPlayerSymbols && + (playerSymbolsDiag2+emptySymbolsDiag2) == lineSize { + count++ + } + + return count +} diff --git a/part_8/tic_tac_toe_v8/player/strategy_limited_minimax.go b/part_8/tic_tac_toe_v8/player/strategy_limited_minimax.go new file mode 100644 index 0000000..b7acf10 --- /dev/null +++ b/part_8/tic_tac_toe_v8/player/strategy_limited_minimax.go @@ -0,0 +1,114 @@ +package player + +import ( + "sync" + b "tic-tac-toe/board" +) + +const maxDepth = 2 // Ограничение глубины для минимакса + +// Метод запуска стратегии с ограничением глубины для минимакса +func (p *ComputerPlayer) makeLimitedDepthMinimax(board *b.Board) (int, int) { + bestScore := -100000 + var bestMove []int + emptyCells := p.getEmptyCells(board) + + if len(emptyCells) == 0 { + return -1, -1 // Нет доступных ходов + } + if len(emptyCells) == 1 { + return emptyCells[0][0], emptyCells[0][1] // Единственный возможный ход + } + + // Создаем канал для результатов + type moveResult struct { + move []int + score int + } + resultChan := make(chan moveResult, len(emptyCells)) + var wg sync.WaitGroup + + // Запускаем горутины для каждого возможного хода + for _, cell := range emptyCells { + wg.Add(1) + go func(r, c int) { + defer wg.Done() + boardCopy := p.copyBoard(board) + boardCopy.Board[r][c] = p.Figure + score := p.minimaxRecursive(boardCopy, 0, false, maxDepth) + resultChan <- moveResult{move: []int{r, c}, score: score} + }(cell[0], cell[1]) + } + + wg.Wait() // Ждем завершения всех горутин + close(resultChan) // Закрываем канал + + // Определяем лучший ход + for result := range resultChan { + if result.score > bestScore { + bestScore = result.score + bestMove = result.move + } + } + + if bestMove == nil { + // Если по какой-то причине лучший ход не найден (маловероятно) + // переходи на стратегию поведения среднего уровня сложности + return p.makeMediumMove(board) + } + + return bestMove[0], bestMove[1] +} + +// Рекурсивная часть минимакса с ограничением глубины +func (p *ComputerPlayer) minimaxRecursive( + board *b.Board, depth int, isMaximizing bool, + maxDepthLimit int, +) int { + opponentFigure := b.Cross + if p.Figure == b.Cross { + opponentFigure = b.Nought + } + + if board.CheckWin(p.Figure) { + return 10 - depth // Выигрыш текущего игрока + } + if board.CheckWin(opponentFigure) { + return depth - 10 // Проигрыш текущего игрока (выигрыш оппонента) + } + if board.CheckDraw() { + return 0 // Ничья + } + + if depth >= maxDepthLimit { // Ограничение глубины + // Если достигнута максимальная глубина, используем эвристическую оценку + return p.evaluateBoardHeuristic(board, p.Figure) + } + + emptyCells := p.getEmptyCells(board) + + if isMaximizing { + bestScore := -100000 + for _, cell := range emptyCells { + boardCopy := p.copyBoard(board) + boardCopy.Board[cell[0]][cell[1]] = p.Figure + score := p.minimaxRecursive( + boardCopy, depth+1, false, maxDepthLimit, + ) + bestScore = max(bestScore, score) + } + return bestScore + } else { + bestScore := 100000 + // opponentFigure уже определен выше + for _, cell := range emptyCells { + boardCopy := p.copyBoard(board) + boardCopy.Board[cell[0]][cell[1]] = opponentFigure + score := p.minimaxRecursive( + boardCopy, depth+1, true, maxDepthLimit, + ) + bestScore = min(bestScore, score) + } + return bestScore + } +} diff --git a/part_8/tic_tac_toe_v8/player/strategy_zonebased.go b/part_8/tic_tac_toe_v8/player/strategy_zonebased.go new file mode 100644 index 0000000..e65b8a0 --- /dev/null +++ b/part_8/tic_tac_toe_v8/player/strategy_zonebased.go @@ -0,0 +1,121 @@ +package player + +import ( + b "tic-tac-toe/board" +) + +// Параллельный анализ на основе зон +func (p *ComputerPlayer) makeZoneBasedMove(board *b.Board) (int, int) { + // Если доска не очень большая, используем эвристику + if board.Size <= 5 { // Пороговое значение, можно настроить + return p.makeParallelHeuristicMove(board) + } + + bestScore := -100000 + var bestMove []int + emptyCells := p.getEmptyCells(board) + if len(emptyCells) == 0 { + return -1, -1 + } + if len(emptyCells) == 1 { + return emptyCells[0][0], emptyCells[0][1] + } + + // Определяем размер зоны (например, 3x3) + zoneSize := 3 + if board.Size < zoneSize { + zoneSize = board.Size // Если доска меньше зоны, зона равна доске + } + + type moveResult struct { + move []int + score int + } + // Используем буферизированный канал, чтобы не блокировать горутины, + // если основная горутина не успевает обрабатывать результаты + + // Размер канала равен количеству пустых клеток, + // т.к. для каждой может быть запущена горутина + resultChan := make(chan moveResult, len(emptyCells)) + numZonesToProcess := 0 // Счетчик для корректного ожидания + + for _, cell := range emptyCells { + numZonesToProcess++ + // Запускаем горутину для каждой пустой клетки + go func(centerCell []int) { + localBestScore := -100000 + var localBestMove []int + + // Определяем границы зоны вокруг centerCell + minRow := max(0, centerCell[0]-zoneSize/2) + maxRow := min(board.Size-1, centerCell[0]+zoneSize/2) + minCol := max(0, centerCell[1]-zoneSize/2) + maxCol := min(board.Size-1, centerCell[1]+zoneSize/2) + + // Ищем ходы в зоне + foundMoveInZone := false + for r := minRow; r <= maxRow; r++ { + for c := minCol; c <= maxCol; c++ { + // Если найден пустая клетка в зоне + if board.Board[r][c] == b.Empty { + foundMoveInZone = true + boardCopy := p.copyBoard(board) + boardCopy.Board[r][c] = p.Figure + // Оцениваем ход испо + score := p.evaluateBoardHeuristic(boardCopy, p.Figure) + + // Если найден лучший ход + if score > localBestScore { + localBestScore = score + localBestMove = []int{r, c} + } + } + } + } + + // Если найден лучший ход в зоне + if foundMoveInZone && localBestMove != nil { + resultChan <- moveResult{ + move: localBestMove, score: localBestScore, + } + } else if !foundMoveInZone && + board.Board[centerCell[0]][centerCell[1]] == b.Empty { + // Если зона вокруг centerCell не содержит других + // пустых клеток, но сама centerCell пуста – + // оцениваем ход в centerCell + boardCopy := p.copyBoard(board) + boardCopy.Board[centerCell[0]][centerCell[1]] = p.Figure + score := p.evaluateBoardHeuristic(boardCopy, p.Figure) + resultChan <- moveResult{move: centerCell, score: score} + } else { + // Если не найдено ходов в зоне или centerCell не пуста + // (не должно случиться, если итерируем по emptyCells), + // отправляем фиктивный результат, + // чтобы не блокировать ожидание. + // Этого не должно происходить в нормальном потоке. + resultChan <- moveResult{move: nil, score: -200000} + } + }(cell) + } + + // Ожидаем завершения всех горутин + processedGoroutines := 0 // Счетчик для корректного ожидания + for processedGoroutines < numZonesToProcess { + result := <-resultChan + processedGoroutines++ + // Если найден лучший ход + if result.move != nil && result.score > bestScore { + bestScore = result.score + bestMove = result.move + } + } + + if bestMove == nil { + // Если по какой-то причине лучший ход не найден (маловероятно) + // переходи на стратегию поведения среднего уровня сложности + return p.makeMediumMove(board) + } + + // Возвращаем лучший ход + return bestMove[0], bestMove[1] +} diff --git a/part_8/tic_tac_toe_v8/room/room.go b/part_8/tic_tac_toe_v8/room/room.go new file mode 100644 index 0000000..26f32eb --- /dev/null +++ b/part_8/tic_tac_toe_v8/room/room.go @@ -0,0 +1,246 @@ +package room + +import ( + "encoding/json" + "log" + "math/rand" + b "tic-tac-toe/board" + db "tic-tac-toe/database" + g "tic-tac-toe/game" + "tic-tac-toe/model" + n "tic-tac-toe/network" + p "tic-tac-toe/player" + "time" +) + +// Room manages the state of a single game room. +type Room struct { + Name string + Board *b.Board + Player1 p.IPlayer + Player2 p.IPlayer + CurrentPlayer p.IPlayer + State g.GameState + repository db.IRepository + Mode g.GameMode + // Уровень сложности компьютера (только для PvC) + Difficulty g.Difficulty +} + +// NewRoom creates a new game room. +func NewRoom( + name string, repository db.IRepository, boardSize int, + gameMode g.GameMode, difficulty g.Difficulty, +) *Room { + room := &Room{ + Name: name, + repository: repository, + Mode: gameMode, + Difficulty: difficulty, + Board: b.NewBoard(boardSize), + State: g.WaitingOpponent, + } + if gameMode == g.PvC { + room.Player2 = p.NewComputerPlayer(b.Nought, difficulty) + } + return room +} + +func (r *Room) IsFull() bool { + return r.Player1 != nil && r.Player2 != nil +} + +func (r *Room) PlayersAmount() int { + if r.Player1 != nil && r.Player2 != nil { + return 2 + } + return 1 +} + +func (r *Room) BoardSize() int { + return r.Board.Size +} + +func (r *Room) AddPlayer(player p.IPlayer) { + if r.Player1 == nil { + r.Player1 = player + if r.Player1.GetSymbol() != "X" { + r.Player1.SwitchPlayer() + } + } else if r.Player2 == nil { + r.Player2 = player + if r.Player2.GetSymbol() != "O" { + r.Player2.SwitchPlayer() + } + } +} + +func (r *Room) RemovePlayer(player p.IPlayer) { + if r.Player1 == player { + r.Player1 = nil + if !r.Player2.IsComputer() && r.Player2 != nil { + opponentLeft := &n.OpponentLeft{Nickname: player.GetNickname()} + payloadBytes, err := json.Marshal(opponentLeft) + if err != nil { + log.Printf("Error marshaling OpponentLeft: %v", err) + return + } + msg := &n.Message{ + Cmd: n.CmdOpponentLeft, + Payload: payloadBytes, + } + r.Player2.SendMessage(msg) + } + } else if r.Player2 == player { + r.Player2 = nil + if !r.Player1.IsComputer() && r.Player1 != nil { + opponentLeft := &n.OpponentLeft{Nickname: player.GetNickname()} + payloadBytes, err := json.Marshal(opponentLeft) + if err != nil { + log.Printf("Error marshaling OpponentLeft: %v", err) + return + } + msg := &n.Message{ + Cmd: n.CmdOpponentLeft, + Payload: payloadBytes, + } + r.Player1.SendMessage(msg) + } + } +} + +func (r *Room) InitGame() { + if !r.IsFull() { + return + } + + randomPlayer := []b.BoardField{b.Cross, b.Nought} + if !r.Board.IsEmpty() { + r.Board = b.NewBoard(r.Board.Size) + } + + msg := &n.Message{Cmd: n.CmdInitGame} + initGamePayload := &n.InitGameResponse{ + Board: *r.Board, + } + switch randomPlayer[rand.Intn(len(randomPlayer))] { + case b.Cross: + r.State = g.CrossStep + initGamePayload.CurrentPlayer = b.Cross + // подготовка сообщения + case b.Nought: + r.State = g.NoughtStep + initGamePayload.CurrentPlayer = b.Nought + // подготовка сообщения + } + + if r.Mode == g.PvC { + if r.State == g.CrossStep { + r.CurrentPlayer = r.Player1 + } else if r.State == g.NoughtStep { + r.CurrentPlayer = r.Player2 + } + } + + payloadBytes, err := json.Marshal(initGamePayload) + if err != nil { + log.Printf("Error marshaling InitGameResponse for Player1 after Player2 left: %v", err) + return + } + msg.Payload = payloadBytes + r.Player1.SendMessage(msg) + r.Player2.SendMessage(msg) + + if r.CurrentPlayer.IsComputer() { + row, col, _ := r.CurrentPlayer.MakeMove(r.Board) + r.PlayerStep(r.CurrentPlayer, row, col) + } +} + +// Переключаем активного игрока +func (r *Room) switchCurrentPlayer() { + if r.CurrentPlayer == r.Player1 { + r.CurrentPlayer = r.Player2 + } else { + r.CurrentPlayer = r.Player1 + } +} + +func (r *Room) PlayerStep(player p.IPlayer, row, col int) { + msg := &n.Message{} + if r.State != g.CrossStep && r.State != g.NoughtStep { + return + } + // проверяем, что ход делает текущий игрок + if player != r.CurrentPlayer { + return + } + + r.Board.SetSymbol(row, col, r.CurrentPlayer.GetFigure()) + if r.Board.CheckWin(r.CurrentPlayer.GetFigure()) { + if r.CurrentPlayer.GetFigure() == b.Cross { + r.State = g.CrossWin + } else { + r.State = g.NoughtWin + } + msg.Cmd = n.CmdEndGame + endGamePayload := &n.EndGameResponse{ + Board: *r.Board, + CurrentPlayer: r.CurrentPlayer.GetFigure(), + } + msg.Payload, _ = json.Marshal(endGamePayload) + + figureWinner := r.CurrentPlayer.GetFigure() + winnerNickName := r.CurrentPlayer.GetNickname() + + if r.CurrentPlayer == r.Player1 { + r.CurrentPlayer = r.Player2 + } else { + r.CurrentPlayer = r.Player1 + } + + anotherPlayerNickName := r.CurrentPlayer.GetNickname() + r.repository.SaveFinishedGame(&model.FinishGameSnapshot{ + Board: r.Board, + PlayerFigure: figureWinner, + WinnerName: winnerNickName, + AnotherPlayerName: anotherPlayerNickName, + Time: time.Now(), + }) + } else if r.Board.CheckDraw() { + r.State = g.Draw + msg.Cmd = n.CmdEndGame + endGamePayload := &n.EndGameResponse{ + Board: *r.Board, + CurrentPlayer: b.Empty, + } + msg.Payload, _ = json.Marshal(endGamePayload) + } else { + if r.CurrentPlayer.GetFigure() == b.Cross { + r.State = g.NoughtStep + } else { + r.State = g.CrossStep + } + r.switchCurrentPlayer() + msg.Cmd = n.CmdUpdateState + stateUpdatePayload := &n.GameStateUpdate{ + Board: *r.Board, + CurrentPlayer: r.CurrentPlayer.GetFigure(), + } + msg.Payload, _ = json.Marshal(stateUpdatePayload) + } + + r.Player1.SendMessage(msg) + r.Player2.SendMessage(msg) + + if r.State == g.CrossWin || r.State == g.NoughtWin || r.State == g.Draw { + time.Sleep(10 * time.Second) + r.InitGame() + return + } + + if r.CurrentPlayer.IsComputer() { + row, col, _ := r.CurrentPlayer.MakeMove(r.Board) + r.PlayerStep(r.CurrentPlayer, row, col) + } +} diff --git a/part_8/tic_tac_toe_v8/server/client_message_handlers.go b/part_8/tic_tac_toe_v8/server/client_message_handlers.go new file mode 100644 index 0000000..298b504 --- /dev/null +++ b/part_8/tic_tac_toe_v8/server/client_message_handlers.go @@ -0,0 +1,197 @@ +package server + +import ( + "encoding/json" + "log" + "net" + "strconv" + "tic-tac-toe/network" + p "tic-tac-toe/player" +) + +var defaultPlayerCounts int = 0 + +func (s *Server) handleCommand(client net.Conn, msg *network.Message) { + log.Printf( + "Received command '%s' from %s", + msg.Cmd, client.RemoteAddr(), + ) + + switch msg.Cmd { + case network.CmdNickname: + s.nickNameHandler(client, msg) + case network.CmdMakeMoveRequest: + s.makeMoveHandler(client, msg) + case network.CmdListRoomsRequest: + s.listRoomsHandler(client, msg) + case network.CmdJoinRoomRequest: + s.joinRoomHandler(client, msg) + case network.CmdLeaveRoomRequest: + s.leaveRoomHandler(client, msg) + case network.CmdFinishedGamesResponse: + s.getFinishedGamesHandler(client, msg) + case network.CmdFinishedGameByIdRequest: + s.getFinishedGameByIdHandler(client, msg) + default: + log.Printf("Unknown command: %s", msg.Cmd) + } +} + +func (s *Server) nickNameHandler(client net.Conn, msg *network.Message) { + nicknameRequest := &network.NicknameRequest{} + if err := json.Unmarshal(msg.Payload, nicknameRequest); err != nil { + log.Printf("Error unmarshaling NicknameRequest: %v", err) + return + } + if s.players[nicknameRequest.Nickname] != nil { + nicknameRequest.Nickname = nicknameRequest.Nickname + + "_" + strconv.Itoa(defaultPlayerCounts) + defaultPlayerCounts++ + } + s.players[nicknameRequest.Nickname] = p.NewHumanPlayer( + nicknameRequest.Nickname, &client, + ) + response := &network.NickNameResponse{ + Nickname: nicknameRequest.Nickname, + } + msg.Payload, _ = json.Marshal(response) + msg.Cmd = network.CmdNickNameResponse + json.NewEncoder(client).Encode(msg) +} + +func (s *Server) joinRoomHandler(client net.Conn, msg *network.Message) { + joinRoomRequest := &network.JoinRoomRequest{} + if err := json.Unmarshal(msg.Payload, joinRoomRequest); err != nil { + log.Printf("Error unmarshaling JoinRoomRequest: %v", err) + return + } + room, okRoom := s.rooms[joinRoomRequest.RoomName] + player, okPlayer := s.players[joinRoomRequest.PlayerName] + if !okRoom || !okPlayer { + response := &network.ErrorResponse{Message: "Room not found"} + msg.Cmd = network.CmdError + msg.Payload, _ = json.Marshal(response) + json.NewEncoder(client).Encode(msg) + return + } + s.mutex.Lock() + if room.IsFull() { + s.mutex.Unlock() + response := &network.ErrorResponse{Message: "Room is full"} + msg.Cmd = network.CmdError + msg.Payload, _ = json.Marshal(response) + json.NewEncoder(client).Encode(msg) + return + } + room.AddPlayer(player) + s.mutex.Unlock() + response := &network.RoomJoinResponse{ + RoomName: joinRoomRequest.RoomName, + PlayerSymbol: player.GetFigure(), + Board: *room.Board, + } + msg.Payload, _ = json.Marshal(response) + msg.Cmd = network.CmdRoomJoinResponse + json.NewEncoder(client).Encode(msg) + room.InitGame() +} + +func (s *Server) leaveRoomHandler(client net.Conn, msg *network.Message) { + leaveRoomRequest := &network.LeaveRoomRequest{} + if err := json.Unmarshal(msg.Payload, leaveRoomRequest); err != nil { + log.Printf("Error unmarshaling LeaveRoomRequest: %v", err) + return + } + room, okRoom := s.rooms[leaveRoomRequest.RoomName] + player, okPlayer := s.players[leaveRoomRequest.PlayerName] + if !okRoom || !okPlayer { + response := &network.ErrorResponse{Message: "Room not found"} + msg.Cmd = network.CmdError + msg.Payload, _ = json.Marshal(response) + json.NewEncoder(client).Encode(msg) + return + } + s.mutex.Lock() + room.RemovePlayer(player) + s.mutex.Unlock() +} + +func (s *Server) listRoomsHandler(client net.Conn, msg *network.Message) { + s.mutex.Lock() + defer s.mutex.Unlock() + var roomInfos []network.RoomInfo + for _, room := range s.rooms { + roomInfos = append(roomInfos, network.RoomInfo{ + Name: room.Name, + BoardSize: room.BoardSize(), + IsFull: room.IsFull(), + GameMode: room.Mode, + Difficult: room.Difficulty, + }) + } + + response := &network.RoomListResponse{ + Rooms: roomInfos, + } + msg.Cmd = network.CmdRoomListResponse + msg.Payload, _ = json.Marshal(response) + json.NewEncoder(client).Encode(msg) +} + +func (s *Server) getFinishedGamesHandler(client net.Conn, msg *network.Message) { + // получаем данные из БД + finishedGames, err := s.repository.GetAllFinishedGames() + if err != nil { + response := &network.ErrorResponse{Message: "Error getting finished games"} + msg.Cmd = network.CmdError + msg.Payload, _ = json.Marshal(response) + json.NewEncoder(client).Encode(msg) + return + } + response := &network.FinishedGamesResponse{ + Games: finishedGames, + } + msg.Cmd = network.CmdFinishedGamesResponse + msg.Payload, _ = json.Marshal(response) + json.NewEncoder(client).Encode(msg) +} + +func (s *Server) getFinishedGameByIdHandler(client net.Conn, msg *network.Message) { + getFinishedGameByIdRequest := &network.GetFinishedGameByIdRequest{} + if err := json.Unmarshal(msg.Payload, getFinishedGameByIdRequest); err != nil { + log.Printf("Error unmarshaling GetFinishedGameByIdRequest: %v", err) + return + } + finishedGame, err := s.repository.GetFinishedGameById(getFinishedGameByIdRequest.GameID) + if err != nil { + response := &network.ErrorResponse{Message: "Error getting finished game by id"} + json.NewEncoder(client).Encode(response) + return + } + response := &network.FinishedGameResponse{ + Game: finishedGame, + } + json.NewEncoder(client).Encode(response) +} + +func (s *Server) makeMoveHandler(client net.Conn, msg *network.Message) { + makeMoveRequest := &network.MakeMoveRequest{} + if err := json.Unmarshal(msg.Payload, makeMoveRequest); err != nil { + log.Printf("Error unmarshaling MakeMoveRequest: %v", err) + return + } + room, okRoom := s.rooms[makeMoveRequest.RoomName] + player, okPlayer := s.players[makeMoveRequest.PlayerName] + if !okRoom || !okPlayer { + response := &network.ErrorResponse{Message: "Room not found"} + msg.Cmd = network.CmdError + msg.Payload, _ = json.Marshal(response) + json.NewEncoder(client).Encode(msg) + return + } + room.PlayerStep( + player, + makeMoveRequest.PositionRow, + makeMoveRequest.PositionCol, + ) +} diff --git a/part_8/tic_tac_toe_v8/server/server.go b/part_8/tic_tac_toe_v8/server/server.go new file mode 100644 index 0000000..e5f3a4d --- /dev/null +++ b/part_8/tic_tac_toe_v8/server/server.go @@ -0,0 +1,116 @@ +package server + +import ( + "encoding/json" + "log" + "net" + "sync" + + db "tic-tac-toe/database" + g "tic-tac-toe/game" + "tic-tac-toe/network" + "tic-tac-toe/player" + "tic-tac-toe/room" +) + +// Server manages client connections and game rooms. +type Server struct { + listener net.Listener + repository db.IRepository + rooms map[string]*room.Room + players map[string]player.IPlayer + mutex sync.RWMutex +} + +// NewServer creates and returns a new server instance. +func NewServer(addr string, repository db.IRepository) (*Server, error) { + listener, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + server := &Server{ + listener: listener, + repository: repository, + rooms: make(map[string]*room.Room), + players: make(map[string]player.IPlayer), + } + + server.rooms["room1"] = room.NewRoom( + "room1", server.repository, 3, g.PvP, g.None, + ) + server.rooms["room2"] = room.NewRoom( + "room2", server.repository, 3, g.PvC, g.Easy, + ) + server.rooms["room3"] = room.NewRoom( + "room3", server.repository, 3, g.PvC, g.Medium, + ) + server.rooms["room4"] = room.NewRoom( + "room4", server.repository, 3, g.PvC, g.Hard, + ) + + return server, nil +} + +// Start begins listening for and handling client connections. +func (s *Server) Start() { + log.Printf("Server started, listening on %s", s.listener.Addr()) + defer s.listener.Close() + + for { + conn, err := s.listener.Accept() + if err != nil { + log.Printf("Error accepting connection: %v", err) + continue + } + + go s.handleConnection(conn) + } +} + +// handleConnection manages a single client connection. +func (s *Server) handleConnection(conn net.Conn) { + log.Printf("New client connected: %s", conn.RemoteAddr()) + defer conn.Close() + + decoder := json.NewDecoder(conn) + for { + var msg network.Message + if err := decoder.Decode(&msg); err != nil { + log.Printf("Client %s disconnected: %v", conn.RemoteAddr(), err) + s.disconnectedClientHandler(conn) + return + } + + s.handleCommand(conn, &msg) + } +} + +func (s *Server) disconnectedClientHandler(conn net.Conn) { + var player player.IPlayer + for _, room := range s.rooms { + if room.Player1 != nil { + if room.Player1.CheckSocket(conn) { + player = room.Player1 + room.RemovePlayer(room.Player1) + break + } + } + if room.Player2 != nil { + if room.Player2.CheckSocket(conn) { + player = room.Player2 + room.RemovePlayer(room.Player2) + break + } + } + } + if player == nil { + log.Printf( + "Client %s disconnected: player not found", + conn.RemoteAddr(), + ) + return + } + s.mutex.Lock() + delete(s.players, player.GetNickname()) + s.mutex.Unlock() +} diff --git a/part_8/tic_tac_toe_v8/tic_tac_toe.db b/part_8/tic_tac_toe_v8/tic_tac_toe.db new file mode 100644 index 0000000..b942bff Binary files /dev/null and b/part_8/tic_tac_toe_v8/tic_tac_toe.db differ