1
0
mirror of https://github.com/MADTeacher/go_basics.git synced 2025-11-23 21:34:47 +02:00

duplicate deleted

This commit is contained in:
Stanislav Chernyshev
2025-06-21 15:40:25 +03:00
parent 1c2614a78d
commit b895709c86
33 changed files with 0 additions and 2737 deletions

View File

@@ -1,14 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${fileDirname}", // <- ставим запятую
"console": "integratedTerminal"
}
]
}

View File

@@ -1,203 +0,0 @@
package board
import (
"fmt"
"sync"
)
const (
BoardDefaultSize int = 3
BoardMinSize int = 3
BoardMaxSize int = 9
)
type Board struct {
Board [][]BoardField `json:"board"`
Size int `json:"size"`
}
func NewBoard(size int) *Board {
board := make([][]BoardField, size)
for i := range board {
board[i] = make([]BoardField, size)
}
return &Board{Board: board, Size: size}
}
func (b *Board) IsEmpty() bool {
for i := range b.Size {
for j := range b.Size {
if b.Board[i][j] != Empty {
return false
}
}
}
return true
}
// Отображение игрового поля
func (b *Board) PrintBoard() {
fmt.Print(" ")
for i := range b.Size {
fmt.Printf("%d ", i+1)
}
fmt.Println()
for i := range b.Size {
fmt.Printf("%d ", i+1)
for j := range b.Size {
switch b.Board[i][j] {
case Empty:
fmt.Print(". ")
case Cross:
fmt.Print("X ")
case Nought:
fmt.Print("O ")
}
}
fmt.Println()
}
}
// Проверка возможности и выполнения хода
func (b *Board) makeMove(x, y int) bool {
return b.Board[x][y] == Empty
}
func (b *Board) SetSymbol(x, y int, player BoardField) bool {
if b.makeMove(x, y) {
b.Board[x][y] = player
return true
}
return false
}
// Проверка выигрыша
func (b *Board) CheckWin(player BoardField) bool {
if b.Size <= 4 {
// Для маленьких досок используем обычную проверку
return b.checkWinSequential(player)
}
// Для больших досок используем параллельную проверку
// 3 направления проверок: строки/столбцы, 2 диагонали
resultChan := make(chan bool, 3)
var wg sync.WaitGroup
wg.Add(3)
// Параллельная проверка строк и столбцов
go func() {
defer wg.Done()
for i := range b.Size {
rowWin, colWin := true, true
for j := 0; j < b.Size; j++ {
if b.Board[i][j] != player {
rowWin = false
}
if b.Board[j][i] != player {
colWin = false
}
}
if rowWin || colWin {
resultChan <- true
return // Нашли выигрыш, выходим из горутины
}
}
resultChan <- false
}()
// Параллельная проверка главной диагонали
go func() {
defer wg.Done()
mainDiag := true
for i := range b.Size {
if b.Board[i][i] != player {
mainDiag = false
break
}
}
resultChan <- mainDiag
}()
// Параллельная проверка побочной диагонали
go func() {
defer wg.Done()
antiDiag := true
for i := range b.Size {
if b.Board[i][b.Size-i-1] != player {
antiDiag = false
break
}
}
resultChan <- antiDiag
}()
// Запускаем горутину, которая закроет канал после завершения всех проверок
go func() {
wg.Wait()
close(resultChan)
}()
// Получаем результаты проверок с помощью for range.
// Этот цикл будет ждать, пока канал не будет закрыт.
for result := range resultChan {
if result {
return true // Найден выигрыш.
}
}
return false
}
// Оригинальный алгоритм проверки выигрыша для малых досок
func (b *Board) checkWinSequential(player BoardField) bool {
// Проверка строк и столбцов
for i := range b.Size {
rowWin, colWin := true, true
for j := range b.Size {
if b.Board[i][j] != player {
rowWin = false
}
if b.Board[j][i] != player {
colWin = false
}
}
if rowWin || colWin {
return true
}
}
// Главная диагональ
mainDiag := true
for i := range b.Size {
if b.Board[i][i] != player {
mainDiag = false
break
}
}
if mainDiag {
return true
}
// Побочная диагональ
antiDiag := true
for i := range b.Size {
if b.Board[i][b.Size-i-1] != player {
antiDiag = false
break
}
}
return antiDiag
}
// Проверка на ничью
func (b *Board) CheckDraw() bool {
for i := range b.Size {
for j := range b.Size {
if b.Board[i][j] == Empty {
return false
}
}
}
return true
}

View File

@@ -1,23 +0,0 @@
package board
type BoardField int
// фигуры в клетке поля
const (
Empty BoardField = iota
Cross
Nought
)
func (bf BoardField) String() string {
switch bf {
case Empty:
return "."
case Cross:
return "X"
case Nought:
return "O"
default:
return "?"
}
}

View File

@@ -1,114 +0,0 @@
package client
import (
"encoding/json"
"fmt"
"log"
"net"
"sync"
"time"
b "tic-tac-toe/board"
"tic-tac-toe/network"
)
// Client represents the client-side application.
type Client struct {
conn net.Conn
board *b.Board
mySymbol b.BoardField
currentPlayer b.BoardField
playerName string
roomName string
state State
mutex sync.RWMutex
lastMsgTime time.Time
}
// NewClient creates a new client and connects to the server.
func NewClient(addr string) (*Client, error) {
conn, err := net.Dial("tcp", addr)
if err != nil {
return nil, err
}
return &Client{
conn: conn,
state: waitNickNameConfirm,
mySymbol: b.Empty, // Will be set upon joining a room
}, nil
}
func (c *Client) setNickname(nickname string) {
c.playerName = nickname
}
func (c *Client) getState() State {
c.mutex.RLock()
defer c.mutex.RUnlock()
return c.state
}
func (c *Client) setState(state State) {
c.mutex.Lock()
defer c.mutex.Unlock()
// Display a message only when transitioning to opponentMove
if state == opponentMove && c.state != opponentMove {
fmt.Println("\nWaiting for opponent's move...")
} else if state == waitingOpponentInRoom && c.state != waitingOpponentInRoom {
fmt.Println("\nWaiting for opponent to join...")
}
c.state = state
}
// Start begins the client's main loop for sending and receiving messages.
func (c *Client) Start() {
defer c.conn.Close()
fmt.Println("Connected to server. ")
go c.readFromServer()
c.menu()
}
// readFromServer continuously reads messages from the server and handles them.
func (c *Client) readFromServer() {
decoder := json.NewDecoder(c.conn)
for {
var msg network.Message
if err := decoder.Decode(&msg); err != nil {
log.Printf("Disconnected from server: %v", err)
return // Exit goroutine if connection is lost
}
switch msg.Cmd {
case network.CmdRoomJoinResponse:
c.handleRoomJoinResponse(msg.Payload)
case network.CmdInitGame:
c.handleInitGame(msg.Payload)
case network.CmdUpdateState:
c.handleUpdateState(msg.Payload)
case network.CmdEndGame:
c.handleEndGame(msg.Payload)
case network.CmdError:
c.handleError(msg.Payload)
case network.CmdRoomListResponse:
c.handleRoomListResponse(msg.Payload)
case network.CmdNickNameResponse:
c.handleNickNameResponse(msg.Payload)
case network.CmdOpponentLeft:
c.handleOpponentLeft(msg.Payload)
case network.CmdFinishedGamesResponse:
c.handleFinishedGamesResponse(msg.Payload)
case network.CmdFinishedGameResponse:
c.handleFinishedGameResponse(msg.Payload)
default:
log.Printf(
"Received unhandled message type '%s' "+
"from server. Payload: %s\n> ",
msg.Cmd, string(msg.Payload),
)
}
}
}

View File

@@ -1,15 +0,0 @@
package client
type State int
const (
waitNickNameConfirm State = iota
mainMenu
waitRoomJoin
playing
playerMove
opponentMove
endGame
waitingOpponentInRoom
waitResponseFromServer
)

View File

@@ -1,158 +0,0 @@
package client
import (
"bufio"
"encoding/json"
"fmt"
"log"
"os"
"strconv"
"strings"
"tic-tac-toe/network"
"time"
)
func (c *Client) menu() {
reader := bufio.NewReader(os.Stdin)
encoder := json.NewEncoder(c.conn)
fmt.Print("Enter your nickname: ")
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)
c.playerName = input
var msg network.Message
msg.Cmd = network.CmdNickname
payloadData := network.NicknameRequest{Nickname: c.playerName}
jsonPayload, err := json.Marshal(payloadData)
if err != nil {
log.Printf("Error marshalling payload for command %s: %v", msg.Cmd, err)
return
}
msg.Payload = jsonPayload
if err := encoder.Encode(msg); err != nil {
log.Printf("Failed to send message to server: %v. Disconnecting.", err)
return // Exit if we can't send to server
}
for {
switch c.getState() {
case waitNickNameConfirm:
time.Sleep(100 * time.Millisecond)
continue
case mainMenu:
c.mainMenu(reader, encoder)
case playerMove:
c.playing(reader, encoder)
case opponentMove:
// Just wait silently for opponent's move
time.Sleep(1000 * time.Millisecond)
continue
case endGame:
fmt.Println("\nGame has ended. Restarting in 10 seconds...")
time.Sleep(10 * time.Second)
continue
case waitResponseFromServer:
time.Sleep(100 * time.Millisecond)
continue
case waitingOpponentInRoom:
// Rate-limit messages to once every 3 seconds
now := time.Now()
if now.Sub(c.lastMsgTime) > 3*time.Second {
c.lastMsgTime = now
fmt.Println("\nWaiting for opponent to join...")
fmt.Println("Press 'q' and Enter to return to main menu")
fmt.Print("> ")
}
// Poll for input every cycle but don't block
var buffer [1]byte
n, _ := os.Stdin.Read(buffer[:])
if n > 0 && (buffer[0] == 'q' || buffer[0] == 'Q') {
fmt.Println("Leaving room...")
var msg network.Message
msg.Cmd = network.CmdLeaveRoomRequest
payload := network.LeaveRoomRequest{
RoomName: c.roomName,
PlayerName: c.playerName,
}
jsonPayload, _ := json.Marshal(payload)
msg.Payload = jsonPayload
encoder.Encode(msg)
c.setState(mainMenu)
continue
}
// Short sleep to avoid CPU spinning
time.Sleep(100 * time.Millisecond)
continue
}
}
}
func (c *Client) mainMenu(reader *bufio.Reader, encoder *json.Encoder) {
var msg network.Message
fmt.Println("Enter command:")
fmt.Println("1 - Get room list")
fmt.Println("2 - Join room")
fmt.Println("3 - Get finished games")
fmt.Println("4 - Get finished game by id")
fmt.Println("5 - Exit")
fmt.Print("> ")
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)
command, err := strconv.Atoi(input)
if err != nil {
fmt.Println("Invalid command.")
return
}
switch command {
case 1:
msg.Cmd = network.CmdListRoomsRequest
encoder.Encode(msg)
c.setState(waitResponseFromServer)
case 2:
fmt.Print("Enter room name: ")
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)
c.roomName = input
msg.Cmd = network.CmdJoinRoomRequest
payload := network.JoinRoomRequest{
RoomName: c.roomName,
PlayerName: c.playerName,
}
jsonPayload, _ := json.Marshal(payload)
msg.Payload = jsonPayload
encoder.Encode(msg)
c.setState(waitResponseFromServer)
case 3:
msg.Cmd = network.CmdFinishedGamesRequest
encoder.Encode(msg)
c.setState(waitResponseFromServer)
case 4:
fmt.Print("Enter game id: ")
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)
gameId, err := strconv.Atoi(input)
if err != nil {
fmt.Println("Invalid game id.")
return
}
msg.Cmd = network.CmdFinishedGameByIdRequest
payload := network.GetFinishedGameByIdRequest{GameID: gameId}
jsonPayload, _ := json.Marshal(payload)
msg.Payload = jsonPayload
encoder.Encode(msg)
c.setState(waitResponseFromServer)
case 5:
os.Exit(0)
default:
fmt.Println("Unknown command.")
return
}
}

View File

@@ -1,60 +0,0 @@
package client
import (
"bufio"
"encoding/json"
"fmt"
"strconv"
"strings"
"tic-tac-toe/network"
)
func (c *Client) playing(reader *bufio.Reader, encoder *json.Encoder) {
fmt.Printf("\nEnter command: <row> <col> or q for exit to main menu\n> ")
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)
if input == "q" {
var msg network.Message
msg.Cmd = network.CmdLeaveRoomRequest
payload := network.LeaveRoomRequest{
RoomName: c.roomName,
PlayerName: c.playerName,
}
jsonPayload, _ := json.Marshal(payload)
msg.Payload = jsonPayload
encoder.Encode(msg)
c.setState(mainMenu)
return
}
parts := strings.Fields(input)
if len(parts) != 2 {
fmt.Println("Usage: <row> <col>")
return
}
var msg network.Message
row, err1 := strconv.Atoi(parts[0])
col, err2 := strconv.Atoi(parts[1])
if err1 != nil || err2 != nil {
fmt.Println("Row and column must be numbers.")
return
}
if !c.validateMove(row, col) {
return // validateMove prints the error
}
msg.Cmd = network.CmdMakeMoveRequest
payload := network.MakeMoveRequest{
RoomName: c.roomName,
PlayerName: c.playerName,
PositionRow: row - 1,
PositionCol: col - 1,
}
jsonPayload, _ := json.Marshal(payload)
msg.Payload = jsonPayload
encoder.Encode(msg)
c.setState(waitResponseFromServer)
}

View File

@@ -1,210 +0,0 @@
package client
import (
"encoding/json"
"fmt"
"log"
b "tic-tac-toe/board"
g "tic-tac-toe/game"
"tic-tac-toe/network"
)
// handleRoomJoinResponse processes the RoomJoinResponse message from the server.
func (c *Client) handleRoomJoinResponse(payload json.RawMessage) {
var res network.RoomJoinResponse
if err := json.Unmarshal(payload, &res); err == nil {
c.mySymbol = res.PlayerSymbol
c.roomName = res.RoomName
if res.Board.Size > 0 { // Check if board is valid
c.board = &res.Board
fmt.Printf("\nSuccessfully joined room '%s' as %s.\n", res.RoomName, res.PlayerSymbol)
c.board.PrintBoard()
} else {
fmt.Printf("\nSuccessfully joined room '%s' as %s. Waiting for game to start...\n", res.RoomName, res.PlayerSymbol)
}
} else {
log.Printf("Error unmarshalling RoomJoinResponse: %v", err)
}
c.setState(waitingOpponentInRoom)
}
// handleInitGame processes the InitGameResponse message from the server.
func (c *Client) handleInitGame(payload json.RawMessage) {
var res network.InitGameResponse
if err := json.Unmarshal(payload, &res); err == nil {
c.board = &res.Board
c.currentPlayer = res.CurrentPlayer
fmt.Println("\n--- Game Started ---")
c.board.PrintBoard()
c.printTurnInfo()
if res.CurrentPlayer == c.mySymbol {
c.setState(playerMove)
} else {
c.setState(opponentMove)
}
} else {
log.Printf("Error unmarshalling InitGameResponse: %v", err)
}
}
// handleUpdateState processes the GameStateUpdate message from the server.
func (c *Client) handleUpdateState(payload json.RawMessage) {
var res network.GameStateUpdate
if err := json.Unmarshal(payload, &res); err == nil {
c.board = &res.Board
c.currentPlayer = res.CurrentPlayer
fmt.Println("\n--- Game State Update ---")
c.board.PrintBoard()
c.printTurnInfo()
if res.CurrentPlayer == c.mySymbol {
c.setState(playerMove)
} else {
c.setState(opponentMove)
}
} else {
log.Printf("Error unmarshalling GameStateUpdate: %v", err)
}
}
// handleEndGame processes the EndGameResponse message from the server.
func (c *Client) handleEndGame(payload json.RawMessage) {
var res network.EndGameResponse
if err := json.Unmarshal(payload, &res); err == nil {
c.board = &res.Board
fmt.Println("\n--- Game Over ---")
c.board.PrintBoard()
if res.CurrentPlayer == b.Empty {
fmt.Println("It's a Draw!")
} else {
fmt.Printf("Player %s wins!\n", res.CurrentPlayer)
}
c.setState(endGame)
fmt.Print("> ")
} else {
log.Printf("Error unmarshalling EndGameResponse: %v", err)
}
}
// handleError processes the ErrorResponse message from the server.
func (c *Client) handleError(payload json.RawMessage) {
var errPayload network.ErrorResponse
if err := json.Unmarshal(payload, &errPayload); err == nil {
fmt.Printf("\nServer Error: %s\n> ", errPayload.Message)
} else {
log.Printf("Error unmarshalling ErrorResponse: %v", err)
}
c.setState(mainMenu)
}
// gameModeToString converts GameMode to a string representation.
func gameModeToString(mode g.GameMode) string {
switch mode {
case g.PvP:
return "PvP"
case g.PvC:
return "PvC"
default:
return "Unknown"
}
}
func difficultyToString(difficulty g.Difficulty) string {
switch difficulty {
case g.Easy:
return "Easy"
case g.Medium:
return "Medium"
case g.Hard:
return "Hard"
default:
return ""
}
}
// handleFinishedGamesResponse processes the FinishedGamesResponse message from the server.
func (c *Client) handleFinishedGamesResponse(payload json.RawMessage) {
var res network.FinishedGamesResponse
if err := json.Unmarshal(payload, &res); err == nil {
fmt.Println("\nFinished games:")
if res.Games == nil || len(*res.Games) == 0 {
fmt.Println("No finished games.")
} else {
for _, game := range *res.Games {
fmt.Printf("- Game #%d: %s vs %s (Winner: %s) at %v\n",
game.ID, game.WinnerName,
game.AnotherPlayerName, game.WinnerName,
game.Time.Format("2006-01-02 15:04:05"),
)
}
}
} else {
log.Printf("Error unmarshalling FinishedGamesResponse: %v", err)
}
c.setState(mainMenu)
}
// handleRoomListResponse processes the RoomListResponse message from the server.
func (c *Client) handleRoomListResponse(payload json.RawMessage) {
var roomList network.RoomListResponse
if err := json.Unmarshal(payload, &roomList); err == nil {
fmt.Println("\nAvailable rooms:")
if len(roomList.Rooms) == 0 {
fmt.Println("No rooms available.")
} else {
for _, room := range roomList.Rooms {
fmt.Printf("- %s (Board Size: %dx%d, Full: %t, Mode: %s, Difficulty: %s)\n",
room.Name,
room.BoardSize, room.BoardSize,
room.IsFull,
gameModeToString(room.GameMode),
difficultyToString(room.Difficult),
)
}
}
} else {
log.Printf("Error unmarshalling RoomListResponse: %v", err)
}
c.setState(mainMenu)
}
// handleNickNameResponse processes the NickNameResponse message from the server.
func (c *Client) handleNickNameResponse(payload json.RawMessage) {
var res network.NickNameResponse
if err := json.Unmarshal(payload, &res); err == nil {
fmt.Printf("\nWelcome, %s!\n> ", res.Nickname)
c.setNickname(res.Nickname)
c.setState(mainMenu)
} else {
log.Printf("Error unmarshalling NickNameResponse: %v", err)
}
}
// handleOpponentLeft processes the OpponentLeft message from the server.
func (c *Client) handleOpponentLeft(payload json.RawMessage) {
var res network.OpponentLeft
if err := json.Unmarshal(payload, &res); err == nil {
fmt.Printf("\nPlayer '%s' has left the game.\n> ", res.Nickname)
} else {
log.Printf("Error unmarshalling OpponentLeft: %v", err)
}
c.setState(waitingOpponentInRoom)
}
// handleFinishedGameResponse processes the FinishedGameResponse message from the server.
func (c *Client) handleFinishedGameResponse(payload json.RawMessage) {
var res network.FinishedGameResponse
if err := json.Unmarshal(payload, &res); err == nil {
fmt.Println("\nFinished game:")
fmt.Printf("- Game #%d: %s vs %s (Winner: %s) at %v\n",
res.Game.ID, res.Game.WinnerName,
res.Game.AnotherPlayerName, res.Game.WinnerName,
res.Game.Time.Format("2006-01-02 15:04:05"),
)
c.board = res.Game.Board
c.board.PrintBoard()
} else {
log.Printf("Error unmarshalling FinishedGameResponse: %v", err)
}
c.setState(mainMenu)
}

View File

@@ -1,38 +0,0 @@
package client
import (
"fmt"
b "tic-tac-toe/board"
)
func (c *Client) printTurnInfo() {
if c.board == nil {
return
}
if c.currentPlayer == c.mySymbol {
fmt.Println("It's your turn.")
} else if c.currentPlayer != b.Empty {
fmt.Printf("It's player %s's turn.\n", c.currentPlayer)
} else {
// Game might be over or in an intermediate state
}
fmt.Print("> ")
}
// validateMove checks if a move is valid based on the local board state.
func (c *Client) validateMove(row, col int) bool {
if c.board == nil {
fmt.Println("Game has not started yet.")
return false
}
if row < 1 || row > c.board.Size || col < 1 || col > c.board.Size {
fmt.Printf("Invalid move. Row and column must be between 1 and %d.\n", c.board.Size)
return false
}
// Convert to 0-indexed for board access
if c.board.Board[row-1][col-1] != b.Empty {
fmt.Println("Invalid move. Cell is already occupied.")
return false
}
return true
}

View File

@@ -1,74 +0,0 @@
package database
import (
"encoding/json"
m "tic-tac-toe/model"
)
func (r *SQLiteRepository) createPlayer(
nickName string,
) (*Player, error) {
player := &Player{NickName: nickName}
if err := r.db.Create(player).Error; err != nil {
return nil, err
}
return player, nil
}
func (r *SQLiteRepository) getPlayer(
nickName string,
) (*Player, error) {
var player Player
if err := r.db.Where(
"nick_name = ?", nickName,
).First(&player).Error; err != nil {
return nil, err
}
return &player, nil
}
func (r *SQLiteRepository) SaveFinishedGame(
snapshot *m.FinishGameSnapshot) error {
boardJSON, err := json.Marshal(snapshot.Board)
if err != nil {
return err
}
player, _ := r.getPlayer(snapshot.WinnerName)
if player == nil {
player, _ = r.createPlayer(snapshot.WinnerName)
}
return r.db.Create(&PlayerFinishGame{
BoardJSON: boardJSON,
PlayerFigure: int(snapshot.PlayerFigure),
WinnerName: snapshot.WinnerName,
PlayerNickName: player.NickName,
Time: snapshot.Time,
}).Error
}
func (r *SQLiteRepository) GetAllFinishedGames() (*[]m.FinishGameSnapshot, error) {
var playerFinishGames []PlayerFinishGame
if err := r.db.Find(&playerFinishGames).Error; err != nil {
return nil, err
}
var finishGameSnapshots []m.FinishGameSnapshot
for _, playerFinishGame := range playerFinishGames {
temp, err := playerFinishGame.ToModel()
if err != nil {
return nil, err
}
finishGameSnapshots = append(finishGameSnapshots, *temp)
}
return &finishGameSnapshots, nil
}
func (r *SQLiteRepository) GetFinishedGameById(id int) (*m.FinishGameSnapshot, error) {
var playerFinishGame PlayerFinishGame
if err := r.db.Where("id = ?", id).First(&playerFinishGame).Error; err != nil {
return nil, err
}
return playerFinishGame.ToModel()
}

View File

@@ -1,45 +0,0 @@
package database
import (
"fmt"
"os"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type SQLiteRepository struct {
db *gorm.DB // заменили на *gorm.DB
}
func NewSQLiteRepository() (*SQLiteRepository, error) {
// Создаем репозиторий
repository := &SQLiteRepository{}
// Проверяем существование файла базы данных
dbExists := true
if _, err := os.Stat(dbName); os.IsNotExist(err) {
dbExists = false
}
// Открываем соединение с базой данных
db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{})
if err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
// Сохраняем соединение в репозитории
repository.db = db
// Если база данных только что создана, выполняем миграцию
if !dbExists {
fmt.Println("Creating new database schema")
if err := db.AutoMigrate(&Player{}, &PlayerFinishGame{}); err != nil {
return nil, fmt.Errorf("failed to migrate database: %w", err)
}
} else {
fmt.Println("Using existing database")
}
return repository, nil
}

View File

@@ -1,12 +0,0 @@
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")
)

View File

@@ -1,13 +0,0 @@
package database
import "tic-tac-toe/model"
// Интерфейс для работы с базой данных
type IRepository interface {
// Сохраняет информацию о завершенной игре
SaveFinishedGame(snapshot *model.FinishGameSnapshot) error
// Получает все завершенные игры для указанного игрока
GetAllFinishedGames() (*[]model.FinishGameSnapshot, error)
// Получает конкретную завершенную игру по ID
GetFinishedGameById(id int) (*model.FinishGameSnapshot, error)
}

View File

@@ -1,21 +0,0 @@
package database
import "time"
// Player представляет модель таблицы
// для хранения профилей игроков в БД
type Player struct {
NickName string `gorm:"primary_key;not null"`
}
// PlayerFinishGame представляет модель таблицы
// для хранения завершенной игры в БД
type PlayerFinishGame struct {
ID int `gorm:"primary_key;autoIncrement;not null"`
WinnerName string `gorm:"not null"`
BoardJSON []byte `gorm:"type:json;not null"`
PlayerFigure int `gorm:"not null"`
Time time.Time `gorm:"not null"`
PlayerNickName string `gorm:"not null"`
Player *Player `gorm:"foreignKey:PlayerNickName;references:NickName"`
}

View File

@@ -1,36 +0,0 @@
package database
import (
"encoding/json"
b "tic-tac-toe/board"
m "tic-tac-toe/model"
)
// Задаем имя таблицы для структуры Player
func (p *Player) TableName() string {
return "players"
}
// Задаем имя таблицы для структуры PlayerFinishGame
func (pfg *PlayerFinishGame) TableName() string {
return "player_finish_games"
}
// Преобразуем таблицу PlayerFinishGame в модель PlayerFinishGame
// из пакета model
func (f *PlayerFinishGame) ToModel() (*m.FinishGameSnapshot, error) {
var board b.Board
if err := json.Unmarshal(f.BoardJSON, &board); err != nil {
return nil, err
}
return &m.FinishGameSnapshot{
ID: f.ID,
Board: &board,
PlayerFigure: b.BoardField(f.PlayerFigure),
WinnerName: f.WinnerName,
AnotherPlayerName: f.PlayerNickName,
Time: f.Time,
}, nil
}

View File

@@ -1,31 +0,0 @@
package game
type GameState int
// состояние игрового процесса
const (
WaitingOpponent GameState = iota
Draw
CrossWin
NoughtWin
CrossStep
NoughtStep
)
// Режим игры
type GameMode int
const (
PvP GameMode = iota
PvC
)
// Уровни сложности компьютера
type Difficulty int
const (
None Difficulty = iota
Easy
Medium
Hard
)

View File

@@ -1,16 +0,0 @@
module tic-tac-toe
go 1.24.0
require github.com/mattn/go-sqlite3 v1.14.28 // indirect
require (
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.30.0
)
require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
golang.org/x/text v0.26.0 // indirect
)

View File

@@ -1,14 +0,0 @@
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=

View File

@@ -1,37 +0,0 @@
package main
import (
"flag"
"log"
"tic-tac-toe/client"
"tic-tac-toe/database"
"tic-tac-toe/server"
)
func main() {
mode := flag.String("mode", "client", "start in 'server' or 'client' mode")
addr := flag.String("addr", ":8088", "address to run on")
flag.Parse()
repository, err := database.NewSQLiteRepository()
if err != nil {
log.Fatalf("Failed to create repository: %v", err)
}
switch *mode {
case "server":
srv, err := server.NewServer(*addr, repository)
if err != nil {
log.Fatalf("Failed to create server: %v", err)
}
srv.Start()
case "client":
cli, err := client.NewClient(*addr)
if err != nil {
log.Fatalf("Failed to connect to server: %v", err)
}
cli.Start()
default:
log.Fatalf("Unknown mode: %s. Use 'server' or 'client'.", *mode)
}
}

View File

@@ -1,15 +0,0 @@
package model
import (
"tic-tac-toe/board"
"time"
)
type FinishGameSnapshot struct {
ID int `json:"id"`
Board *board.Board `json:"board"`
PlayerFigure board.BoardField `json:"player_figure"`
WinnerName string `json:"winner_name"`
AnotherPlayerName string `json:"another_player_name"`
Time time.Time `json:"time"`
}

View File

@@ -1,52 +0,0 @@
package network
const (
// Client to Server Commands
CmdNickname Command = "nickname"
CmdJoinRoomRequest Command = "join_room"
CmdLeaveRoomRequest Command = "leave_room"
CmdListRoomsRequest Command = "list_rooms"
CmdMakeMoveRequest Command = "make_move"
CmdFinishedGamesRequest Command = "get_finished_games"
CmdFinishedGameByIdRequest Command = "get_finished_game_by_id"
)
// LoginRequest отправляется клиентом для входа в систему.
type NicknameRequest struct {
Nickname string `json:"nickname"`
}
// JoinRoomRequest отправляется клиентом для подключения к существующей комнате.
type JoinRoomRequest struct {
RoomName string `json:"room_name"`
PlayerName string `json:"player_name"`
}
// LeaveRoomRequest отправляется клиентом для выхода из текущей комнаты.
type LeaveRoomRequest struct {
RoomName string `json:"room_name"`
PlayerName string `json:"player_name"`
}
// ListRoomsRequest отправляется клиентом для получения списка доступных комнат.
// Обычно для этого запроса не требуется специальная полезная нагрузка.
type ListRoomsRequest struct {
}
// MakeMoveRequest отправляется клиентом для совершения хода в игре.
type MakeMoveRequest struct {
RoomName string `json:"room_name"`
PlayerName string `json:"player_name"`
PositionRow int `json:"position_row"`
PositionCol int `json:"position_col"`
}
// GetFinishedGamesRequest отправляется клиентом для получения списка завершенных игр.
// Обычно для этого запроса не требуется специальная полезная нагрузка, если запрашиваются все игры для пользователя.
type GetFinishedGamesRequest struct {
}
// GetFinishedGameByIdRequest отправляется клиентом для получения конкретной завершенной игры.
type GetFinishedGameByIdRequest struct {
GameID int `json:"game_id"`
}

View File

@@ -1,10 +0,0 @@
package network
import "encoding/json"
type Command string
type Message struct {
Cmd Command `json:"command"`
Payload json.RawMessage `json:"payload,omitempty"`
}

View File

@@ -1,94 +0,0 @@
package network
import (
b "tic-tac-toe/board"
g "tic-tac-toe/game"
"tic-tac-toe/model"
)
const (
// Server to Client Commands
CmdUpdateState Command = "update_state"
CmdError Command = "error"
CmdNickNameResponse Command = "nick_name_response"
CmdRoomCreated Command = "room_created"
CmdRoomJoinResponse Command = "room_join_response"
CmdRoomListResponse Command = "room_list_response"
CmdInitGame Command = "init_game"
CmdOpponentLeft Command = "opponent_left"
CmdEndGame Command = "end_game"
CmdFinishedGamesResponse Command = "finished_games_response"
CmdFinishedGameResponse Command = "finished_game_response"
)
// сообщение о том, что противник покинул игру
// инициализирующее сообщение в начале партии
// InitGameResponse отправляется сервером при инициализации игры.
type InitGameResponse struct {
Board b.Board `json:"board"`
CurrentPlayer b.BoardField `json:"current_player"`
}
// EndGameResponse отправляется сервером при завершении игры.
type EndGameResponse struct {
Board b.Board `json:"board"`
CurrentPlayer b.BoardField `json:"current_player"`
}
type OpponentLeft struct {
Nickname string `json:"nickname"`
}
// RoomInfo содержит информацию о комнате.
type RoomInfo struct {
Name string `json:"name"`
BoardSize int `json:"board_size"`
IsFull bool `json:"is_full"`
GameMode g.GameMode `json:"game_mode"`
Difficult g.Difficulty `json:"difficult"`
}
// RoomListсодержит список доступных комнат.
type RoomListResponse struct {
Rooms []RoomInfo `json:"rooms"`
}
// GameStateUpdate содержит информацию об обновлении состояния игры.
type GameStateUpdate struct {
Board b.Board `json:"board"`
CurrentPlayer b.BoardField `json:"current_player"`
}
// ErrorResponse отправляется сервером при возникновении ошибки.
type ErrorResponse struct {
Message string `json:"message"`
}
// NickNameResponse отправляется сервером при успешном входе клиента.
type NickNameResponse struct {
Nickname string `json:"nickname"`
}
// RoomCreatedResponse отправляется сервером после успешного создания комнаты.
type RoomCreatedResponse struct {
RoomID string `json:"room_id"`
RoomName string `json:"room_name"`
}
// RoomJoinResponse отправляется сервером, когда клиент успешно присоединился к комнате.
type RoomJoinResponse struct {
RoomName string `json:"room_name"`
PlayerSymbol b.BoardField `json:"player_symbol"`
Board b.Board `json:"board"`
}
// FinishedGamesResponse отправляется сервером со списком завершенных игр.
type FinishedGamesResponse struct {
Games *[]model.FinishGameSnapshot `json:"games"`
}
// FinishedGameResponse отправляется сервером с информацией о конкретной завершенной игре.
type FinishedGameResponse struct {
Game *model.FinishGameSnapshot `json:"game"`
}

View File

@@ -1,342 +0,0 @@
package player
import (
"fmt"
"math/rand"
"net"
b "tic-tac-toe/board"
g "tic-tac-toe/game"
"tic-tac-toe/network"
"time"
)
// Структура для представления игрока-компьютера
type ComputerPlayer struct {
Figure b.BoardField `json:"figure"`
Difficulty g.Difficulty `json:"difficulty"`
rand *rand.Rand
}
// Создаем нового игрока-компьютера с заданным уровнем сложности
func NewComputerPlayer(
figure b.BoardField,
difficulty g.Difficulty,
) *ComputerPlayer {
source := rand.NewSource(time.Now().UnixNano())
return &ComputerPlayer{
Figure: figure,
Difficulty: difficulty,
rand: rand.New(source),
}
}
func (p *ComputerPlayer) GetSymbol() string {
if p.Figure == b.Cross {
return "X"
}
return "O"
}
func (p *ComputerPlayer) SendMessage(msg *network.Message) {
}
func (p *ComputerPlayer) GetNickname() string {
return "Computer"
}
func (p *ComputerPlayer) SwitchPlayer() {
if p.Figure == b.Cross {
p.Figure = b.Nought
} else {
p.Figure = b.Cross
}
}
func (p *ComputerPlayer) GetFigure() b.BoardField {
return p.Figure
}
func (p *ComputerPlayer) IsComputer() bool {
return true
}
// Реализуем ход компьютера в зависимости от выбранной сложности
func (p *ComputerPlayer) MakeMove(board *b.Board) (int, int, bool) {
fmt.Printf("%s (Computer) making move... ", p.GetSymbol())
var row, col int
switch p.Difficulty {
case g.Easy:
row, col = p.makeEasyMove(board)
case g.Medium:
row, col = p.makeMediumMove(board)
case g.Hard:
row, col = p.makeHardMove(board)
}
fmt.Printf("Move made (%d, %d)\n", row+1, col+1)
return row, col, true
}
// Легкий уровень: случайный ход на свободную клетку
func (p *ComputerPlayer) makeEasyMove(board *b.Board) (int, int) {
emptyCells := p.getEmptyCells(board)
if len(emptyCells) == 0 {
return -1, -1
}
// Выбираем случайную свободную клетку
randomIndex := p.rand.Intn(len(emptyCells))
return emptyCells[randomIndex][0], emptyCells[randomIndex][1]
}
func (p *ComputerPlayer) CheckSocket(conn net.Conn) bool {
return false
}
// Средний уровень: проверяет возможность выигрыша
// или блокировки выигрыша противника
func (p *ComputerPlayer) makeMediumMove(board *b.Board) (int, int) {
// Проверяем, можем ли мы выиграть за один ход
if move := p.findWinningMove(board, p.Figure); move != nil {
return move[0], move[1]
}
// Проверяем, нужно ли блокировать победу противника
opponentFigure := b.Cross
if p.Figure == b.Cross {
opponentFigure = b.Nought
}
if move := p.findWinningMove(board, opponentFigure); move != nil {
return move[0], move[1]
}
// Занимаем центр, если свободен (хорошая стратегия)
center := board.Size / 2
if board.Board[center][center] == b.Empty {
return center, center
}
// Занимаем угол, если свободен
corners := [][]int{
{0, 0},
{0, board.Size - 1},
{board.Size - 1, 0},
{board.Size - 1, board.Size - 1},
}
for _, corner := range corners {
if board.Board[corner[0]][corner[1]] == b.Empty {
return corner[0], corner[1]
}
}
// Если нет лучшего хода, делаем случайный ход
return p.makeEasyMove(board)
}
// Сложный уровень: использует алгоритм минимакс для оптимального хода
func (p *ComputerPlayer) makeHardMove(board *b.Board) (int, int) {
// Если доска пустая, ходим в центр или угол (оптимальный первый ход)
emptyCells := p.getEmptyCells(board)
if len(emptyCells) == board.Size*board.Size {
// Первый ход - центр или угол
center := board.Size / 2
return center, center
}
// Используем минимакс для доски 3x3
// Для больших досок это слишком ресурсоемко
if board.Size <= 3 {
bestScore := -1000
bestMove := []int{-1, -1}
// Создаем канал для результатов
type moveResult struct {
move []int
score int
}
resultChan := make(chan moveResult, len(emptyCells))
// Запускаем горутину для каждого возможного хода
for _, cell := range emptyCells {
go func(cell []int) {
row, col := cell[0], cell[1]
// Копируем доску чтобы избежать гонок данных
boardCopy := p.copyBoard(board)
// Пробуем сделать ход
boardCopy.Board[row][col] = p.Figure
// Вычисляем оценку хода через минимакс
score := p.minimax(boardCopy, 0, false)
// Отправляем результат в канал
resultChan <- moveResult{
move: []int{row, col},
score: score,
}
}(cell)
}
// Собираем результаты всех горутин
for i := 0; i < len(emptyCells); i++ {
result := <-resultChan
if result.score > bestScore {
bestScore = result.score
bestMove = result.move
}
}
return bestMove[0], bestMove[1]
}
// Для больших досок выбираем случайно одну из трех параллельных стратегий
strategyChoice := p.rand.Intn(3)
switch strategyChoice {
case 0:
fmt.Println("Using limited-depth parallel minimax strategy")
return p.makeLimitedDepthMinimax(board)
case 1:
fmt.Println("Using parallel heuristic evaluation strategy")
return p.makeParallelHeuristicMove(board)
case 2:
fmt.Println("Using zone-based parallel analysis strategy")
return p.makeZoneBasedMove(board)
default:
//В случае ошибки используем стратегию среднего уровня
return p.makeMediumMove(board)
}
}
// Алгоритм минимакс для определения оптимального хода
func (p *ComputerPlayer) minimax(
board *b.Board,
depth int, isMaximizing bool,
) int {
opponentFigure := b.Cross
if p.Figure == b.Cross {
opponentFigure = b.Nought
}
// Проверяем терминальное состояние
if board.CheckWin(p.Figure) {
return 10 - depth // Выигрыш, чем быстрее, тем лучше
} else if board.CheckWin(opponentFigure) {
return depth - 10 // Проигрыш, чем дольше, тем лучше
} else if board.CheckDraw() {
return 0 // Ничья
}
emptyCells := p.getEmptyCells(board)
if isMaximizing {
bestScore := -1000
// Проходим по всем свободным клеткам
for _, cell := range emptyCells {
row, col := cell[0], cell[1]
// Делаем ход
board.Board[row][col] = p.Figure
// Рекурсивно оцениваем ход
score := p.minimax(board, depth+1, false)
// Отменяем ход
board.Board[row][col] = b.Empty
bestScore = max(score, bestScore)
}
return bestScore
} else {
bestScore := 1000
// Проходим по всем свободным клеткам
for _, cell := range emptyCells {
row, col := cell[0], cell[1]
// Делаем ход противника
board.Board[row][col] = opponentFigure
// Рекурсивно оцениваем ход
score := p.minimax(board, depth+1, true)
// Отменяем ход
board.Board[row][col] = b.Empty
bestScore = min(score, bestScore)
}
return bestScore
}
}
// Вспомогательная функция для поиска хода, приводящего к выигрышу
func (p *ComputerPlayer) findWinningMove(
board *b.Board,
figure b.BoardField,
) []int {
for _, cell := range p.getEmptyCells(board) {
row, col := cell[0], cell[1]
// Пробуем сделать ход
board.Board[row][col] = figure
// Проверяем, приведет ли этот ход к выигрышу
if board.CheckWin(figure) {
// Отменяем ход и возвращаем координаты
board.Board[row][col] = b.Empty
return []int{row, col}
}
// Отменяем ход
board.Board[row][col] = b.Empty
}
return nil // Нет выигрышного хода
}
// Получение списка пустых клеток
func (p *ComputerPlayer) getEmptyCells(board *b.Board) [][]int {
var emptyCells [][]int
for i := 0; i < board.Size; i++ {
for j := 0; j < board.Size; j++ {
if board.Board[i][j] == b.Empty {
emptyCells = append(emptyCells, []int{i, j})
}
}
}
return emptyCells
}
// Вспомогательные функции max и min
func max(a, b int) int {
if a > b {
return a
}
return b
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
// Копирование доски для избежания гонок данных при параллельном вычислении
func (p *ComputerPlayer) copyBoard(board *b.Board) *b.Board {
newBoard := b.NewBoard(board.Size)
for i := 0; i < board.Size; i++ {
for j := 0; j < board.Size; j++ {
newBoard.Board[i][j] = board.Board[i][j]
}
}
return newBoard
}

View File

@@ -1,32 +0,0 @@
package player
import (
"net"
b "tic-tac-toe/board"
"tic-tac-toe/network"
)
// Интерфейс для любого игрока, будь то человек или компьютер
type IPlayer interface {
// Получение символа игрока (X или O)
GetSymbol() string
// Переключение хода на другого игрока
SwitchPlayer()
SendMessage(msg *network.Message)
GetNickname() string
// Получение текущей фигуры игрока
GetFigure() b.BoardField
// Выполнение хода игрока
// Возвращает координаты хода (x, y) и признак успешности
MakeMove(board *b.Board) (int, int, bool)
// Проверка, является ли игрок компьютером
IsComputer() bool
CheckSocket(conn net.Conn) bool
}

View File

@@ -1,95 +0,0 @@
package player
import (
"encoding/json"
"fmt"
"net"
"strconv"
"strings"
b "tic-tac-toe/board"
"tic-tac-toe/network"
)
// Структура для представления игрока-человека
type HumanPlayer struct {
Figure b.BoardField `json:"figure"`
Nickname string `json:"nickname"`
Conn *net.Conn `json:"-"`
}
func NewHumanPlayer(
nickname string, conn *net.Conn,
) *HumanPlayer {
return &HumanPlayer{Figure: b.Cross, Nickname: nickname, Conn: conn}
}
func (p *HumanPlayer) CheckSocket(conn net.Conn) bool {
return *p.Conn == conn
}
// Возвращаем символ игрока
func (p *HumanPlayer) GetSymbol() string {
if p.Figure == b.Cross {
return "X"
}
return "O"
}
func (p *HumanPlayer) SendMessage(msg *network.Message) {
json.NewEncoder(*p.Conn).Encode(msg)
}
func (p *HumanPlayer) GetNickname() string {
return p.Nickname
}
// Изменяем фигуру текущего игрока
func (p *HumanPlayer) SwitchPlayer() {
if p.Figure == b.Cross {
p.Figure = b.Nought
} else {
p.Figure = b.Cross
}
}
// Возвращаем текущую фигуру игрока
func (p *HumanPlayer) GetFigure() b.BoardField {
return p.Figure
}
// Метод-заглушка, т.к. ввод игрока осуществляется на
// уровне пакета game, где нужно еще отрабатывать
// команду на выход и сохранение игровой сессии
func (p *HumanPlayer) MakeMove(board *b.Board) (int, int, bool) {
return -1, -1, false
}
// Обрабатываем строку ввода и
// преобразуем ее в координаты хода
func (p *HumanPlayer) ParseMove(
input string,
board *b.Board,
) (int, int, bool) {
parts := strings.Fields(input)
if len(parts) != 2 {
fmt.Println("Invalid input. Please try again.")
return -1, -1, false
}
row, err1 := strconv.Atoi(parts[0])
col, err2 := strconv.Atoi(parts[1])
if err1 != nil || err2 != nil ||
row < 1 || col < 1 || row > board.Size ||
col > board.Size {
fmt.Println("Invalid input. Please try again.")
return -1, -1, false
}
// Преобразуем введенные координаты (начиная с 1)
// в индексы массива (начиная с 0)
return row - 1, col - 1, true
}
func (p *HumanPlayer) IsComputer() bool {
return false
}

View File

@@ -1,169 +0,0 @@
package player
import (
"sync"
b "tic-tac-toe/board"
)
// Метод запуска параллельной эвристической оценки ходов
func (p *ComputerPlayer) makeParallelHeuristicMove(board *b.Board) (int, int) {
bestScore := -100000
var bestMove []int
emptyCells := p.getEmptyCells(board)
if len(emptyCells) == 0 {
return -1, -1
}
if len(emptyCells) == 1 {
return emptyCells[0][0], emptyCells[0][1]
}
// Создаем канал для результатов
type moveResult struct {
move []int
score int
}
resultChan := make(chan moveResult, len(emptyCells))
var wg sync.WaitGroup
// Запускаем горутины для каждого возможного хода
for _, cell := range emptyCells {
wg.Add(1)
go func(r, c int) {
defer wg.Done()
boardCopy := p.copyBoard(board)
boardCopy.Board[r][c] = p.Figure
score := p.evaluateBoardHeuristic(boardCopy, p.Figure)
resultChan <- moveResult{move: []int{r, c}, score: score}
}(cell[0], cell[1])
}
wg.Wait()
close(resultChan)
// Определяем лучший ход
for result := range resultChan {
if result.score > bestScore {
bestScore = result.score
bestMove = result.move
}
}
if bestMove == nil {
// Если по какой-то причине лучший ход не найден (маловероятно)
// переходи на стратегию поведения среднего уровня сложности
return p.makeMediumMove(board)
}
return bestMove[0], bestMove[1]
}
// Эвристическая оценка доски
// Количество рядов, столбцов или диагоналей, где у игрока есть N фигур
// и остальные клетки пусты. Также учитываем блокировку противника.
func (p *ComputerPlayer) evaluateBoardHeuristic(
board *b.Board, player b.BoardField,
) int {
score := 0
opponent := b.Cross
if player == b.Cross {
opponent = b.Nought
}
// Оценка за почти выигрышные линии для игрока
// Почти выигрыш
score += p.countPotentialLines(board, player, board.Size-1) * 100
// Две фигуры в ряд (для Size > 2)
score += p.countPotentialLines(board, player, board.Size-2) * 10
// Штраф за почти выигрышные линии для оппонента (блокировка)
// Блокировка почти выигрыша оппонента
score -= p.countPotentialLines(board, opponent, board.Size-1) * 90
// Блокировка двух фигур оппонента
score -= p.countPotentialLines(board, opponent, board.Size-2) * 5
// Бонус за занятие центра (особенно на нечетных досках)
if board.Size%2 == 1 {
center := board.Size / 2
if board.Board[center][center] == player {
score += 5
} else if board.Board[center][center] == opponent {
score -= 5
}
}
return score
}
// Вспомогательная функция для подсчета потенциальных линий
func (p *ComputerPlayer) countPotentialLines(
board *b.Board, player b.BoardField, numPlayerSymbols int,
) int {
count := 0
lineSize := board.Size
// Проверка строк
for r := 0; r < lineSize; r++ {
playerSymbols := 0
emptySymbols := 0
for c := 0; c < lineSize; c++ {
if board.Board[r][c] == player {
playerSymbols++
} else if board.Board[r][c] == b.Empty {
emptySymbols++
}
}
if playerSymbols == numPlayerSymbols &&
(playerSymbols+emptySymbols) == lineSize {
count++
}
}
// Проверка столбцов
for c := 0; c < lineSize; c++ {
playerSymbols := 0
emptySymbols := 0
for r := 0; r < lineSize; r++ {
if board.Board[r][c] == player {
playerSymbols++
} else if board.Board[r][c] == b.Empty {
emptySymbols++
}
}
if playerSymbols == numPlayerSymbols &&
(playerSymbols+emptySymbols) == lineSize {
count++
}
}
// Проверка главной диагонали
playerSymbolsDiag1 := 0
emptySymbolsDiag1 := 0
for i := 0; i < lineSize; i++ {
if board.Board[i][i] == player {
playerSymbolsDiag1++
} else if board.Board[i][i] == b.Empty {
emptySymbolsDiag1++
}
}
if playerSymbolsDiag1 == numPlayerSymbols &&
(playerSymbolsDiag1+emptySymbolsDiag1) == lineSize {
count++
}
// Проверка побочной диагонали
playerSymbolsDiag2 := 0
emptySymbolsDiag2 := 0
for i := 0; i < lineSize; i++ {
if board.Board[i][lineSize-1-i] == player {
playerSymbolsDiag2++
} else if board.Board[i][lineSize-1-i] == b.Empty {
emptySymbolsDiag2++
}
}
if playerSymbolsDiag2 == numPlayerSymbols &&
(playerSymbolsDiag2+emptySymbolsDiag2) == lineSize {
count++
}
return count
}

View File

@@ -1,114 +0,0 @@
package player
import (
"sync"
b "tic-tac-toe/board"
)
const maxDepth = 2 // Ограничение глубины для минимакса
// Метод запуска стратегии с ограничением глубины для минимакса
func (p *ComputerPlayer) makeLimitedDepthMinimax(board *b.Board) (int, int) {
bestScore := -100000
var bestMove []int
emptyCells := p.getEmptyCells(board)
if len(emptyCells) == 0 {
return -1, -1 // Нет доступных ходов
}
if len(emptyCells) == 1 {
return emptyCells[0][0], emptyCells[0][1] // Единственный возможный ход
}
// Создаем канал для результатов
type moveResult struct {
move []int
score int
}
resultChan := make(chan moveResult, len(emptyCells))
var wg sync.WaitGroup
// Запускаем горутины для каждого возможного хода
for _, cell := range emptyCells {
wg.Add(1)
go func(r, c int) {
defer wg.Done()
boardCopy := p.copyBoard(board)
boardCopy.Board[r][c] = p.Figure
score := p.minimaxRecursive(boardCopy, 0, false, maxDepth)
resultChan <- moveResult{move: []int{r, c}, score: score}
}(cell[0], cell[1])
}
wg.Wait() // Ждем завершения всех горутин
close(resultChan) // Закрываем канал
// Определяем лучший ход
for result := range resultChan {
if result.score > bestScore {
bestScore = result.score
bestMove = result.move
}
}
if bestMove == nil {
// Если по какой-то причине лучший ход не найден (маловероятно)
// переходи на стратегию поведения среднего уровня сложности
return p.makeMediumMove(board)
}
return bestMove[0], bestMove[1]
}
// Рекурсивная часть минимакса с ограничением глубины
func (p *ComputerPlayer) minimaxRecursive(
board *b.Board, depth int, isMaximizing bool,
maxDepthLimit int,
) int {
opponentFigure := b.Cross
if p.Figure == b.Cross {
opponentFigure = b.Nought
}
if board.CheckWin(p.Figure) {
return 10 - depth // Выигрыш текущего игрока
}
if board.CheckWin(opponentFigure) {
return depth - 10 // Проигрыш текущего игрока (выигрыш оппонента)
}
if board.CheckDraw() {
return 0 // Ничья
}
if depth >= maxDepthLimit { // Ограничение глубины
// Если достигнута максимальная глубина, используем эвристическую оценку
return p.evaluateBoardHeuristic(board, p.Figure)
}
emptyCells := p.getEmptyCells(board)
if isMaximizing {
bestScore := -100000
for _, cell := range emptyCells {
boardCopy := p.copyBoard(board)
boardCopy.Board[cell[0]][cell[1]] = p.Figure
score := p.minimaxRecursive(
boardCopy, depth+1, false, maxDepthLimit,
)
bestScore = max(bestScore, score)
}
return bestScore
} else {
bestScore := 100000
// opponentFigure уже определен выше
for _, cell := range emptyCells {
boardCopy := p.copyBoard(board)
boardCopy.Board[cell[0]][cell[1]] = opponentFigure
score := p.minimaxRecursive(
boardCopy, depth+1, true, maxDepthLimit,
)
bestScore = min(bestScore, score)
}
return bestScore
}
}

View File

@@ -1,121 +0,0 @@
package player
import (
b "tic-tac-toe/board"
)
// Параллельный анализ на основе зон
func (p *ComputerPlayer) makeZoneBasedMove(board *b.Board) (int, int) {
// Если доска не очень большая, используем эвристику
if board.Size <= 5 { // Пороговое значение, можно настроить
return p.makeParallelHeuristicMove(board)
}
bestScore := -100000
var bestMove []int
emptyCells := p.getEmptyCells(board)
if len(emptyCells) == 0 {
return -1, -1
}
if len(emptyCells) == 1 {
return emptyCells[0][0], emptyCells[0][1]
}
// Определяем размер зоны (например, 3x3)
zoneSize := 3
if board.Size < zoneSize {
zoneSize = board.Size // Если доска меньше зоны, зона равна доске
}
type moveResult struct {
move []int
score int
}
// Используем буферизированный канал, чтобы не блокировать горутины,
// если основная горутина не успевает обрабатывать результаты
// Размер канала равен количеству пустых клеток,
// т.к. для каждой может быть запущена горутина
resultChan := make(chan moveResult, len(emptyCells))
numZonesToProcess := 0 // Счетчик для корректного ожидания
for _, cell := range emptyCells {
numZonesToProcess++
// Запускаем горутину для каждой пустой клетки
go func(centerCell []int) {
localBestScore := -100000
var localBestMove []int
// Определяем границы зоны вокруг centerCell
minRow := max(0, centerCell[0]-zoneSize/2)
maxRow := min(board.Size-1, centerCell[0]+zoneSize/2)
minCol := max(0, centerCell[1]-zoneSize/2)
maxCol := min(board.Size-1, centerCell[1]+zoneSize/2)
// Ищем ходы в зоне
foundMoveInZone := false
for r := minRow; r <= maxRow; r++ {
for c := minCol; c <= maxCol; c++ {
// Если найден пустая клетка в зоне
if board.Board[r][c] == b.Empty {
foundMoveInZone = true
boardCopy := p.copyBoard(board)
boardCopy.Board[r][c] = p.Figure
// Оцениваем ход испо
score := p.evaluateBoardHeuristic(boardCopy, p.Figure)
// Если найден лучший ход
if score > localBestScore {
localBestScore = score
localBestMove = []int{r, c}
}
}
}
}
// Если найден лучший ход в зоне
if foundMoveInZone && localBestMove != nil {
resultChan <- moveResult{
move: localBestMove, score: localBestScore,
}
} else if !foundMoveInZone &&
board.Board[centerCell[0]][centerCell[1]] == b.Empty {
// Если зона вокруг centerCell не содержит других
// пустых клеток, но сама centerCell пуста –
// оцениваем ход в centerCell
boardCopy := p.copyBoard(board)
boardCopy.Board[centerCell[0]][centerCell[1]] = p.Figure
score := p.evaluateBoardHeuristic(boardCopy, p.Figure)
resultChan <- moveResult{move: centerCell, score: score}
} else {
// Если не найдено ходов в зоне или centerCell не пуста
// (не должно случиться, если итерируем по emptyCells),
// отправляем фиктивный результат,
// чтобы не блокировать ожидание.
// Этого не должно происходить в нормальном потоке.
resultChan <- moveResult{move: nil, score: -200000}
}
}(cell)
}
// Ожидаем завершения всех горутин
processedGoroutines := 0 // Счетчик для корректного ожидания
for processedGoroutines < numZonesToProcess {
result := <-resultChan
processedGoroutines++
// Если найден лучший ход
if result.move != nil && result.score > bestScore {
bestScore = result.score
bestMove = result.move
}
}
if bestMove == nil {
// Если по какой-то причине лучший ход не найден (маловероятно)
// переходи на стратегию поведения среднего уровня сложности
return p.makeMediumMove(board)
}
// Возвращаем лучший ход
return bestMove[0], bestMove[1]
}

View File

@@ -1,246 +0,0 @@
package room
import (
"encoding/json"
"log"
"math/rand"
b "tic-tac-toe/board"
db "tic-tac-toe/database"
g "tic-tac-toe/game"
"tic-tac-toe/model"
n "tic-tac-toe/network"
p "tic-tac-toe/player"
"time"
)
// Room manages the state of a single game room.
type Room struct {
Name string
Board *b.Board
Player1 p.IPlayer
Player2 p.IPlayer
CurrentPlayer p.IPlayer
State g.GameState
repository db.IRepository
Mode g.GameMode
// Уровень сложности компьютера (только для PvC)
Difficulty g.Difficulty
}
// NewRoom creates a new game room.
func NewRoom(
name string, repository db.IRepository, boardSize int,
gameMode g.GameMode, difficulty g.Difficulty,
) *Room {
room := &Room{
Name: name,
repository: repository,
Mode: gameMode,
Difficulty: difficulty,
Board: b.NewBoard(boardSize),
State: g.WaitingOpponent,
}
if gameMode == g.PvC {
room.Player2 = p.NewComputerPlayer(b.Nought, difficulty)
}
return room
}
func (r *Room) IsFull() bool {
return r.Player1 != nil && r.Player2 != nil
}
func (r *Room) PlayersAmount() int {
if r.Player1 != nil && r.Player2 != nil {
return 2
}
return 1
}
func (r *Room) BoardSize() int {
return r.Board.Size
}
func (r *Room) AddPlayer(player p.IPlayer) {
if r.Player1 == nil {
r.Player1 = player
if r.Player1.GetSymbol() != "X" {
r.Player1.SwitchPlayer()
}
} else if r.Player2 == nil {
r.Player2 = player
if r.Player2.GetSymbol() != "O" {
r.Player2.SwitchPlayer()
}
}
}
func (r *Room) RemovePlayer(player p.IPlayer) {
if r.Player1 == player {
r.Player1 = nil
if !r.Player2.IsComputer() && r.Player2 != nil {
opponentLeft := &n.OpponentLeft{Nickname: player.GetNickname()}
payloadBytes, err := json.Marshal(opponentLeft)
if err != nil {
log.Printf("Error marshaling OpponentLeft: %v", err)
return
}
msg := &n.Message{
Cmd: n.CmdOpponentLeft,
Payload: payloadBytes,
}
r.Player2.SendMessage(msg)
}
} else if r.Player2 == player {
r.Player2 = nil
if !r.Player1.IsComputer() && r.Player1 != nil {
opponentLeft := &n.OpponentLeft{Nickname: player.GetNickname()}
payloadBytes, err := json.Marshal(opponentLeft)
if err != nil {
log.Printf("Error marshaling OpponentLeft: %v", err)
return
}
msg := &n.Message{
Cmd: n.CmdOpponentLeft,
Payload: payloadBytes,
}
r.Player1.SendMessage(msg)
}
}
}
func (r *Room) InitGame() {
if !r.IsFull() {
return
}
randomPlayer := []b.BoardField{b.Cross, b.Nought}
if !r.Board.IsEmpty() {
r.Board = b.NewBoard(r.Board.Size)
}
msg := &n.Message{Cmd: n.CmdInitGame}
initGamePayload := &n.InitGameResponse{
Board: *r.Board,
}
switch randomPlayer[rand.Intn(len(randomPlayer))] {
case b.Cross:
r.State = g.CrossStep
initGamePayload.CurrentPlayer = b.Cross
// подготовка сообщения
case b.Nought:
r.State = g.NoughtStep
initGamePayload.CurrentPlayer = b.Nought
// подготовка сообщения
}
if r.Mode == g.PvC {
if r.State == g.CrossStep {
r.CurrentPlayer = r.Player1
} else if r.State == g.NoughtStep {
r.CurrentPlayer = r.Player2
}
}
payloadBytes, err := json.Marshal(initGamePayload)
if err != nil {
log.Printf("Error marshaling InitGameResponse for Player1 after Player2 left: %v", err)
return
}
msg.Payload = payloadBytes
r.Player1.SendMessage(msg)
r.Player2.SendMessage(msg)
if r.CurrentPlayer.IsComputer() {
row, col, _ := r.CurrentPlayer.MakeMove(r.Board)
r.PlayerStep(r.CurrentPlayer, row, col)
}
}
// Переключаем активного игрока
func (r *Room) switchCurrentPlayer() {
if r.CurrentPlayer == r.Player1 {
r.CurrentPlayer = r.Player2
} else {
r.CurrentPlayer = r.Player1
}
}
func (r *Room) PlayerStep(player p.IPlayer, row, col int) {
msg := &n.Message{}
if r.State != g.CrossStep && r.State != g.NoughtStep {
return
}
// проверяем, что ход делает текущий игрок
if player != r.CurrentPlayer {
return
}
r.Board.SetSymbol(row, col, r.CurrentPlayer.GetFigure())
if r.Board.CheckWin(r.CurrentPlayer.GetFigure()) {
if r.CurrentPlayer.GetFigure() == b.Cross {
r.State = g.CrossWin
} else {
r.State = g.NoughtWin
}
msg.Cmd = n.CmdEndGame
endGamePayload := &n.EndGameResponse{
Board: *r.Board,
CurrentPlayer: r.CurrentPlayer.GetFigure(),
}
msg.Payload, _ = json.Marshal(endGamePayload)
figureWinner := r.CurrentPlayer.GetFigure()
winnerNickName := r.CurrentPlayer.GetNickname()
if r.CurrentPlayer == r.Player1 {
r.CurrentPlayer = r.Player2
} else {
r.CurrentPlayer = r.Player1
}
anotherPlayerNickName := r.CurrentPlayer.GetNickname()
r.repository.SaveFinishedGame(&model.FinishGameSnapshot{
Board: r.Board,
PlayerFigure: figureWinner,
WinnerName: winnerNickName,
AnotherPlayerName: anotherPlayerNickName,
Time: time.Now(),
})
} else if r.Board.CheckDraw() {
r.State = g.Draw
msg.Cmd = n.CmdEndGame
endGamePayload := &n.EndGameResponse{
Board: *r.Board,
CurrentPlayer: b.Empty,
}
msg.Payload, _ = json.Marshal(endGamePayload)
} else {
if r.CurrentPlayer.GetFigure() == b.Cross {
r.State = g.NoughtStep
} else {
r.State = g.CrossStep
}
r.switchCurrentPlayer()
msg.Cmd = n.CmdUpdateState
stateUpdatePayload := &n.GameStateUpdate{
Board: *r.Board,
CurrentPlayer: r.CurrentPlayer.GetFigure(),
}
msg.Payload, _ = json.Marshal(stateUpdatePayload)
}
r.Player1.SendMessage(msg)
r.Player2.SendMessage(msg)
if r.State == g.CrossWin || r.State == g.NoughtWin || r.State == g.Draw {
time.Sleep(10 * time.Second)
r.InitGame()
return
}
if r.CurrentPlayer.IsComputer() {
row, col, _ := r.CurrentPlayer.MakeMove(r.Board)
r.PlayerStep(r.CurrentPlayer, row, col)
}
}

View File

@@ -1,197 +0,0 @@
package server
import (
"encoding/json"
"log"
"net"
"strconv"
"tic-tac-toe/network"
p "tic-tac-toe/player"
)
var defaultPlayerCounts int = 0
func (s *Server) handleCommand(client net.Conn, msg *network.Message) {
log.Printf(
"Received command '%s' from %s",
msg.Cmd, client.RemoteAddr(),
)
switch msg.Cmd {
case network.CmdNickname:
s.nickNameHandler(client, msg)
case network.CmdMakeMoveRequest:
s.makeMoveHandler(client, msg)
case network.CmdListRoomsRequest:
s.listRoomsHandler(client, msg)
case network.CmdJoinRoomRequest:
s.joinRoomHandler(client, msg)
case network.CmdLeaveRoomRequest:
s.leaveRoomHandler(client, msg)
case network.CmdFinishedGamesResponse:
s.getFinishedGamesHandler(client, msg)
case network.CmdFinishedGameByIdRequest:
s.getFinishedGameByIdHandler(client, msg)
default:
log.Printf("Unknown command: %s", msg.Cmd)
}
}
func (s *Server) nickNameHandler(client net.Conn, msg *network.Message) {
nicknameRequest := &network.NicknameRequest{}
if err := json.Unmarshal(msg.Payload, nicknameRequest); err != nil {
log.Printf("Error unmarshaling NicknameRequest: %v", err)
return
}
if s.players[nicknameRequest.Nickname] != nil {
nicknameRequest.Nickname = nicknameRequest.Nickname +
"_" + strconv.Itoa(defaultPlayerCounts)
defaultPlayerCounts++
}
s.players[nicknameRequest.Nickname] = p.NewHumanPlayer(
nicknameRequest.Nickname, &client,
)
response := &network.NickNameResponse{
Nickname: nicknameRequest.Nickname,
}
msg.Payload, _ = json.Marshal(response)
msg.Cmd = network.CmdNickNameResponse
json.NewEncoder(client).Encode(msg)
}
func (s *Server) joinRoomHandler(client net.Conn, msg *network.Message) {
joinRoomRequest := &network.JoinRoomRequest{}
if err := json.Unmarshal(msg.Payload, joinRoomRequest); err != nil {
log.Printf("Error unmarshaling JoinRoomRequest: %v", err)
return
}
room, okRoom := s.rooms[joinRoomRequest.RoomName]
player, okPlayer := s.players[joinRoomRequest.PlayerName]
if !okRoom || !okPlayer {
response := &network.ErrorResponse{Message: "Room not found"}
msg.Cmd = network.CmdError
msg.Payload, _ = json.Marshal(response)
json.NewEncoder(client).Encode(msg)
return
}
s.mutex.Lock()
if room.IsFull() {
s.mutex.Unlock()
response := &network.ErrorResponse{Message: "Room is full"}
msg.Cmd = network.CmdError
msg.Payload, _ = json.Marshal(response)
json.NewEncoder(client).Encode(msg)
return
}
room.AddPlayer(player)
s.mutex.Unlock()
response := &network.RoomJoinResponse{
RoomName: joinRoomRequest.RoomName,
PlayerSymbol: player.GetFigure(),
Board: *room.Board,
}
msg.Payload, _ = json.Marshal(response)
msg.Cmd = network.CmdRoomJoinResponse
json.NewEncoder(client).Encode(msg)
room.InitGame()
}
func (s *Server) leaveRoomHandler(client net.Conn, msg *network.Message) {
leaveRoomRequest := &network.LeaveRoomRequest{}
if err := json.Unmarshal(msg.Payload, leaveRoomRequest); err != nil {
log.Printf("Error unmarshaling LeaveRoomRequest: %v", err)
return
}
room, okRoom := s.rooms[leaveRoomRequest.RoomName]
player, okPlayer := s.players[leaveRoomRequest.PlayerName]
if !okRoom || !okPlayer {
response := &network.ErrorResponse{Message: "Room not found"}
msg.Cmd = network.CmdError
msg.Payload, _ = json.Marshal(response)
json.NewEncoder(client).Encode(msg)
return
}
s.mutex.Lock()
room.RemovePlayer(player)
s.mutex.Unlock()
}
func (s *Server) listRoomsHandler(client net.Conn, msg *network.Message) {
s.mutex.Lock()
defer s.mutex.Unlock()
var roomInfos []network.RoomInfo
for _, room := range s.rooms {
roomInfos = append(roomInfos, network.RoomInfo{
Name: room.Name,
BoardSize: room.BoardSize(),
IsFull: room.IsFull(),
GameMode: room.Mode,
Difficult: room.Difficulty,
})
}
response := &network.RoomListResponse{
Rooms: roomInfos,
}
msg.Cmd = network.CmdRoomListResponse
msg.Payload, _ = json.Marshal(response)
json.NewEncoder(client).Encode(msg)
}
func (s *Server) getFinishedGamesHandler(client net.Conn, msg *network.Message) {
// получаем данные из БД
finishedGames, err := s.repository.GetAllFinishedGames()
if err != nil {
response := &network.ErrorResponse{Message: "Error getting finished games"}
msg.Cmd = network.CmdError
msg.Payload, _ = json.Marshal(response)
json.NewEncoder(client).Encode(msg)
return
}
response := &network.FinishedGamesResponse{
Games: finishedGames,
}
msg.Cmd = network.CmdFinishedGamesResponse
msg.Payload, _ = json.Marshal(response)
json.NewEncoder(client).Encode(msg)
}
func (s *Server) getFinishedGameByIdHandler(client net.Conn, msg *network.Message) {
getFinishedGameByIdRequest := &network.GetFinishedGameByIdRequest{}
if err := json.Unmarshal(msg.Payload, getFinishedGameByIdRequest); err != nil {
log.Printf("Error unmarshaling GetFinishedGameByIdRequest: %v", err)
return
}
finishedGame, err := s.repository.GetFinishedGameById(getFinishedGameByIdRequest.GameID)
if err != nil {
response := &network.ErrorResponse{Message: "Error getting finished game by id"}
json.NewEncoder(client).Encode(response)
return
}
response := &network.FinishedGameResponse{
Game: finishedGame,
}
json.NewEncoder(client).Encode(response)
}
func (s *Server) makeMoveHandler(client net.Conn, msg *network.Message) {
makeMoveRequest := &network.MakeMoveRequest{}
if err := json.Unmarshal(msg.Payload, makeMoveRequest); err != nil {
log.Printf("Error unmarshaling MakeMoveRequest: %v", err)
return
}
room, okRoom := s.rooms[makeMoveRequest.RoomName]
player, okPlayer := s.players[makeMoveRequest.PlayerName]
if !okRoom || !okPlayer {
response := &network.ErrorResponse{Message: "Room not found"}
msg.Cmd = network.CmdError
msg.Payload, _ = json.Marshal(response)
json.NewEncoder(client).Encode(msg)
return
}
room.PlayerStep(
player,
makeMoveRequest.PositionRow,
makeMoveRequest.PositionCol,
)
}

View File

@@ -1,116 +0,0 @@
package server
import (
"encoding/json"
"log"
"net"
"sync"
db "tic-tac-toe/database"
g "tic-tac-toe/game"
"tic-tac-toe/network"
"tic-tac-toe/player"
"tic-tac-toe/room"
)
// Server manages client connections and game rooms.
type Server struct {
listener net.Listener
repository db.IRepository
rooms map[string]*room.Room
players map[string]player.IPlayer
mutex sync.RWMutex
}
// NewServer creates and returns a new server instance.
func NewServer(addr string, repository db.IRepository) (*Server, error) {
listener, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
server := &Server{
listener: listener,
repository: repository,
rooms: make(map[string]*room.Room),
players: make(map[string]player.IPlayer),
}
server.rooms["room1"] = room.NewRoom(
"room1", server.repository, 3, g.PvP, g.None,
)
server.rooms["room2"] = room.NewRoom(
"room2", server.repository, 3, g.PvC, g.Easy,
)
server.rooms["room3"] = room.NewRoom(
"room3", server.repository, 3, g.PvC, g.Medium,
)
server.rooms["room4"] = room.NewRoom(
"room4", server.repository, 3, g.PvC, g.Hard,
)
return server, nil
}
// Start begins listening for and handling client connections.
func (s *Server) Start() {
log.Printf("Server started, listening on %s", s.listener.Addr())
defer s.listener.Close()
for {
conn, err := s.listener.Accept()
if err != nil {
log.Printf("Error accepting connection: %v", err)
continue
}
go s.handleConnection(conn)
}
}
// handleConnection manages a single client connection.
func (s *Server) handleConnection(conn net.Conn) {
log.Printf("New client connected: %s", conn.RemoteAddr())
defer conn.Close()
decoder := json.NewDecoder(conn)
for {
var msg network.Message
if err := decoder.Decode(&msg); err != nil {
log.Printf("Client %s disconnected: %v", conn.RemoteAddr(), err)
s.disconnectedClientHandler(conn)
return
}
s.handleCommand(conn, &msg)
}
}
func (s *Server) disconnectedClientHandler(conn net.Conn) {
var player player.IPlayer
for _, room := range s.rooms {
if room.Player1 != nil {
if room.Player1.CheckSocket(conn) {
player = room.Player1
room.RemovePlayer(room.Player1)
break
}
}
if room.Player2 != nil {
if room.Player2.CheckSocket(conn) {
player = room.Player2
room.RemovePlayer(room.Player2)
break
}
}
}
if player == nil {
log.Printf(
"Client %s disconnected: player not found",
conn.RemoteAddr(),
)
return
}
s.mutex.Lock()
delete(s.players, player.GetNickname())
s.mutex.Unlock()
}

Binary file not shown.