diff --git a/part_6/tic_tac_toe/game/game.go b/part_6/tic_tac_toe/game/game.go deleted file mode 100644 index 4934b45..0000000 --- a/part_6/tic_tac_toe/game/game.go +++ /dev/null @@ -1,122 +0,0 @@ -package game - -import ( - "bufio" - "fmt" - "strconv" - "strings" -) - -type Game struct { - Board *Board `json:"board"` - Player *Player `json:"player"` - Reader *bufio.Reader `json:"-"` - State GameState `json:"state"` - Saver IGameSaver `json:"-"` -} - -func NewGame(board Board, player Player, - reader *bufio.Reader, saver IGameSaver) *Game { - return &Game{ - Board: &board, - Player: &player, - Reader: reader, - State: playing, - Saver: saver, - } -} - -func (g *Game) updateState() { - if g.Board.checkWin(g.Player.Figure) { - if g.Player.Figure == cross { - g.State = crossWin - } else { - g.State = noughtWin - } - } else if g.Board.checkDraw() { - g.State = draw - } -} - -func (g *Game) saveCheck(input string) bool { - if input == "save" { - fmt.Println("Enter file name: ") - fileName, err := g.Reader.ReadString('\n') - if err != nil { - fmt.Println("Invalid input. Please try again.") - return false - } - - fileName = strings.TrimSpace(fileName) - err = g.Saver.SaveGame(fileName, g) - if err != nil { - fmt.Println("Error saving game.") - return false - } - fmt.Println("Game saved successfully!!!") - return true - } - - return false -} - -// Игровой цикл -func (g *Game) Play() { - for g.State == playing { - g.Board.printBoard() - fmt.Printf( - "%s's turn. Enter row and column (e.g. 1 2): ", - g.Player.getSymbol()) - - input, err := g.Reader.ReadString('\n') - input = strings.TrimSpace(input) - if err != nil { - fmt.Println("Invalid input. Please try again.") - continue - } - - input = strings.TrimSpace(input) - if input == "q" { - g.State = quit - break - } - - if g.saveCheck(input) { - continue - } - - parts := strings.Fields(input) - if len(parts) != 2 { - fmt.Println("Invalid input. Please try again.") - continue - } - - row, err1 := strconv.Atoi(parts[0]) - col, err2 := strconv.Atoi(parts[1]) - if err1 != nil || err2 != nil || - row < 1 || col < 1 || row > g.Board.Size || - col > g.Board.Size { - fmt.Println("Invalid input. Please try again.") - continue - } - if g.Board.setSymbol(row-1, col-1, g.Player.Figure) { - g.updateState() - g.Player.switchPlayer() - - } else { - fmt.Println("This cell is already occupied!") - } - } - - g.Board.printBoard() - - if g.State == crossWin { - fmt.Println("X wins!") - } else if g.State == noughtWin { - fmt.Println("O wins!") - } else if g.State == draw { - fmt.Println("It's a draw!") - } else { - fmt.Println("Game over!") - } -} diff --git a/part_6/tic_tac_toe/game/i_game_loader.go b/part_6/tic_tac_toe/game/i_game_loader.go deleted file mode 100644 index c598d13..0000000 --- a/part_6/tic_tac_toe/game/i_game_loader.go +++ /dev/null @@ -1,9 +0,0 @@ -package game - -type IGameLoader interface { - LoadGame(path string) (*Game, error) -} - -type IGameSaver interface { - SaveGame(path string, game *Game) error -} diff --git a/part_6/tic_tac_toe/game/json_game_loader.go b/part_6/tic_tac_toe/game/json_game_loader.go deleted file mode 100644 index 8778c6a..0000000 --- a/part_6/tic_tac_toe/game/json_game_loader.go +++ /dev/null @@ -1,50 +0,0 @@ -package game - -import ( - "encoding/json" - "os" - "strings" -) - -func NewJsonGameLoader() IGameLoader { - return &JsonGameLoader{} -} - -type JsonGameLoader struct{} - -func (j *JsonGameLoader) LoadGame(path string) (*Game, error) { - if !strings.HasSuffix(path, ".json") { - path += ".json" - } - file, err := os.Open(path) - if err != nil { - return nil, err - } - defer file.Close() - - decoder := json.NewDecoder(file) - var game Game - err = decoder.Decode(&game) - if err != nil { - return nil, err - } - return &game, nil -} - -func (j *JsonGameLoader) SaveGame(path string, game *Game) error { - if !strings.HasSuffix(path, ".json") { - path += ".json" - } - file, err := os.Create(path) - if err != nil { - return err - } - defer file.Close() - - encoder := json.NewEncoder(file) - err = encoder.Encode(game) - if err != nil { - return err - } - return nil -} diff --git a/part_6/tic_tac_toe/game/player.go b/part_6/tic_tac_toe/game/player.go deleted file mode 100644 index 2441444..0000000 --- a/part_6/tic_tac_toe/game/player.go +++ /dev/null @@ -1,24 +0,0 @@ -package game - -type Player struct { - Figure BoardField `json:"figure"` -} - -func NewPlayer() *Player { - return &Player{Figure: cross} -} - -func (p *Player) switchPlayer() { - if p.Figure == cross { - p.Figure = nought - } else { - p.Figure = cross - } -} - -func (p *Player) getSymbol() string { - if p.Figure == cross { - return "X" - } - return "O" -} diff --git a/part_6/tic_tac_toe/go.mod b/part_6/tic_tac_toe/go.mod deleted file mode 100644 index ced943a..0000000 --- a/part_6/tic_tac_toe/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module tic-tac-toe - -go 1.24.0 diff --git a/part_6/tic_tac_toe/main.go b/part_6/tic_tac_toe/main.go deleted file mode 100644 index 5d7de46..0000000 --- a/part_6/tic_tac_toe/main.go +++ /dev/null @@ -1,76 +0,0 @@ -package main - -import ( - "bufio" - "fmt" - "os" - "strconv" - "strings" - "tic-tac-toe/game" -) - -func main() { - reader := bufio.NewReader(os.Stdin) - loader := game.NewJsonGameLoader() - boardSize := 0 - - for { - fmt.Println("1 - load game") - fmt.Println("2 - new game") - fmt.Println("q - quit") - - input, _ := reader.ReadString('\n') - input = strings.TrimSpace(input) - switch input { - case "1": - var loadedGame *game.Game - var err error - for { - fmt.Println("Enter file name: ") - fileName, _ := reader.ReadString('\n') - fileName = strings.TrimSpace(fileName) - loadedGame, err = loader.LoadGame(fileName) - if err != nil { - fmt.Println("Error loading game.") - continue - } - break - } - loadedGame.Reader = reader - loadedGame.Saver = loader.(game.IGameSaver) - loadedGame.Play() - case "2": - for { - fmt.Print("Enter the size of the board (3-9): ") - input, err := reader.ReadString('\n') - if err != nil { - fmt.Println("Error reading input.") - continue - } - input = strings.TrimSpace(input) - boardSize, err = strconv.Atoi(input) - if err != nil { - // Использовать предыдущий размер по умолчанию - boardSize = game.BoardDefaultSize - } - if boardSize < game.BoardMinSize || - boardSize > game.BoardMaxSize { - fmt.Println("Invalid board size.") - } else { - break - } - } - - board := game.NewBoard(boardSize) - player := game.NewPlayer() - game := game.NewGame(*board, *player, reader, - loader.(game.IGameSaver)) - game.Play() - case "q": - return - default: - fmt.Println("Invalid input. Please try again.") - return - } - } -} diff --git a/part_6/tic_tac_toe/myGame.json b/part_6/tic_tac_toe/myGame.json deleted file mode 100644 index 8b2b247..0000000 --- a/part_6/tic_tac_toe/myGame.json +++ /dev/null @@ -1 +0,0 @@ -{"board":{"board":[[0,2,0,0],[0,0,1,0],[0,0,0,0],[0,0,1,0]],"size":4},"player":{"figure":2},"state":0} diff --git a/part_6/tic_tac_toe/.vscode/launch.json b/part_6/tic_tac_toe_v5/.vscode/launch.json similarity index 100% rename from part_6/tic_tac_toe/.vscode/launch.json rename to part_6/tic_tac_toe_v5/.vscode/launch.json diff --git a/part_6/tic_tac_toe/game/board.go b/part_6/tic_tac_toe_v5/board/board.go similarity index 85% rename from part_6/tic_tac_toe/game/board.go rename to part_6/tic_tac_toe_v5/board/board.go index ff0290a..f32c64e 100644 --- a/part_6/tic_tac_toe/game/board.go +++ b/part_6/tic_tac_toe_v5/board/board.go @@ -1,4 +1,4 @@ -package game +package board import ( "fmt" @@ -24,7 +24,7 @@ func NewBoard(size int) *Board { } // Отображение игрового поля -func (b *Board) printBoard() { +func (b *Board) PrintBoard() { fmt.Print(" ") for i := range b.Size { fmt.Printf("%d ", i+1) @@ -34,11 +34,11 @@ func (b *Board) printBoard() { fmt.Printf("%d ", i+1) for j := range b.Size { switch b.Board[i][j] { - case empty: + case Empty: fmt.Print(". ") - case cross: + case Cross: fmt.Print("X ") - case nought: + case Nought: fmt.Print("O ") } } @@ -48,10 +48,10 @@ func (b *Board) printBoard() { // Проверка возможности и выполнения хода func (b *Board) makeMove(x, y int) bool { - return b.Board[x][y] == empty + return b.Board[x][y] == Empty } -func (b *Board) setSymbol(x, y int, player BoardField) bool { +func (b *Board) SetSymbol(x, y int, player BoardField) bool { if b.makeMove(x, y) { b.Board[x][y] = player return true @@ -60,7 +60,7 @@ func (b *Board) setSymbol(x, y int, player BoardField) bool { } // Проверка выигрыша -func (b *Board) checkWin(player BoardField) bool { +func (b *Board) CheckWin(player BoardField) bool { // Проверка строк и столбцов for i := range b.Size { rowWin, colWin := true, true @@ -101,10 +101,10 @@ func (b *Board) checkWin(player BoardField) bool { } // Проверка на ничью -func (b *Board) checkDraw() bool { +func (b *Board) CheckDraw() bool { for i := range b.Size { for j := range b.Size { - if b.Board[i][j] == empty { + if b.Board[i][j] == Empty { return false } } diff --git a/part_6/tic_tac_toe/game/board_cell_type.go b/part_6/tic_tac_toe_v5/board/board_cell_type.go similarity index 57% rename from part_6/tic_tac_toe/game/board_cell_type.go rename to part_6/tic_tac_toe_v5/board/board_cell_type.go index 6763716..1beb472 100644 --- a/part_6/tic_tac_toe/game/board_cell_type.go +++ b/part_6/tic_tac_toe_v5/board/board_cell_type.go @@ -1,10 +1,10 @@ -package game +package board type BoardField int // фигуры в клетке поля const ( - empty BoardField = iota - cross - nought + Empty BoardField = iota + Cross + Nought ) diff --git a/part_6/tic_tac_toe_v5/database/crud.go b/part_6/tic_tac_toe_v5/database/crud.go new file mode 100644 index 0000000..1dee778 --- /dev/null +++ b/part_6/tic_tac_toe_v5/database/crud.go @@ -0,0 +1,127 @@ +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) SaveSnapshot( + snapshot *m.GameSnapshot, + playerNickName string, +) error { + player, _ := r.GetPlayer(playerNickName) + if player == nil { + player, _ = r.CreatePlayer(playerNickName) + } + + boardJSON, err := json.Marshal(snapshot.Board) + if err != nil { + return err + } + + return r.db.Create(&GameSnapshot{ + BoardJSON: boardJSON, + PlayerFigure: int(snapshot.PlayerFigure), + State: int(snapshot.State), + Mode: int(snapshot.Mode), + Difficulty: int(snapshot.Difficulty), + IsCurrentFirst: snapshot.IsCurrentFirst, + PlayerNickName: player.NickName, + }).Error +} + +func (r *SQLiteRepository) GetSnapshots( + nickName string) (*[]m.GameSnapshot, error) { + var snapshots []GameSnapshot + // ищем игрока по никнейму + player, err := r.GetPlayer(nickName) + if err != nil { + return nil, err + } + + // находим все снапшоты игрока + if err := r.db.Where( + "player_nick_name = ?", player.NickName, + ).Find(&snapshots).Error; err != nil { + return nil, err + } + + var gameSnapshots []m.GameSnapshot + for _, snapshot := range snapshots { + temp, err := snapshot.ToModel() + if err != nil { + return nil, err + } + gameSnapshots = append(gameSnapshots, *temp) + } + return &gameSnapshots, nil +} + +func (r *SQLiteRepository) IsSnapshotExist(snapshotName string, nickName string) (bool, error) { + var snapshot GameSnapshot + if err := r.db.Where( + "snapshot_name = ? AND player_nick_name = ?", snapshotName, nickName, + ).First(&snapshot).Error; err != nil { + return false, err + } + return true, nil +} + +func (r *SQLiteRepository) SaveFinishedGame( + snapshot *m.FinishGameSnapshot) error { + boardJSON, err := json.Marshal(snapshot.Board) + if err != nil { + return err + } + + player, _ := r.GetPlayer(snapshot.PlayerNickName) + if player == nil { + player, _ = r.CreatePlayer(snapshot.PlayerNickName) + } + + return r.db.Create(&PlayerFinishGame{ + BoardJSON: boardJSON, + PlayerFigure: int(snapshot.PlayerFigure), + WinnerName: snapshot.WinnerName, + PlayerNickName: player.NickName, + Time: snapshot.Time, + }).Error +} + +func (r *SQLiteRepository) GetFinishedGames(nickName string) (*[]m.FinishGameSnapshot, error) { + var playerFinishGames []PlayerFinishGame + if err := r.db.Where( + "player_nick_name = ?", nickName, + ).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 +} diff --git a/part_6/tic_tac_toe_v5/database/database.go b/part_6/tic_tac_toe_v5/database/database.go new file mode 100644 index 0000000..16af663 --- /dev/null +++ b/part_6/tic_tac_toe_v5/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{}, &GameSnapshot{}, &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_6/tic_tac_toe_v5/database/db_definition.go b/part_6/tic_tac_toe_v5/database/db_definition.go new file mode 100644 index 0000000..475aea3 --- /dev/null +++ b/part_6/tic_tac_toe_v5/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_6/tic_tac_toe_v5/database/i_repository.go b/part_6/tic_tac_toe_v5/database/i_repository.go new file mode 100644 index 0000000..e791b8a --- /dev/null +++ b/part_6/tic_tac_toe_v5/database/i_repository.go @@ -0,0 +1,13 @@ +package database + +import "tic-tac-toe/model" + +type IRepository interface { + CreatePlayer(nickName string) (*Player, error) + GetPlayer(nickName string) (*Player, error) + SaveSnapshot(snapshot *model.GameSnapshot, playerNickName string) error + GetSnapshots(nickName string) (*[]model.GameSnapshot, error) + IsSnapshotExist(snapshotName string, nickName string) (bool, error) + SaveFinishedGame(snapshot *model.FinishGameSnapshot) error + GetFinishedGames(nickName string) (*[]model.FinishGameSnapshot, error) +} diff --git a/part_6/tic_tac_toe_v5/database/models.go b/part_6/tic_tac_toe_v5/database/models.go new file mode 100644 index 0000000..a0f9877 --- /dev/null +++ b/part_6/tic_tac_toe_v5/database/models.go @@ -0,0 +1,31 @@ +package database + +import "time" + +type Player struct { + NickName string `gorm:"primary_key;not null"` +} + +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"` +} + +// GameSnapshot представляет модель для хранения снапшота игры в БД +type GameSnapshot struct { + ID int `gorm:"primaryKey;autoIncrement;not null"` + SnapshotName string `gorm:"not null"` + BoardJSON []byte `gorm:"type:json;not null"` + PlayerFigure int `gorm:"not null"` + 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 new file mode 100644 index 0000000..ecdd608 --- /dev/null +++ b/part_6/tic_tac_toe_v5/database/utils.go @@ -0,0 +1,54 @@ +package database + +import ( + "encoding/json" + + b "tic-tac-toe/board" + m "tic-tac-toe/model" + p "tic-tac-toe/player" +) + +func (p *Player) TableName() string { + return "players" +} + +func (pfg *PlayerFinishGame) TableName() string { + return "player_finish_games" +} + +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{ + Board: &board, + PlayerFigure: b.BoardField(f.PlayerFigure), + WinnerName: f.WinnerName, + PlayerNickName: f.PlayerNickName, + Time: f.Time, + }, nil +} + +func (g *GameSnapshot) TableName() string { + return "game_snapshots" +} + +func (gs *GameSnapshot) ToModel() (*m.GameSnapshot, error) { + // Десериализуем BoardJSON в структуру Board + var board b.Board + if err := json.Unmarshal(gs.BoardJSON, &board); err != nil { + return nil, err + } + + 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, + }, 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 new file mode 100644 index 0000000..a98b1c4 --- /dev/null +++ b/part_6/tic_tac_toe_v5/game/game_core.go @@ -0,0 +1,80 @@ +package game + +import ( + "bufio" + b "tic-tac-toe/board" + db "tic-tac-toe/database" + p "tic-tac-toe/player" +) + +type Game struct { + Board *b.Board `json:"board"` + Player p.IPlayer `json:"player"` + // Не сериализуется напрямую + Player2 p.IPlayer `json:"-"` + // Не сериализуется напрямую + CurrentPlayer p.IPlayer `json:"-"` + Reader *bufio.Reader `json:"-"` + State GameState `json:"state"` + repository db.IRepository `json:"-"` + // Режим игры (PvP или PvC) + Mode GameMode `json:"mode"` + // Уровень сложности компьютера (только для PvC) + Difficulty p.Difficulty `json:"difficulty,omitempty"` + // Флаг для определения текущего игрока + IsCurrentFirst bool `json:"is_current_first"` +} + +// Создаем новую игру +func NewGame(board b.Board, reader *bufio.Reader, repository db.IRepository, + mode GameMode, difficulty p.Difficulty) *Game { + // Создаем первого игрока (всегда человек на X) + player1 := p.NewHumanPlayer(b.Cross, reader) + + var player2 p.IPlayer + if mode == PlayerVsPlayer { + // Для режима игрок против игрока создаем второго человека-игрока + player2 = p.NewHumanPlayer(b.Nought, reader) + } else { + // Для режима игрок против компьютера создаем компьютерного игрока + player2 = p.NewComputerPlayer(b.Nought, difficulty) + } + + return &Game{ + Board: &board, + Player: player1, + Player2: player2, + CurrentPlayer: player1, + Reader: reader, + State: playing, + repository: repository, + Mode: mode, + Difficulty: difficulty, + IsCurrentFirst: true, + } +} + +// Переключаем активного игрока +func (g *Game) switchCurrentPlayer() { + if g.CurrentPlayer == g.Player { + g.CurrentPlayer = g.Player2 + } else { + g.CurrentPlayer = g.Player + } +} + +// Обновляем состояние игры +func (g *Game) updateState() { + if g.Board.CheckWin(g.CurrentPlayer.GetFigure()) { + if g.CurrentPlayer.GetFigure() == b.Cross { + g.State = crossWin + } else { + g.State = noughtWin + } + return + } + + if g.Board.CheckDraw() { + g.State = draw + } +} diff --git a/part_6/tic_tac_toe_v5/game/game_play.go b/part_6/tic_tac_toe_v5/game/game_play.go new file mode 100644 index 0000000..1bdcb89 --- /dev/null +++ b/part_6/tic_tac_toe_v5/game/game_play.go @@ -0,0 +1,170 @@ +package game + +import ( + "fmt" + "strings" + "time" + + "tic-tac-toe/model" + p "tic-tac-toe/player" +) + +func (g *Game) Play() bool { + fmt.Println("For saving the game enter: save filename") + fmt.Println("For exiting the game enter : q") + fmt.Println("For making a move enter: row col") + + for g.State == playing { + g.Board.PrintBoard() + + // Определяем, кто делает ход: человек или компьютер + if g.Mode == PlayerVsComputer && g.CurrentPlayer == g.Player2 { + // Если ход компьютера, просто вызываем его MakeMove + fmt.Println("Computer is making a move...") + row, col, _ := g.CurrentPlayer.MakeMove(g.Board) + + // Применяем ход компьютера к доске + g.Board.SetSymbol(row, col, g.CurrentPlayer.GetFigure()) + } else { + fmt.Printf( + "%s's turn. Enter row and column (e.g. 1 2): ", + g.CurrentPlayer.GetSymbol(), + ) + + // Читаем ввод пользователя + input, _ := g.Reader.ReadString('\n') + input = strings.TrimSpace(input) + + // Проверка выхода из игры + if input == "q" { + g.State = quit + break + } + + // Проверка и выполнение сохранения игры + if g.saveCheck(input) { + continue + } + + // Получаем ход человека-игрока через парсинг ввода + hPlayer, ok := g.CurrentPlayer.(*p.HumanPlayer) + if !ok { + fmt.Println("Invalide data. Please try again!") + continue + } + + // Парсим ввод и получаем координаты хода + row, col, validMove := hPlayer.ParseMove(input, g.Board) + if !validMove { + fmt.Println("Invalide data. Please try again!") + continue + } + + // Устанавливаем символ на доску + if !g.Board.SetSymbol(row, col, hPlayer.GetFigure()) { + fmt.Println("This cell is already occupied!") + continue + } + } + + // Обновляем состояние игры + g.updateState() + + // Если игра продолжается, меняем игрока + if g.State == playing { + g.switchCurrentPlayer() + } + } + + // Печатаем итоговую доску и результат + g.Board.PrintBoard() + fmt.Println() + + var winner string + switch g.State { + case crossWin: + winner = "X" + fmt.Println("X wins!") + case noughtWin: + fmt.Println("O wins!") + winner = "O" + case draw: + fmt.Println("It's a draw!") + winner = "Draw" + } + + g.saveFinishedGame(winner) + // Возвращаем true, если игра закончилась нормально (не выходом) + return g.State != quit +} + +// Сохраняем результат завершенной игры +func (g *Game) saveFinishedGame(winner string) { + // Запрашиваем ник игрока + fmt.Print("Enter your nickname to save the game result: ") + nickName, _ := g.Reader.ReadString('\n') + nickName = strings.TrimSpace(nickName) + + if nickName == "" { + fmt.Println("Nickname is empty, game result not saved.") + return + } + + // Определяем победителя + + // Создаем снапшот + finishSnapshot := &model.FinishGameSnapshot{ + Board: g.Board, + PlayerFigure: g.CurrentPlayer.GetFigure(), + WinnerName: winner, + PlayerNickName: nickName, + Time: time.Now(), + } + + // Сохраняем в базу данных + if err := g.repository.SaveFinishedGame(finishSnapshot); err != nil { + fmt.Printf("Error saving game result: %v\n", err) + } +} + +// Проверяем, являются ли введенные данные командой на сохранение +func (g *Game) saveCheck(input string) bool { + // Проверяем, если пользователь ввел только "save" без имени файла + if input == "save" { + fmt.Println("Error: missing filename. " + + "Please use the format: save filename") + return false + } + + // Проверяем команду сохранения с именем файла + if len(input) > 5 && input[:5] == "save " { + filename := input[5:] + + // Проверяем, что имя файла не пустое + if len(strings.TrimSpace(filename)) == 0 { + fmt.Println("Error: empty file name. " + + "Please use the format: save filename") + return false + } + + fmt.Print("Enter nickname: ") + nickName, _ := g.Reader.ReadString('\n') + nickName = strings.TrimSpace(nickName) + + exist, _ := g.repository.IsSnapshotExist(filename, nickName) + if exist { + fmt.Println("Snapshot already exists. Please choose another name.") + return false + } + + shapshot := g.gameSnapshot() + shapshot.SnapshotName = filename + if err := g.repository.SaveSnapshot(shapshot, nickName); err != nil { + fmt.Printf("Error saving game: %v\n", err) + return false + } + fmt.Println("Game saved") + return true + } + return false +} diff --git a/part_6/tic_tac_toe_v5/game/game_serialization.go b/part_6/tic_tac_toe_v5/game/game_serialization.go new file mode 100644 index 0000000..8083478 --- /dev/null +++ b/part_6/tic_tac_toe_v5/game/game_serialization.go @@ -0,0 +1,84 @@ +package game + +import ( + "bufio" + "fmt" + b "tic-tac-toe/board" + db "tic-tac-toe/database" + m "tic-tac-toe/model" + 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, + } +} + +// Восстанавливаем игру из снапшота +func (g *Game) RestoreFromSnapshot( + snapshot *m.GameSnapshot, + reader *bufio.Reader, + repository db.IRepository, +) { + g.Board = snapshot.Board + 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} + + g.Reader = reader + g.repository = repository + + g.recreatePlayersAfterLoad(reader) +} + +// Восстанавливаем объекты игроков после загрузки из JSON +func (g *Game) recreatePlayersAfterLoad(reader *bufio.Reader) { + // Создаем игроков в зависимости от режима игры + if g.Player == nil { + fmt.Println("Error: Player is nil") + return + } + + playerFigure := g.Player.GetFigure() + g.Player = p.NewHumanPlayer(playerFigure, reader) + + // Получаем фигуру второго игрока + var player2Figure b.BoardField + if playerFigure == b.Cross { + player2Figure = b.Nought + } else { + player2Figure = b.Cross + } + + // Создаем второго игрока в зависимости от режима + if g.Mode == PlayerVsPlayer { + g.Player2 = p.NewHumanPlayer(player2Figure, reader) + } else { + g.Player2 = p.NewComputerPlayer(player2Figure, g.Difficulty) + } + + // Восстанавливаем указатель на текущего игрока + if g.IsCurrentFirst { + g.CurrentPlayer = g.Player + } else { + g.CurrentPlayer = g.Player2 + } +} diff --git a/part_6/tic_tac_toe_v5/game/game_setup.go b/part_6/tic_tac_toe_v5/game/game_setup.go new file mode 100644 index 0000000..f2deec3 --- /dev/null +++ b/part_6/tic_tac_toe_v5/game/game_setup.go @@ -0,0 +1,116 @@ +package game + +import ( + "bufio" + "fmt" + "strconv" + "strings" + b "tic-tac-toe/board" + db "tic-tac-toe/database" + p "tic-tac-toe/player" +) + +// Создаем новую игру с пользовательскими настройками +func SetupGame(reader *bufio.Reader, repository db.IRepository) *Game { + // Запрашиваем размер игрового поля + size := getBoardSize(reader) + + // Создаем доску + board := *b.NewBoard(size) + + // Запрашиваем режим игры + mode := getGameMode(reader) + + // Если выбран режим против компьютера, запрашиваем сложность + var difficulty p.Difficulty + if mode == PlayerVsComputer { + difficulty = getDifficulty(reader) + } + + // Создаем новую игру + return NewGame(board, reader, repository, mode, difficulty) +} + +// Запрашиваем у пользователя размер доски +func getBoardSize(reader *bufio.Reader) int { + size := b.BoardDefaultSize + var err error + for { + fmt.Printf("Choose board size (min: %d, max: %d, default: %d): ", + b.BoardMinSize, b.BoardMaxSize, b.BoardDefaultSize) + + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(input) + + // Если пользователь не ввел ничего, используем размер по умолчанию + if input == "" { + return b.BoardDefaultSize + } + + // Пытаемся преобразовать ввод в число + size, err = strconv.Atoi(input) + if err != nil || size < b.BoardMinSize || size > b.BoardMaxSize { + fmt.Println("Invalid input. Please try again!") + continue + } + + return size + } +} + +// Запрашиваем у пользователя режим игры +func getGameMode(reader *bufio.Reader) GameMode { + for { + fmt.Println("Choose game mode:") + fmt.Println("1 - Player vs Player (PvP)") + fmt.Println("2 - Player vs Computer (PvC)") + fmt.Print("Your choice: ") + + input, err := reader.ReadString('\n') + input = strings.TrimSpace(input) + + if err != nil { + fmt.Println("Invalid input. Please try again!") + continue + } + + switch input { + case "1": + return PlayerVsPlayer + case "2": + return PlayerVsComputer + default: + fmt.Println("Invalid input. Please try again!") + } + } +} + +// Запрашиваем у пользователя уровень сложности компьютера +func getDifficulty(reader *bufio.Reader) p.Difficulty { + for { + fmt.Println("Choose computer difficulty:") + fmt.Println("1 - Easy (random moves)") + fmt.Println("2 - Medium (block winning moves)") + fmt.Println("3 - Hard (optimal strategy)") + fmt.Print("Your choice: ") + + input, err := reader.ReadString('\n') + input = strings.TrimSpace(input) + + if err != nil { + fmt.Println("Invalid input. Please try again!") + continue + } + + switch input { + case "1": + return p.Easy + case "2": + return p.Medium + case "3": + return p.Hard + default: + fmt.Println("Invalid input. Please try again!") + } + } +} diff --git a/part_6/tic_tac_toe/game/game_state.go b/part_6/tic_tac_toe_v5/game/game_state.go similarity index 60% rename from part_6/tic_tac_toe/game/game_state.go rename to part_6/tic_tac_toe_v5/game/game_state.go index 2539e8f..2f0b218 100644 --- a/part_6/tic_tac_toe/game/game_state.go +++ b/part_6/tic_tac_toe_v5/game/game_state.go @@ -10,3 +10,11 @@ const ( noughtWin quit ) + +// Режим игры +type GameMode int + +const ( + PlayerVsPlayer GameMode = iota + PlayerVsComputer +) diff --git a/part_6/tic_tac_toe_v5/go.mod b/part_6/tic_tac_toe_v5/go.mod new file mode 100644 index 0000000..0255d13 --- /dev/null +++ b/part_6/tic_tac_toe_v5/go.mod @@ -0,0 +1,14 @@ +module tic-tac-toe + +go 1.24.0 + +require gorm.io/gorm v1.30.0 + +require github.com/mattn/go-sqlite3 v1.14.22 // indirect + +require ( + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + golang.org/x/text v0.20.0 // indirect + gorm.io/driver/sqlite v1.6.0 +) diff --git a/part_6/tic_tac_toe_v5/go.sum b/part_6/tic_tac_toe_v5/go.sum new file mode 100644 index 0000000..528a324 --- /dev/null +++ b/part_6/tic_tac_toe_v5/go.sum @@ -0,0 +1,12 @@ +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.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +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_6/tic_tac_toe_v5/main.go b/part_6/tic_tac_toe_v5/main.go new file mode 100644 index 0000000..16f336d --- /dev/null +++ b/part_6/tic_tac_toe_v5/main.go @@ -0,0 +1,198 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" + "tic-tac-toe/database" + "tic-tac-toe/game" +) + +func loadGame(reader *bufio.Reader, repository database.IRepository) { + loadedGame := &game.Game{} + + for { + fmt.Print("Input your nickname: ") + nickName, _ := reader.ReadString('\n') + nickName = strings.TrimSpace(nickName) + + snapshote, err := repository.GetSnapshots(nickName) + if err != nil { + fmt.Println("Error loading game: ", err) + continue + } + + // Выводим все снапшоты игрока + fmt.Println("\n═══════════════════ SAVED GAMES ════════════════════") + fmt.Println("┌────────┬────────────────┬─────────┬─────────┬────────────┐") + fmt.Println("│ ID │ Name │ Figure │ Mode │ Difficulty │") + fmt.Println("├────────┼────────────────┼─────────┼─────────┼────────────┤") + + if len(*snapshote) == 0 { + fmt.Println("│ │ No saved games found │") + fmt.Println("└────────┴───────────────────────────────────────────────────┘") + } else { + for ID, snapshot := range *snapshote { + // Конвертируем режим игры (0=PvP, 1=PvC) в читаемый текст + gameMode := "PvP" + if snapshot.Mode == 1 { + gameMode = "PvC" + } + + // Конвертируем сложность (0=Easy, 1=Medium, 2=Hard) в читаемый текст + difficulty := "-" + if snapshot.Mode == 1 { // Только для режима PvC + switch snapshot.Difficulty { + case 0: + difficulty = "Easy" + case 1: + difficulty = "Medium" + case 2: + difficulty = "Hard" + } + } + + // Форматированный вывод с выравниванием колонок + figure := "X" + if snapshot.PlayerFigure == 1 { + figure = "O" + } + + name := snapshot.SnapshotName + if name == "" { + name = "Game " + strconv.Itoa(ID) + } + + fmt.Printf("│ %-4d │ %-14s │ %-4s │ %-5s │ %-7s │\n", + ID, name, figure, gameMode, difficulty) + } + fmt.Println("└────────┴────────────────┴─────────┴─────────┴────────────┘") + } + + snapID := -1 + for { + fmt.Print("Enter snapshot number: ") + num, _ := reader.ReadString('\n') + num = strings.TrimSpace(num) + + if snapID, _ = strconv.Atoi(num); snapID < 0 || snapID >= len(*snapshote) { + fmt.Println("Invalid snapshot number. Please try again.") + continue + } + break + } + // Восстанавливаем все необходимые поля игры + loadedGame.RestoreFromSnapshot( + &(*snapshote)[snapID], reader, + repository, + ) + + break + } + + // Запускаем игру + loadedGame.Play() +} + +func showFinishedGames(reader *bufio.Reader, repository database.IRepository) { + fmt.Print("Enter nickname: ") + nickName, _ := reader.ReadString('\n') + nickName = strings.TrimSpace(nickName) + + finishedGames, err := repository.GetFinishedGames(nickName) + if err != nil { + fmt.Println("Error loading finished games: ", err) + return + } + + fmt.Println("\n═══════════════════ FINISHED GAMES ════════════════════") + fmt.Println("┌────────┬─────────┬────────────┬────────────────┐") + fmt.Println("│ ID │ Figure │ Winner │ Date │") + fmt.Println("├────────┼─────────┼────────────┼────────────────┤") + + if len(*finishedGames) == 0 { + fmt.Println("│ │ No finished games found │") + fmt.Println("└────────┴───────────────────────────────────────────────┘") + } else { + for ID, game := range *finishedGames { + // Форматированный вывод с выравниванием колонок + figure := "X" + if game.PlayerFigure == 1 { + figure = "O" + } + + // Форматируем дату + dateStr := game.Time.Format("02.01 15:04") + + fmt.Printf("│ %-4d │ %-4s │ %-8s │ %-13s │\n", + ID, + figure, + game.WinnerName, + dateStr) + } + fmt.Println("└────────┴─────────┴────────────┴────────────────┘") + } + snapID := -1 + for { + fmt.Print("Enter snapshot number: ") + num, _ := reader.ReadString('\n') + num = strings.TrimSpace(num) + + if snapID, _ = strconv.Atoi(num); snapID < 0 || snapID >= len(*finishedGames) { + fmt.Println("Invalid snapshot number. Please try again.") + continue + } + break + } + + chosenGame := (*finishedGames)[snapID] + chosenGame.Board.PrintBoard() + fmt.Println() + fmt.Println("Winner: ", chosenGame.WinnerName) + fmt.Println("Date: ", chosenGame.Time.Format("02.01.2006 15:04")) + fmt.Println() +} + +func main() { + reader := bufio.NewReader(os.Stdin) + repository, err := database.NewSQLiteRepository() + if err != nil { + fmt.Println("Error creating game storage: ", err) + return + } + + for { + fmt.Println("Welcome to Tic-Tac-Toe!") + fmt.Println("1 - Load game") + fmt.Println("2 - New game") + fmt.Println("3 - Show all finished games") + fmt.Println("q - Exit") + fmt.Print("Your choice: ") + + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(input) + + switch input { + case "1": // Загрузка сохраненной игры + loadGame(reader, repository) + + case "2": // Создаем новую игру с помощью диалога настройки + newGame := game.SetupGame(reader, + repository) + // Запускаем игру + newGame.Play() + + case "3": // Показать все завершенные игры + showFinishedGames(reader, repository) + + case "q": + fmt.Println("Goodbye!") + return + + default: + fmt.Println("Invalid choice. Please try again.") + } + } +} diff --git a/part_6/tic_tac_toe_v5/model/finish_game_shapshot.go b/part_6/tic_tac_toe_v5/model/finish_game_shapshot.go new file mode 100644 index 0000000..a57a553 --- /dev/null +++ b/part_6/tic_tac_toe_v5/model/finish_game_shapshot.go @@ -0,0 +1,14 @@ +package model + +import ( + "tic-tac-toe/board" + "time" +) + +type FinishGameSnapshot struct { + Board *board.Board `json:"board"` + PlayerFigure board.BoardField `json:"player_figure"` + WinnerName string `json:"winner_name"` + PlayerNickName string `json:"nick_name"` + Time time.Time `json:"time"` +} diff --git a/part_6/tic_tac_toe_v5/model/game_snapshot.go b/part_6/tic_tac_toe_v5/model/game_snapshot.go new file mode 100644 index 0000000..fbe3184 --- /dev/null +++ b/part_6/tic_tac_toe_v5/model/game_snapshot.go @@ -0,0 +1,17 @@ +package model + +import ( + b "tic-tac-toe/board" + p "tic-tac-toe/player" +) + +// Структура для сериализации/десериализации игры +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"` +} diff --git a/part_6/tic_tac_toe_v5/player/computer_player.go b/part_6/tic_tac_toe_v5/player/computer_player.go new file mode 100644 index 0000000..310144c --- /dev/null +++ b/part_6/tic_tac_toe_v5/player/computer_player.go @@ -0,0 +1,306 @@ +package player + +import ( + "fmt" + "math/rand" + b "tic-tac-toe/board" + "time" +) + +// Уровни сложности компьютера +type Difficulty int + +const ( + Easy Difficulty = iota + Medium + Hard +) + +// Структура для представления игрока-компьютера +type ComputerPlayer struct { + Figure b.BoardField `json:"figure"` + Difficulty Difficulty `json:"difficulty"` + rand *rand.Rand +} + +// Создаем нового игрока-компьютера с заданным уровнем сложности +func NewComputerPlayer( + figure b.BoardField, + difficulty 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) 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 Easy: + row, col = p.makeEasyMove(board) + case Medium: + row, col = p.makeMediumMove(board) + case 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) 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 + // 50% шанс выбрать центр на нечетной доске + if p.rand.Intn(2) == 0 && board.Size%2 == 1 { + return center, center + } else { + corners := [][]int{ + {0, 0}, + {0, board.Size - 1}, + {board.Size - 1, 0}, + {board.Size - 1, board.Size - 1}, + } + randomCorner := corners[p.rand.Intn(len(corners))] + return randomCorner[0], randomCorner[1] + } + } + + // Для небольших досок (3x3) используем полный минимакс + if board.Size <= 3 { + bestScore := -1000 + bestMove := []int{-1, -1} + + // Рассматриваем все свободные клетки + for _, cell := range emptyCells { + row, col := cell[0], cell[1] + + // Пробуем сделать ход + board.Board[row][col] = p.Figure + + // Вычисляем оценку хода через минимакс + score := p.minimax(board, 0, false) + + // Возвращаем клетку в исходное состояние + board.Board[row][col] = b.Empty + + // Обновляем лучший ход + if score > bestScore { + bestScore = score + bestMove = []int{row, col} + } + } + + return bestMove[0], bestMove[1] + } + + // Для больших досок используем стратегию среднего уровня, + // так как полный минимакс будет слишком ресурсоемким + 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 +} diff --git a/part_6/tic_tac_toe_v5/player/i_player.go b/part_6/tic_tac_toe_v5/player/i_player.go new file mode 100644 index 0000000..68bafe0 --- /dev/null +++ b/part_6/tic_tac_toe_v5/player/i_player.go @@ -0,0 +1,22 @@ +package player + +import b "tic-tac-toe/board" + +// Интерфейс для любого игрока, будь то человек или компьютер +type IPlayer interface { + // Получение символа игрока (X или O) + GetSymbol() string + + // Переключение хода на другого игрока + SwitchPlayer() + + // Получение текущей фигуры игрока + GetFigure() b.BoardField + + // Выполнение хода игрока + // Возвращает координаты хода (x, y) и признак успешности + MakeMove(board *b.Board) (int, int, bool) + + // Проверка, является ли игрок компьютером + IsComputer() bool +} diff --git a/part_6/tic_tac_toe_v5/player/player.go b/part_6/tic_tac_toe_v5/player/player.go new file mode 100644 index 0000000..d55c65f --- /dev/null +++ b/part_6/tic_tac_toe_v5/player/player.go @@ -0,0 +1,81 @@ +package player + +import ( + "bufio" + "fmt" + "strconv" + "strings" + b "tic-tac-toe/board" +) + +// Структура для представления игрока-человека +type HumanPlayer struct { + Figure b.BoardField `json:"figure"` + Reader *bufio.Reader `json:"-"` +} + +func NewHumanPlayer( + figure b.BoardField, + reader *bufio.Reader, +) *HumanPlayer { + return &HumanPlayer{Figure: figure, Reader: reader} +} + +// Возвращаем символ игрока +func (p *HumanPlayer) GetSymbol() string { + if p.Figure == b.Cross { + return "X" + } + return "O" +} + +// Изменяем фигуру текущего игрока +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_6/tic_tac_toe_v5/tic_tac_toe.db b/part_6/tic_tac_toe_v5/tic_tac_toe.db new file mode 100644 index 0000000..706709e Binary files /dev/null and b/part_6/tic_tac_toe_v5/tic_tac_toe.db differ