From d0f3bff57dc8d50a198bb45f8d446a5db423f944 Mon Sep 17 00:00:00 2001 From: Stanislav Chernyshev Date: Mon, 23 Jun 2025 17:12:57 +0300 Subject: [PATCH] =?UTF-8?q?=D1=80=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3=20=D0=B8=D0=B3=D1=80=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tic_tac_toe_v7/board/board_cell_type.go | 12 ++ part_8/tic_tac_toe_v7/client/client.go | 78 +++++++--- part_8/tic_tac_toe_v7/client/client_state.go | 9 +- part_8/tic_tac_toe_v7/client/menu.go | 96 ++++++++---- part_8/tic_tac_toe_v7/client/playing.go | 35 +++-- .../client/server_message_handlers.go | 132 ++++++++-------- part_8/tic_tac_toe_v7/client/utils.go | 47 +++++- .../network/server_to_client.go | 7 - part_8/tic_tac_toe_v7/room/room.go | 145 ++++++++++++------ .../server/client_message_handlers.go | 129 +++++++++++++--- part_8/tic_tac_toe_v7/server/server.go | 58 +++++-- 11 files changed, 518 insertions(+), 230 deletions(-) diff --git a/part_8/tic_tac_toe_v7/board/board_cell_type.go b/part_8/tic_tac_toe_v7/board/board_cell_type.go index 1beb472..4e444d8 100644 --- a/part_8/tic_tac_toe_v7/board/board_cell_type.go +++ b/part_8/tic_tac_toe_v7/board/board_cell_type.go @@ -8,3 +8,15 @@ const ( Cross Nought ) + +// Возвращаем строковое представление фигуры +func (f BoardField) String() string { + switch f { + case Cross: + return "X" + case Nought: + return "O" + default: + return " " + } +} diff --git a/part_8/tic_tac_toe_v7/client/client.go b/part_8/tic_tac_toe_v7/client/client.go index 0746e7b..23cb4be 100644 --- a/part_8/tic_tac_toe_v7/client/client.go +++ b/part_8/tic_tac_toe_v7/client/client.go @@ -12,20 +12,29 @@ import ( "tic-tac-toe/network" ) -// Client represents the client-side application. +// Объявление структуры клиента type Client struct { - conn net.Conn - board *b.Board - mySymbol b.BoardField + // подключение к серверу + conn net.Conn + // игровое поле + board *b.Board + // фигура игрока + mySymbol b.BoardField + // фигура игрока, ход которой сейчас currentPlayer b.BoardField - playerName string - roomName string - state State - mutex sync.RWMutex - lastMsgTime time.Time + // никнейм игрока + 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 { @@ -33,77 +42,98 @@ func NewClient(addr string) (*Client, error) { } return &Client{ - conn: conn, - state: waitNickNameConfirm, - mySymbol: b.Empty, // Will be set upon joining a room + // подключение к серверу + conn: conn, + // начальное состояние клиента + state: waitNickNameConfirm, + // mySymbol будет установлен при подключении к комнате + mySymbol: b.Empty, }, nil } +// Устанавливаем никнейм игрока func (c *Client) setNickname(nickname string) { c.playerName = nickname } +// Получаем текущее состояние клиента func (c *Client) getState() State { - c.mutex.RLock() + c.mutex.RLock() // защищаем доступ к данным defer c.mutex.RUnlock() return c.state } +// Устанавливаем текущее состояние клиента func (c *Client) setState(state State) { - c.mutex.Lock() + c.mutex.Lock() // защищаем доступ к данным defer c.mutex.Unlock() - // Display a message only when transitioning to opponentMove + // Если переходим в состояние opponentMove if state == opponentMove && c.state != opponentMove { fmt.Println("\nWaiting for opponent's move...") - } else if state == waitingOpponentInRoom && c.state != waitingOpponentInRoom { + } else if state == waitingOpponentInRoom && + c.state != waitingOpponentInRoom { + // Если переходим в состояние waitingOpponentInRoom fmt.Println("\nWaiting for opponent to join...") } - c.state = state + 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 { + 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 + return // если соединение потеряно, то выходим из горутины } 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: + default: // Если пришло неизвестное сообщение log.Printf( "Received unhandled message type '%s' "+ "from server. Payload: %s\n> ", diff --git a/part_8/tic_tac_toe_v7/client/client_state.go b/part_8/tic_tac_toe_v7/client/client_state.go index d13fff8..109bcea 100644 --- a/part_8/tic_tac_toe_v7/client/client_state.go +++ b/part_8/tic_tac_toe_v7/client/client_state.go @@ -3,13 +3,18 @@ package client type State int const ( + // ожидание подтверждения никнейма от сервера waitNickNameConfirm State = iota + // главное меню mainMenu - waitRoomJoin - playing + // ход игрока playerMove + // ход оппонента opponentMove + // конец игры endGame + // ожидание присоединения оппонента waitingOpponentInRoom + // ожидание ответа от сервера waitResponseFromServer ) diff --git a/part_8/tic_tac_toe_v7/client/menu.go b/part_8/tic_tac_toe_v7/client/menu.go index 200bae0..5250d36 100644 --- a/part_8/tic_tac_toe_v7/client/menu.go +++ b/part_8/tic_tac_toe_v7/client/menu.go @@ -12,52 +12,74 @@ import ( "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) + 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 + log.Printf( + "Failed to send message to server: %v. Disconnecting.", + err, + ) + // Выходим из программы, если не удалось отправить сообщение + return } - for { - switch c.getState() { - case waitNickNameConfirm: + for { // Бесконечный цикл + switch c.getState() { // Переключение состояний + case waitNickNameConfirm: // Ожидание подтверждения никнейма + // Ожидаем подтверждения никнейма от сервера time.Sleep(100 * time.Millisecond) continue - case mainMenu: + case mainMenu: // Главное меню + // Переходим в главное меню c.mainMenu(reader, encoder) - case playerMove: + case playerMove: // Ход игрока + // Отрабатываем ход игрока c.playing(reader, encoder) - case opponentMove: - // Just wait silently for opponent's move + case opponentMove: // Ход противника + // Ожидаем данные по ходу противника time.Sleep(1000 * time.Millisecond) continue - case endGame: + case endGame: // Конец игры + // Игра завершена. Ждем ее перезапуск от сервера fmt.Println("\nGame has ended. Restarting in 10 seconds...") time.Sleep(10 * time.Second) continue - case waitResponseFromServer: + case waitResponseFromServer: // Ожидание ответа от сервера time.Sleep(100 * time.Millisecond) continue - case waitingOpponentInRoom: - // Rate-limit messages to once every 3 seconds + case waitingOpponentInRoom: // Ожидание противника в комнате + // Здесь нам надо учесть ситуацию, сто противник может + // так и не подключиться к комнате. Поэтому, чтобы не + // заставлять игрока страдать в бесконечном цикле ожидания + // мы ограничиваем сообщения 1 раз в 3 секунды и считываем + // ввод пользователя посредством неблокирующего чтения now := time.Now() + // Если прошло более 3 секунд с момента последнего сообщения if now.Sub(c.lastMsgTime) > 3*time.Second { c.lastMsgTime = now fmt.Println("\nWaiting for opponent to join...") @@ -65,11 +87,15 @@ func (c *Client) menu() { fmt.Print("> ") } - // Poll for input every cycle but don't block + // Проверяем ввод пользователя var buffer [1]byte n, _ := os.Stdin.Read(buffer[:]) + // Если пользователь нажал 'q' или 'Q', + // то выходим в главное меню if n > 0 && (buffer[0] == 'q' || buffer[0] == 'Q') { fmt.Println("Leaving room...") + // Формируем сообщение о выходе из комнаты + // и отправляем на сервер var msg network.Message msg.Cmd = network.CmdLeaveRoomRequest payload := network.LeaveRoomRequest{ @@ -78,21 +104,23 @@ func (c *Client) menu() { } jsonPayload, _ := json.Marshal(payload) msg.Payload = jsonPayload - encoder.Encode(msg) - c.setState(mainMenu) + 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 + var msg network.Message // Создаем буфер для сообщения + // Выводим меню fmt.Println("Enter command:") fmt.Println("1 - Get room list") fmt.Println("2 - Join room") @@ -100,24 +128,31 @@ func (c *Client) mainMenu(reader *bufio.Reader, encoder *json.Encoder) { 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: + case 1: // Получаем список комнат + // Формируем сообщение и отправляем на сервер msg.Cmd = network.CmdListRoomsRequest encoder.Encode(msg) + // Переходим в состояние ожидания ответа от сервера c.setState(waitResponseFromServer) - case 2: + 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{ @@ -126,32 +161,39 @@ func (c *Client) mainMenu(reader *bufio.Reader, encoder *json.Encoder) { } jsonPayload, _ := json.Marshal(payload) msg.Payload = jsonPayload - encoder.Encode(msg) + encoder.Encode(msg) // Отправляем сообщение на сервер + // Переходим в состояние ожидания ответа от сервера c.setState(waitResponseFromServer) - case 3: + case 3: // Получаем список завершенных игр + // Формируем сообщение и отправляем на сервер msg.Cmd = network.CmdFinishedGamesRequest encoder.Encode(msg) + // Переходим в состояние ожидания ответа от сервера c.setState(waitResponseFromServer) - case 4: + case 4: // Получаем завершенную игру по id + // Запрашиваем id игры у пользователя 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) + encoder.Encode(msg) // Отправляем сообщение на сервер + // Переходим в состояние ожидания ответа от сервера c.setState(waitResponseFromServer) - case 5: + case 5: // Выходим из программы os.Exit(0) - default: + default: // Неизвестная команда fmt.Println("Unknown command.") return } diff --git a/part_8/tic_tac_toe_v7/client/playing.go b/part_8/tic_tac_toe_v7/client/playing.go index 56b2952..2d741f8 100644 --- a/part_8/tic_tac_toe_v7/client/playing.go +++ b/part_8/tic_tac_toe_v7/client/playing.go @@ -9,32 +9,38 @@ import ( "tic-tac-toe/network" ) +// Обрабатываем ход игрока func (c *Client) playing(reader *bufio.Reader, encoder *json.Encoder) { - fmt.Printf("\nEnter command: or q for exit to main menu\n> ") + fmt.Printf( + "\nEnter command: 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 + 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) + encoder.Encode(msg) // Отправляем сообщение + c.setState(mainMenu) // Переходим в главное меню return } + // Разделяем ввод игрока на строки parts := strings.Fields(input) if len(parts) != 2 { fmt.Println("Usage: ") return } - var msg network.Message + var msg network.Message // Создаем сообщение + // Преобразуем ввод игрока в числа row, err1 := strconv.Atoi(parts[0]) col, err2 := strconv.Atoi(parts[1]) if err1 != nil || err2 != nil { @@ -42,19 +48,22 @@ func (c *Client) playing(reader *bufio.Reader, encoder *json.Encoder) { return } + // Валидируем ввод игрока if !c.validateMove(row, col) { return // validateMove prints the error } - msg.Cmd = network.CmdMakeMoveRequest + // Создаем сообщение о ходе игрока + msg.Cmd = network.CmdMakeMoveRequest // Устанавливаем команду payload := network.MakeMoveRequest{ - RoomName: c.roomName, - PlayerName: c.playerName, - PositionRow: row - 1, - PositionCol: col - 1, + RoomName: c.roomName, // Устанавливаем имя комнаты + PlayerName: c.playerName, // Устанавливаем никнейм игрока + PositionRow: row - 1, // Устанавливаем строку + PositionCol: col - 1, // Устанавливаем столбец } jsonPayload, _ := json.Marshal(payload) msg.Payload = jsonPayload - encoder.Encode(msg) + encoder.Encode(msg) // Отправляем сообщение + // Переходим в состояние ожидания ответа от сервера c.setState(waitResponseFromServer) } diff --git a/part_8/tic_tac_toe_v7/client/server_message_handlers.go b/part_8/tic_tac_toe_v7/client/server_message_handlers.go index 121ccc8..2e6607c 100644 --- a/part_8/tic_tac_toe_v7/client/server_message_handlers.go +++ b/part_8/tic_tac_toe_v7/client/server_message_handlers.go @@ -5,39 +5,47 @@ import ( "fmt" "log" - b "tic-tac-toe/board" - g "tic-tac-toe/game" // Added for g.GameMode + b "tic-tac-toe/board" // Added for g.GameMode "tic-tac-toe/network" ) -// handleRoomJoinResponse processes the RoomJoinResponse message from the server. +// Обрабатываем ответ на запрос на присоединение к комнате func (c *Client) handleRoomJoinResponse(payload json.RawMessage) { - var res network.RoomJoinResponse + 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.mySymbol = res.PlayerSymbol // Устанавливаем фигуру игрока + c.roomName = res.RoomName // Устанавливаем имя комнаты + if res.Board.Size > 0 { // Проверяем размер поля + 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) + 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 + var res network.InitGameResponse // Десериализуем ответ if err := json.Unmarshal(payload, &res); err == nil { - c.board = &res.Board - c.currentPlayer = res.CurrentPlayer + c.board = &res.Board // Устанавливаем игровое поле + c.currentPlayer = res.CurrentPlayer // Устанавливаем фигуру игрока fmt.Println("\n--- Game Started ---") - c.board.PrintBoard() - c.printTurnInfo() + c.board.PrintBoard() // Выводим игровое поле + c.printTurnInfo() // Выводим информацию о ходе игрока + // Устанавливаем состояние клиента if res.CurrentPlayer == c.mySymbol { c.setState(playerMove) } else { @@ -48,15 +56,16 @@ func (c *Client) handleInitGame(payload json.RawMessage) { } } -// handleUpdateState processes the GameStateUpdate message from the server. +// Обрабатываем сообщение об обновлении состояния игры func (c *Client) handleUpdateState(payload json.RawMessage) { - var res network.GameStateUpdate + var res network.GameStateUpdate // Десериализуем ответ if err := json.Unmarshal(payload, &res); err == nil { - c.board = &res.Board - c.currentPlayer = res.CurrentPlayer + c.board = &res.Board // Устанавливаем игровое поле + c.currentPlayer = res.CurrentPlayer // Устанавливаем фигуру игрока fmt.Println("\n--- Game State Update ---") - c.board.PrintBoard() - c.printTurnInfo() + c.board.PrintBoard() // Выводим игровое поле + c.printTurnInfo() // Выводим информацию о ходе игрока + // Устанавливаем состояние клиента if res.CurrentPlayer == c.mySymbol { c.setState(playerMove) } else { @@ -67,28 +76,29 @@ func (c *Client) handleUpdateState(payload json.RawMessage) { } } -// handleEndGame processes the EndGameResponse message from the server. +// Обрабатываем сообщение об окончании игры func (c *Client) handleEndGame(payload json.RawMessage) { - var res network.EndGameResponse + var res network.EndGameResponse // Десериализуем ответ if err := json.Unmarshal(payload, &res); err == nil { - c.board = &res.Board + c.board = &res.Board // Устанавливаем игровое поле fmt.Println("\n--- Game Over ---") - c.board.PrintBoard() + 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) + 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 + var errPayload network.ErrorResponse // Десериализуем ответ if err := json.Unmarshal(payload, &errPayload); err == nil { fmt.Printf("\nServer Error: %s\n> ", errPayload.Message) } else { @@ -97,41 +107,18 @@ func (c *Client) handleError(payload json.RawMessage) { 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 "" - } -} - -// handleRoomListResponse processes the RoomListResponse message from the server. +// Обрабатываем сообщение о списке комнат func (c *Client) handleRoomListResponse(payload json.RawMessage) { - var roomList network.RoomListResponse + 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 { + } else { // Иначе выводим список комнат for _, room := range roomList.Rooms { - fmt.Printf("- %s (Board Size: %dx%d, Full: %t, Mode: %s, Difficulty: %s)\n", + fmt.Printf("- %s (Board Size: %dx%d, Full: %t, "+ + "Mode: %s, Difficulty: %s)\n", room.Name, room.BoardSize, room.BoardSize, room.IsFull, @@ -146,37 +133,39 @@ func (c *Client) handleRoomListResponse(payload json.RawMessage) { c.setState(mainMenu) } -// handleNickNameResponse processes the NickNameResponse message from the server. +// Обрабатываем ответ на запрос на присоединение к комнате func (c *Client) handleNickNameResponse(payload json.RawMessage) { - var res network.NickNameResponse + 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) + 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 + 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) } -// handleFinishedGamesResponse processes the FinishedGamesResponse message from the server. +// Обрабатываем ответ на запрос на получение списка завершенных игр func (c *Client) handleFinishedGamesResponse(payload json.RawMessage) { - var res network.FinishedGamesResponse + 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 { + } else { // Иначе выводим список завершенных игр for _, game := range *res.Games { fmt.Printf("- Game #%d: %s vs %s (Winner: %s) at %v\n", game.ID, game.WinnerName, @@ -188,13 +177,16 @@ func (c *Client) handleFinishedGamesResponse(payload json.RawMessage) { } else { log.Printf("Error unmarshalling FinishedGamesResponse: %v", err) } + // Устанавливаем состояние клиента c.setState(mainMenu) } -// handleFinishedGameResponse processes the FinishedGameResponse message from the server. +// Обрабатываем ответ на запрос на получение данных +// о конкретной завершенной игре func (c *Client) handleFinishedGameResponse(payload json.RawMessage) { - var res network.FinishedGameResponse + 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, @@ -207,5 +199,5 @@ func (c *Client) handleFinishedGameResponse(payload json.RawMessage) { } else { log.Printf("Error unmarshalling FinishedGameResponse: %v", err) } - c.setState(mainMenu) + c.setState(mainMenu) // Устанавливаем состояние клиента } diff --git a/part_8/tic_tac_toe_v7/client/utils.go b/part_8/tic_tac_toe_v7/client/utils.go index 279ca0d..dc2c02e 100644 --- a/part_8/tic_tac_toe_v7/client/utils.go +++ b/part_8/tic_tac_toe_v7/client/utils.go @@ -3,36 +3,69 @@ package client import ( "fmt" b "tic-tac-toe/board" + g "tic-tac-toe/game" ) +// Выводит информацию о ходе игрока func (c *Client) printTurnInfo() { if c.board == nil { return } - if c.currentPlayer == c.mySymbol { + if c.currentPlayer == c.mySymbol { // Если ход игрока fmt.Println("It's your turn.") - } else if c.currentPlayer != b.Empty { + } 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 { + 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) + 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 + // Преобразуем в 0-индексированный для доступа к полю if c.board.Board[row-1][col-1] != b.Empty { fmt.Println("Invalid move. Cell is already occupied.") return false } return true } + +// Конвертируем экземпляр типа GameMode в строку +func gameModeToString(mode g.GameMode) string { + switch mode { + case g.PvP: + return "PvP" + case g.PvC: + return "PvC" + default: + return "Unknown" + } +} + +// Конвертируем экземпляр типа Difficulty в строку +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 "" + } +} diff --git a/part_8/tic_tac_toe_v7/network/server_to_client.go b/part_8/tic_tac_toe_v7/network/server_to_client.go index b4e0a57..b6c399c 100644 --- a/part_8/tic_tac_toe_v7/network/server_to_client.go +++ b/part_8/tic_tac_toe_v7/network/server_to_client.go @@ -11,7 +11,6 @@ const ( 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" @@ -69,12 +68,6 @@ type NickNameResponse struct { Nickname string `json:"nickname"` } -// Отправляется сервером после успешного создания комнаты -type RoomCreatedResponse struct { - RoomID string `json:"room_id"` - RoomName string `json:"room_name"` -} - // Отправляется сервером, когда клиент успешно присоединился к комнате type RoomJoinResponse struct { RoomName string `json:"room_name"` diff --git a/part_8/tic_tac_toe_v7/room/room.go b/part_8/tic_tac_toe_v7/room/room.go index 658dc07..0c12640 100644 --- a/part_8/tic_tac_toe_v7/room/room.go +++ b/part_8/tic_tac_toe_v7/room/room.go @@ -13,21 +13,25 @@ import ( "time" ) -// Room manages the state of a single game room. +// Room — структура, которая описывает игровую комнату type Room struct { - Name string - Board *b.Board - Player1 p.IPlayer - Player2 p.IPlayer + Name string // Название комнаты + Board *b.Board // Ссылка на игровую доску + Player1 p.IPlayer // Первый игрок (только человек) + Mode g.GameMode // Режим игры: PvP или PvC + + // Второй игрок (может быть человеком или компьютером) + Player2 p.IPlayer + // Текущий игрок, который должен сделать ход CurrentPlayer p.IPlayer - State g.GameState - repository db.IRepository - Mode g.GameMode - // Уровень сложности компьютера (только для PvC) + // Текущее состояние игры (чей ход, победа, ничья и т.д.) + State g.GameState + // Уровень сложности компьютера (используется только в режиме PvC) Difficulty g.Difficulty + // Интерфейс для сохранения завершенных игр в базе данных + repository db.IRepository } -// NewRoom creates a new game room. func NewRoom( name string, repository db.IRepository, boardSize int, gameMode g.GameMode, difficulty g.Difficulty, @@ -40,61 +44,83 @@ func NewRoom( Board: b.NewBoard(boardSize), State: g.WaitingOpponent, } + // Если режим игры — PvC, то создаем компьютерного игрока if gameMode == g.PvC { room.Player2 = p.NewComputerPlayer(b.Nought, difficulty) } return room } +// Возвращает true, если в комнате есть два игрока func (r *Room) IsFull() bool { - return r.Player1 != nil && r.Player2 != nil + return r.Player1 != nil && r.Player2 != nil // Если оба игрока не равны nil, значит комната полная } +// Возвращает количество игроков в комнате func (r *Room) PlayersAmount() int { if r.Player1 != nil && r.Player2 != nil { - return 2 + return 2 // Если оба игрока есть, возвращаем 2 + } else if r.Player1 != nil || r.Player2 != nil { + return 1 // Если только один игрок, возвращаем 1 } - return 1 + return 0 // Если ни один игрок не добавлен, возвращаем 0 } +// Возвращает размер доски func (r *Room) BoardSize() int { return r.Board.Size } +// Добавляем игрока в комнату +// Первый добавленный игрок становится Player1, второй — Player2 +// Символы игроков автоматически корректируются: 1 — X, 2 — O func (r *Room) AddPlayer(player p.IPlayer) { if r.Player1 == nil { - r.Player1 = player + r.Player1 = player // Первый игрок if r.Player1.GetSymbol() != "X" { - r.Player1.SwitchPlayer() + r.Player1.SwitchPlayer() // Если символ не X, меняем на X } } else if r.Player2 == nil { - r.Player2 = player + r.Player2 = player // Второй игрок if r.Player2.GetSymbol() != "O" { - r.Player2.SwitchPlayer() + r.Player2.SwitchPlayer() // Если символ не O, меняем на O } } } +// Удаляем игрока из комнаты +// В случае выхода игрока его оппоненту (если присутствует в +// комнате и человек) отправляется сообщение о выходе соперника func (r *Room) RemovePlayer(player p.IPlayer) { if r.Player1 == player { - r.Player1 = nil + r.Player1 = nil // Удаляем первого игрока + // Если в комнате есть второй игрок и он человек, + // уведомляем его о выходе соперника if r.Player2 != nil && !r.Player2.IsComputer() { - opponentLeft := &n.OpponentLeft{Nickname: player.GetNickname()} + 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, + Cmd: n.CmdOpponentLeft, // Команда "соперник вышел" Payload: payloadBytes, } + // Отправляем сообщение второму игроку r.Player2.SendMessage(msg) } } else if r.Player2 == player { - r.Player2 = nil - if r.Player1 != nil && !r.Player1.IsComputer() { - opponentLeft := &n.OpponentLeft{Nickname: player.GetNickname()} + r.Player2 = nil // Удаляем второго игрока + // Если в комнате есть первый игрок, + // уведомляем его о выходе соперника + if r.Player1 != nil { + opponentLeft := &n.OpponentLeft{ + Nickname: player.GetNickname(), + } payloadBytes, err := json.Marshal(opponentLeft) if err != nil { log.Printf("Error marshaling OpponentLeft: %v", err) @@ -104,26 +130,33 @@ func (r *Room) RemovePlayer(player p.IPlayer) { Cmd: n.CmdOpponentLeft, Payload: payloadBytes, } + // Отправляем сообщение первому игроку r.Player1.SendMessage(msg) } } } +// Инициализируем новую игру в комнате. +// Здесь выбирается случайный игрок, который начинает первым, +// и отправляется сообщение обоим игрокам о начале игры. +// Если первый ход за компьютером, то он делает ход автоматически func (r *Room) InitGame() { if !r.IsFull() { - return + 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} + msg := &n.Message{Cmd: n.CmdInitGame} // Сообщение о начале игры initGamePayload := &n.InitGameResponse{ - Board: *r.Board, + Board: *r.Board, // Текущее состояние доски } - // Select a random starting symbol + // Выбираем случайным образом, кто ходит первым (X или O) starterSymbol := randomPlayer[rand.Intn(len(randomPlayer))] switch starterSymbol { case b.Cross: @@ -134,33 +167,40 @@ func (r *Room) InitGame() { initGamePayload.CurrentPlayer = b.Nought } - // Set the current player based on game mode and starter symbol + // Устанавливаем активного игрока в зависимости + // от режима игры и символа if r.Mode == g.PvC { - // In PvC mode, Player1 is always the human player + // В режиме PvC человек всегда Player1 if r.State == g.CrossStep { r.CurrentPlayer = r.Player1 } else if r.State == g.NoughtStep { r.CurrentPlayer = r.Player2 } } else { - // In PvP mode, set the current player based on who has the starter symbol - if (r.State == g.CrossStep && r.Player1.GetFigure() == b.Cross) || - (r.State == g.NoughtStep && r.Player1.GetFigure() == b.Nought) { + // В режиме PvP ищем, кто играет выбранным символом + if (r.State == g.CrossStep && + r.Player1.GetFigure() == b.Cross) || + (r.State == g.NoughtStep && + r.Player1.GetFigure() == b.Nought) { r.CurrentPlayer = r.Player1 } else { r.CurrentPlayer = r.Player2 } } + // Сериализуем данные для отправки игрокам payloadBytes, err := json.Marshal(initGamePayload) if err != nil { - log.Printf("Error marshaling InitGameResponse for Player1 after Player2 left: %v", err) + log.Printf( + "Error marshaling InitGameResponse for Player1 "+ + "after Player2 left: %v", err) return } msg.Payload = payloadBytes - r.Player1.SendMessage(msg) - r.Player2.SendMessage(msg) + 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) @@ -170,29 +210,38 @@ func (r *Room) InitGame() { // Переключаем активного игрока func (r *Room) switchCurrentPlayer() { if r.CurrentPlayer == r.Player1 { - r.CurrentPlayer = r.Player2 + r.CurrentPlayer = r.Player2 // Если сейчас ходил первый, теперь ход второго } else { - r.CurrentPlayer = r.Player1 + r.CurrentPlayer = r.Player1 // И наоборот } } +// PlayerStep выполняет ход игрока и обновляет состояние игры +// Здесь проверяется правильность хода, обновляется доска, +// определяется победитель или ничья, и отправляются сообщения игрокам. +// Если после хода игра не закончена, ход переходит следующему игроку. +// Если ходит компьютер — он делает ход автоматически. func (r *Room) PlayerStep(player p.IPlayer, row, col int) { - msg := &n.Message{} + msg := &n.Message{} // Создаем новое сообщение для игроков + // Проверяем, что сейчас идет ход (игра не завершена) if r.State != g.CrossStep && r.State != g.NoughtStep { - return + 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 + r.State = g.CrossWin // Победа крестиков } else { - r.State = g.NoughtWin + r.State = g.NoughtWin // Победа ноликов } + // Формируем сообщение о завершении игры msg.Cmd = n.CmdEndGame endGamePayload := &n.EndGameResponse{ Board: *r.Board, @@ -200,6 +249,7 @@ func (r *Room) PlayerStep(player p.IPlayer, row, col int) { } msg.Payload, _ = json.Marshal(endGamePayload) + // Сохраняем информацию о завершенной игре в базе данных figureWinner := r.CurrentPlayer.GetFigure() winnerNickName := r.CurrentPlayer.GetNickname() @@ -217,6 +267,7 @@ func (r *Room) PlayerStep(player p.IPlayer, row, col int) { Time: time.Now(), }) } else if r.Board.CheckDraw() { + // Если доска заполнена, но победителя нет — ничья r.State = g.Draw msg.Cmd = n.CmdEndGame endGamePayload := &n.EndGameResponse{ @@ -225,13 +276,14 @@ func (r *Room) PlayerStep(player p.IPlayer, row, col int) { } 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 + msg.Cmd = n.CmdUpdateState // Сообщаем о новом состоянии игры stateUpdatePayload := &n.GameStateUpdate{ Board: *r.Board, CurrentPlayer: r.CurrentPlayer.GetFigure(), @@ -239,15 +291,20 @@ func (r *Room) PlayerStep(player p.IPlayer, row, col int) { 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 { + // Если игра завершена (победа или ничья), + // ждем 10 секунд и запускаем новую + 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) diff --git a/part_8/tic_tac_toe_v7/server/client_message_handlers.go b/part_8/tic_tac_toe_v7/server/client_message_handlers.go index ac6e75b..839c0e9 100644 --- a/part_8/tic_tac_toe_v7/server/client_message_handlers.go +++ b/part_8/tic_tac_toe_v7/server/client_message_handlers.go @@ -9,8 +9,15 @@ import ( 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", @@ -18,18 +25,25 @@ func (s *Server) handleCommand(client net.Conn, msg *network.Message) { ) 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.CmdFinishedGamesRequest: s.getFinishedGamesHandler(client, msg) + // Обрабатываем команду запроса завершенной игры по ID case network.CmdFinishedGameByIdRequest: s.getFinishedGameByIdHandler(client, msg) default: @@ -37,54 +51,77 @@ func (s *Server) handleCommand(client net.Conn, msg *network.Message) { } } +// Обрабатываем запрос на регистрацию никнейма игрока. +// Проверяем уникальность никнейма, при необходимости +// добавляем суффикс, создаем нового игрока и отправляем +// клиенту подтверждение с итоговым никнеймом. 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 } + s.mutex.Lock() // Защищаем доступ к данным if s.players[nicknameRequest.Nickname] != nil { + // Если никнейм занят, добавляем суффикс nicknameRequest.Nickname = nicknameRequest.Nickname + "_" + strconv.Itoa(defaultPlayerCounts) defaultPlayerCounts++ } + // Создаем игрока и добавляем его в карту игроков s.players[nicknameRequest.Nickname] = p.NewHumanPlayer( nicknameRequest.Nickname, &client, ) + s.mutex.Unlock() + // Формируем ответ клиенту response := &network.NickNameResponse{ Nickname: nicknameRequest.Nickname, } msg.Payload, _ = json.Marshal(response) msg.Cmd = network.CmdNickNameResponse - json.NewEncoder(client).Encode(msg) + 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 } + // Получаем комнату и игрока + s.mutex.RLock() // Защищаем доступ к данным room, okRoom := s.rooms[joinRoomRequest.RoomName] player, okPlayer := s.players[joinRoomRequest.PlayerName] + s.mutex.RUnlock() + // Проверяем существование комнаты и игрока 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() { + 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) + room.AddPlayer(player) // Добавляем игрока в комнату s.mutex.Unlock() + // Формируем ответ клиенту response := &network.RoomJoinResponse{ RoomName: joinRoomRequest.RoomName, PlayerSymbol: player.GetFigure(), @@ -92,33 +129,49 @@ func (s *Server) joinRoomHandler(client net.Conn, msg *network.Message) { } msg.Payload, _ = json.Marshal(response) msg.Cmd = network.CmdRoomJoinResponse - json.NewEncoder(client).Encode(msg) - room.InitGame() + json.NewEncoder(client).Encode(msg) // Отправляем ответ клиенту + room.InitGame() // Инициируем старт игры } -func (s *Server) leaveRoomHandler(client net.Conn, msg *network.Message) { +// Обрабатываем выход игрока из комнаты. +// Проверяем существование комнаты и игрока, удаляем игрока из комнаты. +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 } + // Получаем комнату и игрока + s.mutex.RLock() // Защищаем доступ к данным room, okRoom := s.rooms[leaveRoomRequest.RoomName] player, okPlayer := s.players[leaveRoomRequest.PlayerName] + s.mutex.RUnlock() + // Проверяем существование комнаты и игрока 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) + room.RemovePlayer(player) // Удаляем игрока из комнаты s.mutex.Unlock() } -func (s *Server) listRoomsHandler(client net.Conn, msg *network.Message) { +// Обрабатываем запрос на получение списка всех комнат +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{ @@ -129,25 +182,32 @@ func (s *Server) listRoomsHandler(client net.Conn, msg *network.Message) { Difficult: room.Difficulty, }) } - + // Формируем ответ клиенту response := &network.RoomListResponse{ Rooms: roomInfos, } msg.Cmd = network.CmdRoomListResponse msg.Payload, _ = json.Marshal(response) - json.NewEncoder(client).Encode(msg) + json.NewEncoder(client).Encode(msg) // Отправляем сообщение } -func (s *Server) getFinishedGamesHandler(client net.Conn, msg *network.Message) { - // получаем данные из БД +// Обрабатываем запрос на получение списка завершенных игр +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"} + // Если ошибка, отправляем сообщение об ошибке + 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, } @@ -156,20 +216,36 @@ func (s *Server) getFinishedGamesHandler(client net.Conn, msg *network.Message) json.NewEncoder(client).Encode(msg) } -func (s *Server) getFinishedGameByIdHandler(client net.Conn, msg *network.Message) { +// Обрабатываем запрос на получение завершенной игры по ID +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) + if err := json.Unmarshal( + msg.Payload, getFinishedGameByIdRequest, + ); err != nil { + log.Printf( + "Error unmarshaling GetFinishedGameByIdRequest: %v", + err, + ) return } - finishedGame, err := s.repository.GetFinishedGameById(getFinishedGameByIdRequest.GameID) + // Получаем завершенную игру по ID + finishedGame, err := s.repository.GetFinishedGameById( + getFinishedGameByIdRequest.GameID, + ) if err != nil { - response := &network.ErrorResponse{Message: "Error getting finished game by id"} + // Если ошибка, отправляем сообщение об ошибке + response := &network.ErrorResponse{ + Message: "Error getting finished game by id", + } msg.Cmd = network.CmdError msg.Payload, _ = json.Marshal(response) json.NewEncoder(client).Encode(msg) return } + // Формируем ответ клиенту response := &network.FinishedGameResponse{ Game: finishedGame, } @@ -178,21 +254,32 @@ func (s *Server) getFinishedGameByIdHandler(client net.Conn, msg *network.Messag json.NewEncoder(client).Encode(msg) } -func (s *Server) makeMoveHandler(client net.Conn, msg *network.Message) { +// Обрабатываем запрос на ход игрока +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 } + // Получаем комнату и игрока + s.mutex.RLock() // Защищаем доступ к данным room, okRoom := s.rooms[makeMoveRequest.RoomName] player, okPlayer := s.players[makeMoveRequest.PlayerName] - if !okRoom || !okPlayer { - response := &network.ErrorResponse{Message: "Room not found"} + s.mutex.RUnlock() + 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, diff --git a/part_8/tic_tac_toe_v7/server/server.go b/part_8/tic_tac_toe_v7/server/server.go index 86e5e26..8caa359 100644 --- a/part_8/tic_tac_toe_v7/server/server.go +++ b/part_8/tic_tac_toe_v7/server/server.go @@ -13,21 +13,26 @@ import ( "tic-tac-toe/room" ) -// Server manages client connections and game rooms. type Server struct { - listener net.Listener + // Слушатель для подключения клиентов + listener net.Listener + // Интерфейс для сохранения завершенных игр в базе данных repository db.IRepository - rooms map[string]*room.Room - players map[string]player.IPlayer - mutex sync.RWMutex + // Карта комнат + 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) + listener, err := net.Listen("tcp", addr) // Создаем слушатель if err != nil { return nil, err } + + // Создаем экземпляр структуры Server server := &Server{ listener: listener, repository: repository, @@ -35,6 +40,8 @@ func NewServer(addr string, repository db.IRepository) (*Server, error) { players: make(map[string]player.IPlayer), } + // Создаем комнаты и добавляем их в карту rooms по ключу, + // который является именем комнаты server.rooms["room1"] = room.NewRoom( "room1", server.repository, 3, g.PvP, g.None, ) @@ -48,64 +55,84 @@ func NewServer(addr string, repository db.IRepository) (*Server, error) { "room4", server.repository, 6, 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() + defer s.listener.Close() // Закрываем слушателя при завершении + // Запускаем бесконечный цикл обработки подключений for { + // Принимаем подключение conn, err := s.listener.Accept() - if err != nil { + 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() + defer conn.Close() // Закрываем подключение при завершении + // Создаем декодер для чтения сообщений от клиента decoder := json.NewDecoder(conn) - for { + for { // Бесконечный цикл обработки сообщений + // Создаем переменную для хранения сообщения var msg network.Message - if err := decoder.Decode(&msg); err != nil { + // Декодируем сообщение от клиента + 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 + var player player.IPlayer // Создаем переменную для хранения игрока + // Проходим по всем комнатам for _, room := range s.rooms { + // Если игрок 1 в комнате if room.Player1 != nil { + // Если игрок 1 подключился по этому сокету if room.Player1.CheckSocket(conn) { player = room.Player1 + // Удаляем игрока из комнаты room.RemovePlayer(room.Player1) break } } + // Если игрок 2 в комнате if room.Player2 != nil { + // Если игрок 2 подключился по этому сокету if room.Player2.CheckSocket(conn) { player = room.Player2 + // Удаляем игрока из комнаты room.RemovePlayer(room.Player2) break } } } + // Если игрок не найден if player == nil { log.Printf( "Client %s disconnected: player not found", @@ -113,6 +140,7 @@ func (s *Server) disconnectedClientHandler(conn net.Conn) { ) return } + // Удаляем игрока из карты, предварительно защищая доступ к ней s.mutex.Lock() delete(s.players, player.GetNickname()) s.mutex.Unlock()