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:
14
part_8/tic_tac_toe_v8/.vscode/launch.json
vendored
14
part_8/tic_tac_toe_v8/.vscode/launch.json
vendored
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "Launch Package",
|
|
||||||
"type": "go",
|
|
||||||
"request": "launch",
|
|
||||||
"mode": "auto",
|
|
||||||
"program": "${fileDirname}", // <- ставим запятую
|
|
||||||
"console": "integratedTerminal"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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 "?"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
type State int
|
|
||||||
|
|
||||||
const (
|
|
||||||
waitNickNameConfirm State = iota
|
|
||||||
mainMenu
|
|
||||||
waitRoomJoin
|
|
||||||
playing
|
|
||||||
playerMove
|
|
||||||
opponentMove
|
|
||||||
endGame
|
|
||||||
waitingOpponentInRoom
|
|
||||||
waitResponseFromServer
|
|
||||||
)
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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")
|
|
||||||
)
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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"`
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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=
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"`
|
|
||||||
}
|
|
||||||
@@ -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"`
|
|
||||||
}
|
|
||||||
@@ -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"`
|
|
||||||
}
|
|
||||||
@@ -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"`
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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]
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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.
Reference in New Issue
Block a user