mirror of
https://github.com/MADTeacher/go_basics.git
synced 2025-11-29 05:36:55 +02:00
add new project step and small fix
This commit is contained in:
@@ -7,3 +7,8 @@ type IGameLoader interface {
|
||||
type IGameSaver interface {
|
||||
SaveGame(path string, game *Game) error
|
||||
}
|
||||
|
||||
type IGameStorage interface {
|
||||
IGameLoader
|
||||
IGameSaver
|
||||
}
|
||||
@@ -6,13 +6,13 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func NewJsonGameLoader() IGameLoader {
|
||||
return &JsonGameLoader{}
|
||||
func NewJsonGameStorage() IGameStorage {
|
||||
return &JsonGameStorage{}
|
||||
}
|
||||
|
||||
type JsonGameLoader struct{}
|
||||
type JsonGameStorage struct{}
|
||||
|
||||
func (j *JsonGameLoader) LoadGame(path string) (*Game, error) {
|
||||
func (j *JsonGameStorage) LoadGame(path string) (*Game, error) {
|
||||
if !strings.HasSuffix(path, ".json") {
|
||||
path += ".json"
|
||||
}
|
||||
@@ -31,7 +31,7 @@ func (j *JsonGameLoader) LoadGame(path string) (*Game, error) {
|
||||
return &game, nil
|
||||
}
|
||||
|
||||
func (j *JsonGameLoader) SaveGame(path string, game *Game) error {
|
||||
func (j *JsonGameStorage) SaveGame(path string, game *Game) error {
|
||||
if !strings.HasSuffix(path, ".json") {
|
||||
path += ".json"
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
func main() {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
loader := game.NewJsonGameLoader()
|
||||
storage := game.NewJsonGameStorage()
|
||||
boardSize := 0
|
||||
|
||||
for {
|
||||
@@ -29,16 +29,19 @@ func main() {
|
||||
fmt.Println("Enter file name: ")
|
||||
fileName, _ := reader.ReadString('\n')
|
||||
fileName = strings.TrimSpace(fileName)
|
||||
loadedGame, err = loader.LoadGame(fileName)
|
||||
loadedGame, err = storage.LoadGame(fileName)
|
||||
if err != nil {
|
||||
fmt.Println("Error loading game.")
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
// Для полного восстановления экземпляра игры
|
||||
// присваиваем не дессериализуемым полям структуры
|
||||
// необходимые значения
|
||||
loadedGame.Reader = reader
|
||||
loadedGame.Saver = loader.(game.IGameSaver)
|
||||
loadedGame.Play()
|
||||
loadedGame.Saver = storage.(game.IGameSaver)
|
||||
loadedGame.Play() // Запускаем игру
|
||||
case "2":
|
||||
for {
|
||||
fmt.Print("Enter the size of the board (3-9): ")
|
||||
@@ -64,7 +67,7 @@ func main() {
|
||||
board := game.NewBoard(boardSize)
|
||||
player := game.NewPlayer()
|
||||
game := game.NewGame(*board, *player, reader,
|
||||
loader.(game.IGameSaver))
|
||||
storage.(game.IGameSaver))
|
||||
game.Play()
|
||||
case "q":
|
||||
return
|
||||
14
part_5/tic_tac_toe_v4/.vscode/launch.json
vendored
Normal file
14
part_5/tic_tac_toe_v4/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"name": "Launch Package",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${fileDirname}", // <- ставим запятую
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
113
part_5/tic_tac_toe_v4/board/board.go
Normal file
113
part_5/tic_tac_toe_v4/board/board.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package board
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
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) 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 {
|
||||
// Проверка строк и столбцов
|
||||
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
|
||||
}
|
||||
10
part_5/tic_tac_toe_v4/board/board_cell_type.go
Normal file
10
part_5/tic_tac_toe_v4/board/board_cell_type.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package board
|
||||
|
||||
type BoardField int
|
||||
|
||||
// фигуры в клетке поля
|
||||
const (
|
||||
Empty BoardField = iota
|
||||
Cross
|
||||
Nought
|
||||
)
|
||||
79
part_5/tic_tac_toe_v4/game/game_core.go
Normal file
79
part_5/tic_tac_toe_v4/game/game_core.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
b "tic-tac-toe/board"
|
||||
p "tic-tac-toe/player"
|
||||
s "tic-tac-toe/storage"
|
||||
)
|
||||
|
||||
// Game представляет состояние игры "Крестики-нолики"
|
||||
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"`
|
||||
Saver s.IGameSaver `json:"-"`
|
||||
// Режим игры (PvP или PvC)
|
||||
Mode GameMode `json:"mode"`
|
||||
// Уровень сложности компьютера (только для PvC)
|
||||
Difficulty p.Difficulty `json:"difficulty,omitempty"`
|
||||
// Флаг для определения текущего игрока
|
||||
IsCurrentFirst bool `json:"is_current_first"`
|
||||
}
|
||||
|
||||
// NewGame создает новую игру
|
||||
func NewGame(board b.Board, reader *bufio.Reader, saver s.IGameSaver,
|
||||
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,
|
||||
Saver: saver,
|
||||
Mode: mode,
|
||||
Difficulty: difficulty,
|
||||
IsCurrentFirst: true,
|
||||
}
|
||||
}
|
||||
|
||||
// switchCurrentPlayer переключает активного игрока
|
||||
func (g *Game) switchCurrentPlayer() {
|
||||
if g.CurrentPlayer == g.Player {
|
||||
g.CurrentPlayer = g.Player2
|
||||
} else {
|
||||
g.CurrentPlayer = g.Player
|
||||
}
|
||||
}
|
||||
|
||||
// updateState обновляет состояние игры на основе текущей доски
|
||||
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
|
||||
}
|
||||
}
|
||||
127
part_5/tic_tac_toe_v4/game/game_play.go
Normal file
127
part_5/tic_tac_toe_v4/game/game_play.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
b "tic-tac-toe/board"
|
||||
p "tic-tac-toe/player"
|
||||
)
|
||||
|
||||
// Play запускает игровой цикл
|
||||
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 {
|
||||
// Если ход человека, запрашиваем ввод
|
||||
figure := g.CurrentPlayer.GetFigure()
|
||||
if figure == b.Cross {
|
||||
fmt.Print("X move: ")
|
||||
} else {
|
||||
fmt.Print("O move: ")
|
||||
}
|
||||
|
||||
// Читаем ввод пользователя
|
||||
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()
|
||||
|
||||
switch g.State {
|
||||
case crossWin:
|
||||
fmt.Println("X wins!")
|
||||
case noughtWin:
|
||||
fmt.Println("O wins!")
|
||||
case draw:
|
||||
fmt.Println("It's a draw!")
|
||||
}
|
||||
|
||||
// Возвращаем true, если игра закончилась нормально (не выходом)
|
||||
return g.State != quit
|
||||
}
|
||||
|
||||
// saveCheck проверяет, является ли ввод командой сохранения
|
||||
func (g *Game) saveCheck(input string) bool {
|
||||
// Проверяем, если пользователь ввёл только "save" без имени файла
|
||||
if input == "save" {
|
||||
fmt.Println("Error: missing filename. " +
|
||||
"Please use the format: save filename")
|
||||
return true
|
||||
}
|
||||
|
||||
// Проверяем команду сохранения с именем файла
|
||||
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 true
|
||||
}
|
||||
|
||||
fmt.Printf("Game saved to file: %s\n", filename)
|
||||
shapshot := g.gameSnapshot()
|
||||
if err := g.Saver.SaveGame(filename, shapshot); err != nil {
|
||||
fmt.Printf("Error saving game: %v\n", err)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
82
part_5/tic_tac_toe_v4/game/game_serialization.go
Normal file
82
part_5/tic_tac_toe_v4/game/game_serialization.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
b "tic-tac-toe/board"
|
||||
m "tic-tac-toe/model"
|
||||
p "tic-tac-toe/player"
|
||||
s "tic-tac-toe/storage"
|
||||
)
|
||||
|
||||
// PrepareForSave подготавливает игру к сохранению
|
||||
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,
|
||||
saver s.IGameSaver,
|
||||
) {
|
||||
g.Board = snapshot.Board
|
||||
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}
|
||||
|
||||
g.Reader = reader
|
||||
g.Saver = saver
|
||||
|
||||
g.recreatePlayersAfterLoad(reader)
|
||||
}
|
||||
|
||||
// RecreatePlayersAfterLoad восстанавливает объекты игроков после загрузки из 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
|
||||
}
|
||||
}
|
||||
116
part_5/tic_tac_toe_v4/game/game_setup.go
Normal file
116
part_5/tic_tac_toe_v4/game/game_setup.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
b "tic-tac-toe/board"
|
||||
p "tic-tac-toe/player"
|
||||
s "tic-tac-toe/storage"
|
||||
)
|
||||
|
||||
// SetupGame создает новую игру с пользовательскими настройками
|
||||
func SetupGame(reader *bufio.Reader, saver s.IGameSaver) *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, saver, mode, difficulty)
|
||||
}
|
||||
|
||||
// getBoardSize запрашивает у пользователя размер доски
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// getGameMode запрашивает у пользователя режим игры
|
||||
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!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getDifficulty запрашивает у пользователя уровень сложности компьютера
|
||||
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!")
|
||||
}
|
||||
}
|
||||
}
|
||||
20
part_5/tic_tac_toe_v4/game/game_state.go
Normal file
20
part_5/tic_tac_toe_v4/game/game_state.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package game
|
||||
|
||||
type GameState int
|
||||
|
||||
// состояние игрового процесса
|
||||
const (
|
||||
playing GameState = iota
|
||||
draw
|
||||
crossWin
|
||||
noughtWin
|
||||
quit
|
||||
)
|
||||
|
||||
// Режим игры
|
||||
type GameMode int
|
||||
|
||||
const (
|
||||
PlayerVsPlayer GameMode = iota
|
||||
PlayerVsComputer
|
||||
)
|
||||
3
part_5/tic_tac_toe_v4/go.mod
Normal file
3
part_5/tic_tac_toe_v4/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module tic-tac-toe
|
||||
|
||||
go 1.24.0
|
||||
69
part_5/tic_tac_toe_v4/main.go
Normal file
69
part_5/tic_tac_toe_v4/main.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"tic-tac-toe/game"
|
||||
"tic-tac-toe/storage"
|
||||
)
|
||||
|
||||
func main() {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
gameStorage := storage.NewJsonGameStorage()
|
||||
|
||||
for {
|
||||
fmt.Println("Welcome to Tic-Tac-Toe!")
|
||||
fmt.Println("1 - Load game")
|
||||
fmt.Println("2 - New game")
|
||||
fmt.Println("q - Exit")
|
||||
fmt.Print("Your choice: ")
|
||||
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
|
||||
switch input {
|
||||
case "1":
|
||||
// Загрузка сохраненной игры
|
||||
loadedGame := &game.Game{}
|
||||
|
||||
for {
|
||||
fmt.Println("Input file name: ")
|
||||
fileName, _ := reader.ReadString('\n')
|
||||
fileName = strings.TrimSpace(fileName)
|
||||
|
||||
snapshote, err := gameStorage.LoadGame(fileName)
|
||||
if err != nil {
|
||||
fmt.Println("Error loading game: ", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Восстанавливаем все необходимые поля игры
|
||||
loadedGame.RestoreFromSnapshot(
|
||||
snapshote, reader,
|
||||
gameStorage.(storage.IGameSaver),
|
||||
)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
// Запускаем игру
|
||||
loadedGame.Play()
|
||||
|
||||
case "2":
|
||||
// Создаем новую игру с помощью диалога настройки
|
||||
newGame := game.SetupGame(reader, gameStorage.(storage.IGameSaver))
|
||||
|
||||
// Запускаем игру
|
||||
newGame.Play()
|
||||
|
||||
case "q":
|
||||
fmt.Println("Goodbye!")
|
||||
return
|
||||
|
||||
default:
|
||||
fmt.Println("Invalid choice. Please try again.")
|
||||
}
|
||||
}
|
||||
}
|
||||
16
part_5/tic_tac_toe_v4/model/game_snapshot.go
Normal file
16
part_5/tic_tac_toe_v4/model/game_snapshot.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
b "tic-tac-toe/board"
|
||||
p "tic-tac-toe/player"
|
||||
)
|
||||
|
||||
// Структура для сериализации/десериализации игры
|
||||
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"`
|
||||
}
|
||||
310
part_5/tic_tac_toe_v4/player/computer_player.go
Normal file
310
part_5/tic_tac_toe_v4/player/computer_player.go
Normal file
@@ -0,0 +1,310 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
b "tic-tac-toe/board"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Уровни сложности компьютера
|
||||
type Difficulty int
|
||||
|
||||
const (
|
||||
Easy Difficulty = iota
|
||||
Medium
|
||||
Hard
|
||||
)
|
||||
|
||||
// ComputerPlayer представляет игрока-компьютера
|
||||
type ComputerPlayer struct {
|
||||
Figure b.BoardField `json:"figure"`
|
||||
Difficulty Difficulty `json:"difficulty"`
|
||||
rand *rand.Rand
|
||||
}
|
||||
|
||||
// NewComputerPlayer создает нового игрока-компьютера с заданным уровнем сложности
|
||||
func NewComputerPlayer(
|
||||
figure b.BoardField,
|
||||
difficulty Difficulty,
|
||||
) *ComputerPlayer {
|
||||
source := rand.NewSource(time.Now().UnixNano())
|
||||
return &ComputerPlayer{
|
||||
Figure: figure,
|
||||
Difficulty: difficulty,
|
||||
rand: rand.New(source),
|
||||
}
|
||||
}
|
||||
|
||||
// GetSymbol возвращает символ игрока
|
||||
func (p *ComputerPlayer) GetSymbol() string {
|
||||
if p.Figure == b.Cross {
|
||||
return "X"
|
||||
}
|
||||
return "O"
|
||||
}
|
||||
|
||||
// SwitchPlayer изменяет фигуру текущего игрока
|
||||
func (p *ComputerPlayer) SwitchPlayer() {
|
||||
if p.Figure == b.Cross {
|
||||
p.Figure = b.Nought
|
||||
} else {
|
||||
p.Figure = b.Cross
|
||||
}
|
||||
}
|
||||
|
||||
// GetFigure возвращает текущую фигуру игрока
|
||||
func (p *ComputerPlayer) GetFigure() b.BoardField {
|
||||
return p.Figure
|
||||
}
|
||||
|
||||
// IsComputer возвращает true для компьютера
|
||||
func (p *ComputerPlayer) IsComputer() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// MakeMove реализует ход компьютера в зависимости от выбранной сложности
|
||||
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
|
||||
}
|
||||
22
part_5/tic_tac_toe_v4/player/i_player.go
Normal file
22
part_5/tic_tac_toe_v4/player/i_player.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package game
|
||||
|
||||
import b "tic-tac-toe/board"
|
||||
|
||||
// IPlayer представляет интерфейс для любого игрока (человека или компьютера)
|
||||
type IPlayer interface {
|
||||
// Получение символа игрока (X или O)
|
||||
GetSymbol() string
|
||||
|
||||
// Переключение хода на другого игрока
|
||||
SwitchPlayer()
|
||||
|
||||
// Получение текущей фигуры игрока
|
||||
GetFigure() b.BoardField
|
||||
|
||||
// Выполнение хода игрока
|
||||
// Возвращает координаты хода (x, y) и признак успешности
|
||||
MakeMove(board *b.Board) (int, int, bool)
|
||||
|
||||
// Проверка, является ли игрок компьютером
|
||||
IsComputer() bool
|
||||
}
|
||||
104
part_5/tic_tac_toe_v4/player/player.go
Normal file
104
part_5/tic_tac_toe_v4/player/player.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
b "tic-tac-toe/board"
|
||||
)
|
||||
|
||||
// HumanPlayer представляет игрока-человека
|
||||
type HumanPlayer struct {
|
||||
Figure b.BoardField `json:"figure"`
|
||||
Reader *bufio.Reader `json:"-"`
|
||||
}
|
||||
|
||||
// NewHumanPlayer создает нового игрока-человека
|
||||
func NewHumanPlayer(figure b.BoardField, reader *bufio.Reader) *HumanPlayer {
|
||||
return &HumanPlayer{Figure: figure, Reader: reader}
|
||||
}
|
||||
|
||||
// GetSymbol возвращает символ игрока
|
||||
func (p *HumanPlayer) GetSymbol() string {
|
||||
if p.Figure == b.Cross {
|
||||
return "X"
|
||||
}
|
||||
return "O"
|
||||
}
|
||||
|
||||
// SwitchPlayer изменяет фигуру текущего игрока
|
||||
func (p *HumanPlayer) SwitchPlayer() {
|
||||
if p.Figure == b.Cross {
|
||||
p.Figure = b.Nought
|
||||
} else {
|
||||
p.Figure = b.Cross
|
||||
}
|
||||
}
|
||||
|
||||
// GetFigure возвращает текущую фигуру игрока
|
||||
func (p *HumanPlayer) GetFigure() b.BoardField {
|
||||
return p.Figure
|
||||
}
|
||||
|
||||
// MakeMove обрабатывает строку ввода от человека и преобразует её в координаты хода
|
||||
// input - строка ввода в формате "1 2"
|
||||
func (p *HumanPlayer) MakeMove(board *b.Board) (int, int, bool) {
|
||||
fmt.Printf(
|
||||
"%s's turn. Enter row and column (e.g. 1 2): ",
|
||||
p.GetSymbol(),
|
||||
)
|
||||
|
||||
input, err := p.Reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
if err != nil {
|
||||
fmt.Println("Invalid input. Please try again.")
|
||||
return -1, -1, false
|
||||
}
|
||||
|
||||
return p.ParseMove(input, board)
|
||||
}
|
||||
|
||||
// ParseMove обрабатывает строку ввода от человека и преобразует её в координаты хода
|
||||
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
|
||||
}
|
||||
|
||||
// IsComputer возвращает false для человека-игрока
|
||||
func (p *HumanPlayer) IsComputer() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Для обратной совместимости
|
||||
type Player HumanPlayer
|
||||
|
||||
func NewPlayer() *Player {
|
||||
return (*Player)(NewHumanPlayer(b.Cross, nil))
|
||||
}
|
||||
|
||||
func (p *Player) SwitchPlayer() {
|
||||
(*HumanPlayer)(p).SwitchPlayer()
|
||||
}
|
||||
|
||||
func (p *Player) GetSymbol() string {
|
||||
return (*HumanPlayer)(p).GetSymbol()
|
||||
}
|
||||
1
part_5/tic_tac_toe_v4/rt.json
Normal file
1
part_5/tic_tac_toe_v4/rt.json
Normal file
@@ -0,0 +1 @@
|
||||
{"board":{"board":[[1,2,0],[0,1,0],[0,2,0]],"size":3},"player_figure":1,"state":0,"mode":1,"is_current_first":true}
|
||||
16
part_5/tic_tac_toe_v4/storage/i_game_storage.go
Normal file
16
part_5/tic_tac_toe_v4/storage/i_game_storage.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package storage
|
||||
|
||||
import m "tic-tac-toe/model"
|
||||
|
||||
type IGameLoader interface {
|
||||
LoadGame(path string) (*m.GameSnapshot, error)
|
||||
}
|
||||
|
||||
type IGameSaver interface {
|
||||
SaveGame(path string, game *m.GameSnapshot) error
|
||||
}
|
||||
|
||||
type IGameStorage interface {
|
||||
IGameLoader
|
||||
IGameSaver
|
||||
}
|
||||
52
part_5/tic_tac_toe_v4/storage/json_game_storage.go
Normal file
52
part_5/tic_tac_toe_v4/storage/json_game_storage.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
m "tic-tac-toe/model"
|
||||
)
|
||||
|
||||
func NewJsonGameStorage() IGameStorage {
|
||||
return &JsonGameStorage{}
|
||||
}
|
||||
|
||||
type JsonGameStorage struct{}
|
||||
|
||||
func (j *JsonGameStorage) LoadGame(path string) (*m.GameSnapshot, 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 snapshot m.GameSnapshot
|
||||
err = decoder.Decode(&snapshot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &snapshot, nil
|
||||
}
|
||||
|
||||
func (j *JsonGameStorage) SaveGame(path string, game *m.GameSnapshot) 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
|
||||
}
|
||||
Reference in New Issue
Block a user