mirror of
https://github.com/MADTeacher/go_basics.git
synced 2025-11-23 21:34:47 +02:00
Крестики-нолики с БД
This commit is contained in:
@@ -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!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package game
|
|
||||||
|
|
||||||
type IGameLoader interface {
|
|
||||||
LoadGame(path string) (*Game, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type IGameSaver interface {
|
|
||||||
SaveGame(path string, game *Game) error
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
module tic-tac-toe
|
|
||||||
|
|
||||||
go 1.24.0
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package game
|
package board
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -24,7 +24,7 @@ func NewBoard(size int) *Board {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Отображение игрового поля
|
// Отображение игрового поля
|
||||||
func (b *Board) printBoard() {
|
func (b *Board) PrintBoard() {
|
||||||
fmt.Print(" ")
|
fmt.Print(" ")
|
||||||
for i := range b.Size {
|
for i := range b.Size {
|
||||||
fmt.Printf("%d ", i+1)
|
fmt.Printf("%d ", i+1)
|
||||||
@@ -34,11 +34,11 @@ func (b *Board) printBoard() {
|
|||||||
fmt.Printf("%d ", i+1)
|
fmt.Printf("%d ", i+1)
|
||||||
for j := range b.Size {
|
for j := range b.Size {
|
||||||
switch b.Board[i][j] {
|
switch b.Board[i][j] {
|
||||||
case empty:
|
case Empty:
|
||||||
fmt.Print(". ")
|
fmt.Print(". ")
|
||||||
case cross:
|
case Cross:
|
||||||
fmt.Print("X ")
|
fmt.Print("X ")
|
||||||
case nought:
|
case Nought:
|
||||||
fmt.Print("O ")
|
fmt.Print("O ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,10 +48,10 @@ func (b *Board) printBoard() {
|
|||||||
|
|
||||||
// Проверка возможности и выполнения хода
|
// Проверка возможности и выполнения хода
|
||||||
func (b *Board) makeMove(x, y int) bool {
|
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) {
|
if b.makeMove(x, y) {
|
||||||
b.Board[x][y] = player
|
b.Board[x][y] = player
|
||||||
return true
|
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 {
|
for i := range b.Size {
|
||||||
rowWin, colWin := true, true
|
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 i := range b.Size {
|
||||||
for j := range b.Size {
|
for j := range b.Size {
|
||||||
if b.Board[i][j] == empty {
|
if b.Board[i][j] == Empty {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package game
|
package board
|
||||||
|
|
||||||
type BoardField int
|
type BoardField int
|
||||||
|
|
||||||
// фигуры в клетке поля
|
// фигуры в клетке поля
|
||||||
const (
|
const (
|
||||||
empty BoardField = iota
|
Empty BoardField = iota
|
||||||
cross
|
Cross
|
||||||
nought
|
Nought
|
||||||
)
|
)
|
||||||
127
part_6/tic_tac_toe_v5/database/crud.go
Normal file
127
part_6/tic_tac_toe_v5/database/crud.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
45
part_6/tic_tac_toe_v5/database/database.go
Normal file
45
part_6/tic_tac_toe_v5/database/database.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
12
part_6/tic_tac_toe_v5/database/db_definition.go
Normal file
12
part_6/tic_tac_toe_v5/database/db_definition.go
Normal file
@@ -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")
|
||||||
|
)
|
||||||
13
part_6/tic_tac_toe_v5/database/i_repository.go
Normal file
13
part_6/tic_tac_toe_v5/database/i_repository.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
31
part_6/tic_tac_toe_v5/database/models.go
Normal file
31
part_6/tic_tac_toe_v5/database/models.go
Normal file
@@ -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"`
|
||||||
|
}
|
||||||
54
part_6/tic_tac_toe_v5/database/utils.go
Normal file
54
part_6/tic_tac_toe_v5/database/utils.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
80
part_6/tic_tac_toe_v5/game/game_core.go
Normal file
80
part_6/tic_tac_toe_v5/game/game_core.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
170
part_6/tic_tac_toe_v5/game/game_play.go
Normal file
170
part_6/tic_tac_toe_v5/game/game_play.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
84
part_6/tic_tac_toe_v5/game/game_serialization.go
Normal file
84
part_6/tic_tac_toe_v5/game/game_serialization.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
116
part_6/tic_tac_toe_v5/game/game_setup.go
Normal file
116
part_6/tic_tac_toe_v5/game/game_setup.go
Normal file
@@ -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!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,3 +10,11 @@ const (
|
|||||||
noughtWin
|
noughtWin
|
||||||
quit
|
quit
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Режим игры
|
||||||
|
type GameMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
PlayerVsPlayer GameMode = iota
|
||||||
|
PlayerVsComputer
|
||||||
|
)
|
||||||
14
part_6/tic_tac_toe_v5/go.mod
Normal file
14
part_6/tic_tac_toe_v5/go.mod
Normal file
@@ -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
|
||||||
|
)
|
||||||
12
part_6/tic_tac_toe_v5/go.sum
Normal file
12
part_6/tic_tac_toe_v5/go.sum
Normal file
@@ -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=
|
||||||
198
part_6/tic_tac_toe_v5/main.go
Normal file
198
part_6/tic_tac_toe_v5/main.go
Normal file
@@ -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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
part_6/tic_tac_toe_v5/model/finish_game_shapshot.go
Normal file
14
part_6/tic_tac_toe_v5/model/finish_game_shapshot.go
Normal file
@@ -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"`
|
||||||
|
}
|
||||||
17
part_6/tic_tac_toe_v5/model/game_snapshot.go
Normal file
17
part_6/tic_tac_toe_v5/model/game_snapshot.go
Normal file
@@ -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"`
|
||||||
|
}
|
||||||
306
part_6/tic_tac_toe_v5/player/computer_player.go
Normal file
306
part_6/tic_tac_toe_v5/player/computer_player.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
22
part_6/tic_tac_toe_v5/player/i_player.go
Normal file
22
part_6/tic_tac_toe_v5/player/i_player.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
81
part_6/tic_tac_toe_v5/player/player.go
Normal file
81
part_6/tic_tac_toe_v5/player/player.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
BIN
part_6/tic_tac_toe_v5/tic_tac_toe.db
Normal file
BIN
part_6/tic_tac_toe_v5/tic_tac_toe.db
Normal file
Binary file not shown.
Reference in New Issue
Block a user