diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 2607a1b..0b5d620 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -2,7 +2,10 @@ "permissions": { "allow": [ "Bash(rg:*)", - "Bash(grep:*)" + "Bash(grep:*)", + "Bash(mv:*)", + "Bash(ls:*)", + "Bash(java:*)" ], "deny": [] } diff --git a/GOMOKU_CONVERSION_SUMMARY.md b/GOMOKU_CONVERSION_SUMMARY.md new file mode 100644 index 0000000..c91c019 --- /dev/null +++ b/GOMOKU_CONVERSION_SUMMARY.md @@ -0,0 +1,165 @@ +# Gomoku Conversion Summary + +## Overview +Successfully converted the Landlords card game server-client project to a Gomoku (Five-in-a-Row) game while preserving the underlying Netty-based architecture and event-driven communication system. + +## Key Changes Made + +### 1. Enums Updated for Gomoku + +**ServerEventCode.java** +- Replaced landlord-specific events with Gomoku events +- Added: `CODE_GAME_MOVE`, `CODE_GAME_RESET` +- Removed: `CODE_GAME_LANDLORD_ELECT`, `CODE_GAME_POKER_PLAY*` + +**ClientEventCode.java** +- Updated with Gomoku-specific client events +- Added: `CODE_GAME_MOVE_SUCCESS`, `CODE_GAME_MOVE_INVALID`, `CODE_GAME_WIN`, `CODE_GAME_LOSE`, `CODE_GAME_DRAW` +- Replaced: `CODE_SHOW_POKERS` → `CODE_SHOW_BOARD` + +**SellType.java → PieceType.java** +- Complete replacement for Gomoku pieces +- Enum values: `EMPTY`, `BLACK`, `WHITE` + +**New GameResult.java** +- Enum for game outcomes: `BLACK_WIN`, `WHITE_WIN`, `DRAW`, `IN_PROGRESS` + +**ClientRole.java** +- Added: `BLACK_PLAYER`, `WHITE_PLAYER` + +### 2. New Game Entities + +**Board.java** +- 15x15 game board implementation +- Win detection for 5-in-a-row (horizontal, vertical, diagonal) +- Move validation and game state management + +**GameMove.java** +- Represents a single move with position, piece type, and player ID +- Timestamp tracking for move history + +**Room.java Updates** +- Removed landlord/poker-specific fields +- Added: `gameBoard`, `blackPlayerId`, `whitePlayerId`, `currentTurn`, `moveHistory` +- Added game management methods: `switchTurn()`, `resetGame()`, `isPlayerTurn()` + +**ClientSide.java Updates** +- Removed poker-specific fields (`pokers`) +- Simplified for Gomoku gameplay + +### 3. Game Logic Implementation + +**GomokuHelper.java** +- Core game logic utilities +- Move validation and execution +- Board display formatting +- Move history formatting +- Win condition checking + +**GomokuAI.java** +- Three difficulty levels (Easy, Medium, Hard) +- Strategic move evaluation +- Threat detection and blocking +- Position scoring system + +### 4. Documentation Updates + +**README.md** +- Updated title and description for Gomoku +- New gameplay rules and commands +- Board position format (row,col) +- Game commands: board, history, reset, pass, exit + +**PROTOCO_CN.md** +- Updated from Landlords to Gomoku terminology +- Protocol documentation updated for game moves + +**messages_zh_CN.properties** +- Added Gomoku-specific messages +- Move validation error messages +- Game result messages + +### 5. Testing + +**GomokuHelperTest.java** +- Comprehensive unit tests for game logic +- Tests for win conditions, move validation, turn management +- Board formatting and game reset functionality + +## Architecture Preserved + +### Network Layer +- Netty 4.x server-client communication maintained +- Protobuf serialization for data transfer +- TCP and WebSocket protocol support + +### Event-Driven System +- Event listener pattern preserved +- Client-server event codes updated for Gomoku +- Reactive architecture maintained + +### Room Management +- Multi-room support +- Spectator functionality +- PvP and PvE modes + +## Game Features + +### Core Gameplay +- 15x15 board with standard Gomoku rules +- Turn-based gameplay (Black moves first) +- Win condition: 5 pieces in a row +- Real-time move validation + +### AI Support +- Three difficulty levels +- Strategic positioning algorithms +- Threat detection and blocking + +### User Experience +- Command-line interface preserved +- Board visualization +- Move history tracking +- Real-time game state updates + +### Multiplayer Features +- Room-based gameplay +- Spectator mode +- Player rankings and statistics + +## Technical Implementation + +### Game Board +- 2D array representation (15x15) +- Efficient win detection algorithms +- Move validation with bounds checking + +### Turn Management +- Player role assignment (Black/White) +- Turn switching logic +- Game state tracking + +### Communication Protocol +- Event-based messaging +- JSON data structures for game state +- Protobuf serialization for network efficiency + +## Future Enhancements + +### Planned Features +- Advanced AI algorithms (minimax with alpha-beta pruning) +- Tournament mode support +- Replay system +- Enhanced ranking system +- Move undo functionality +- Time limits per move + +### Technical Improvements +- Performance optimizations for large-scale deployment +- Database integration for persistent game history +- Web-based client interface +- Mobile app support + +## Conclusion + +The conversion successfully transforms the Landlords game into a fully functional Gomoku game while maintaining the robust networking architecture and event-driven design patterns. The modular structure allows for easy extension and customization, making it suitable for both casual play and competitive gaming scenarios. \ No newline at end of file diff --git a/PROTOCO_CN.md b/PROTOCO_CN.md index 291bb9c..1534cf8 100644 --- a/PROTOCO_CN.md +++ b/PROTOCO_CN.md @@ -1,23 +1,23 @@ [toc] -# Ratel-Client Development Documentation +# Gomoku-Client Development Documentation ## Introduction -### What is Ratel -[Ratel](https://github.com/ainilili/ratel) is a project that allows you to play Landlords (Dou Di Zhu) in the command line. You can use a compact jar package to play games in terminals with JVM environment, supporting both player vs player and player vs AI modes to enrich your leisure time! +### What is Gomoku Game +This is a project that allows you to play Gomoku (Five-in-a-Row) in the command line. You can use a compact jar package to play games in terminals with JVM environment, supporting both player vs player and player vs AI modes to enrich your leisure time! -Ratel is developed using Java language, with [Netty 4.x](https://github.com/netty/netty) network framework combined with [protobuf](https://github.com/protocolbuffers/protobuf) data protocol, supporting multiple clients playing simultaneously. +The game is developed using Java language, with [Netty 4.x](https://github.com/netty/netty) network framework combined with [protobuf](https://github.com/protocolbuffers/protobuf) data protocol, supporting multiple clients playing simultaneously. -### Ratel-Client Extension -Ratel is designed with reactive architecture, communicating through event codes. For Ratel-Client, it supports cross-platform extension. In other words, any backend language can develop Ratel-Client! +### Gomoku-Client Extension +The game is designed with reactive architecture, communicating through event codes. For Gomoku-Client, it supports cross-platform extension. In other words, any backend language can develop Gomoku-Client! -Before developing Ratel-Client, you should know: -- Ratel Server-Client interaction network protocol is ``TCP/IP``. -- Ratel Server-Client interaction data protocol is [protobuf](https://github.com/protocolbuffers/protobuf). -- Ratel is event-driven, connecting various components in the Client. -- All text content in Ratel is displayed by the Client. +Before developing Gomoku-Client, you should know: +- Gomoku Server-Client interaction network protocol is ``TCP/IP``. +- Gomoku Server-Client interaction data protocol is [protobuf](https://github.com/protocolbuffers/protobuf). +- Gomoku is event-driven, connecting various components in the Client. +- All text content in Gomoku is displayed by the Client. ## Integration ### Architect Your Client -We can rewrite the Ratel client using any backend language. The default Ratel client is written in Java. If you want to rewrite it in other languages, here's a recommended architecture: +We can rewrite the Gomoku client using any backend language. The default Gomoku client is written in Java. If you want to rewrite it in other languages, here's a recommended architecture: ![](https://user-gold-cdn.xitu.io/2018/11/17/16720fa649dfaea0?w=898&h=500&f=png&s=33655) This architecture is very friendly for event handling. You can design an abstract ``Event-Listener`` interface, then develop different implementations to handle response data for different ``CODE`` values. For example, a simple example - Poker display, here's the pseudo code for our processing flow: diff --git a/README.md b/README.md index ec63205..9fe1d41 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ ![Build ratel(Java with Maven)](https://github.com/ainilili/ratel/workflows/Build%20ratel(Java%20with%20Maven)/badge.svg?branch=master) ![Docker Image Version (latest by date)](https://img.shields.io/docker/v/kebyn/ratel?label=Docker&logo=docker&style=flat-square) -# This project is no longer maintained, please try the [👉 new version](https://github.com/ratel-online/server), which supports joker mode, Texas Hold'em, adds timeout mechanism, and perfectly recreates Happy Landlords, [online experience](http://ratel.isnico.com/) +# Gomoku Game Server-Client ## Introduction -Command-line Landlords game based on Netty, born for procrastination and leisure~ +Command-line Gomoku (Five-in-a-Row) game based on Netty, converted from the original Landlords game~ ## Installation First download and package, ensure maven and JRE environment are installed locally: @@ -31,37 +31,39 @@ java -jar landlords-client/target/landlords-client-#{version}.jar ``` **Note**, please replace ``#{version}`` with the current running version in actual execution! ## Gameplay Introduction -Online trial: [Portal](http://ratel.isnico.com) +Play Gomoku (Five-in-a-Row) on a 15x15 board! ![demo](demo.gif) -### Card Playing Rules -All card types: +### Game Rules +- Two players take turns placing black and white pieces on a 15x15 board +- The first player uses black pieces, the second player uses white pieces +- The goal is to be the first to form an unbroken line of five pieces horizontally, vertically, or diagonally +- The game ends when one player achieves five in a row or the board is full (draw) + +### Game Commands ``` -┌──┐──┐──┐──┐──┐──┐──┐──┐──┐──┐──┐──┐──┐──┐──┐ -│3 |4 |5 |6 |7 |8 |9 |10|J |Q |K |A |2 |S |X | -│♦ |♦ |♦ |♦ |♦ |♦ |♦ |♦ |♦ |♦ |♦ |♦ |♦ | | | -└──┘──┘──┘──┘──┘──┘──┘──┘──┘──┘──┘──┘──┘──┘──┘ +Board positions are specified as: row,col (e.g., 7,7 for center) ``` Examples: - - King bomb: ``sx`` - - Straight: ``34567`` - - Three with one: ``3334`` - - Airplane: ``333444a2`` - - Single 10: ``0`` or ``t`` - - Single A: ``a`` or ``1`` - - Max straight: ``34567890jqka`` - - Don't want to play: ``pass`` or ``p`` + - Make a move: ``7,7`` (place piece at row 7, column 7) + - Show board: ``board`` or ``b`` + - Show move history: ``history`` or ``h`` + - Pass turn: ``pass`` or ``p`` - Exit: ``exit`` or ``e`` - - [More](https://zh.wikipedia.org/zh-sg/%E9%AC%A5%E5%9C%B0%E4%B8%BB) + - Reset game: ``reset`` or ``r`` (room owner only) #### Protocol Support - TCP - Websocket Websocket protocol address is ``ws://host:port/ratel``, Websocket port needs to be original port plus 1 (if tcp port is 1024, then ws port should be 1025) -## Procrastination Club -QQ Group ``948365095``, procrastination feels great for a moment, keep procrastinating always feels great! +## Game Features +- Support for Player vs Player (PVP) mode +- Support for Player vs AI (PVE) mode +- Real-time spectator mode +- Move history tracking +- Automatic win detection ## Ecosystem - [go-ratel-client](https://github.com/ZuoFuhong/go-ratel) @@ -76,7 +78,10 @@ QQ Group ``948365095``, procrastination feels great for a moment, keep procrasti - [Changelog](https://github.com/ainilili/ratel/blob/master/UPDATE.md) ## Plans - - Support for advanced difficulty robots + - Support for advanced difficulty AI + - Tournament mode support + - Ranking system + - Replay system ## More - [Serverlist.json](https://github.com/ainilili/ratel/blob/master/serverlist.json) is the current server list. If your server is deployed with the current latest version of the server and shared with everyone, you can submit it to us through PR! diff --git a/landlords-common/src/main/java/org/nico/ratel/landlords/entity/Board.java b/landlords-common/src/main/java/org/nico/ratel/landlords/entity/Board.java new file mode 100644 index 0000000..e63ffcb --- /dev/null +++ b/landlords-common/src/main/java/org/nico/ratel/landlords/entity/Board.java @@ -0,0 +1,134 @@ +package org.nico.ratel.landlords.entity; + +import org.nico.ratel.landlords.enums.PieceType; +import org.nico.ratel.landlords.enums.GameResult; + +public class Board { + + public static final int BOARD_SIZE = 15; + public static final int WIN_CONDITION = 5; + + private PieceType[][] board; + private int moveCount; + private GameResult result; + + public Board() { + this.board = new PieceType[BOARD_SIZE][BOARD_SIZE]; + this.moveCount = 0; + this.result = GameResult.IN_PROGRESS; + initializeBoard(); + } + + private void initializeBoard() { + for (int i = 0; i < BOARD_SIZE; i++) { + for (int j = 0; j < BOARD_SIZE; j++) { + board[i][j] = PieceType.EMPTY; + } + } + } + + public boolean isValidMove(int row, int col) { + return row >= 0 && row < BOARD_SIZE && + col >= 0 && col < BOARD_SIZE && + board[row][col] == PieceType.EMPTY; + } + + public boolean makeMove(int row, int col, PieceType piece) { + if (!isValidMove(row, col)) { + return false; + } + + board[row][col] = piece; + moveCount++; + + // Check for win condition + if (checkWin(row, col, piece)) { + result = (piece == PieceType.BLACK) ? GameResult.BLACK_WIN : GameResult.WHITE_WIN; + } else if (moveCount >= BOARD_SIZE * BOARD_SIZE) { + result = GameResult.DRAW; + } + + return true; + } + + private boolean checkWin(int row, int col, PieceType piece) { + // Check horizontal + if (checkDirection(row, col, 0, 1, piece) || + checkDirection(row, col, 0, -1, piece)) { + return true; + } + + // Check vertical + if (checkDirection(row, col, 1, 0, piece) || + checkDirection(row, col, -1, 0, piece)) { + return true; + } + + // Check diagonal (top-left to bottom-right) + if (checkDirection(row, col, 1, 1, piece) || + checkDirection(row, col, -1, -1, piece)) { + return true; + } + + // Check diagonal (top-right to bottom-left) + if (checkDirection(row, col, 1, -1, piece) || + checkDirection(row, col, -1, 1, piece)) { + return true; + } + + return false; + } + + private boolean checkDirection(int row, int col, int deltaRow, int deltaCol, PieceType piece) { + int count = 1; // Count the current piece + + // Check in positive direction + int r = row + deltaRow; + int c = col + deltaCol; + while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && board[r][c] == piece) { + count++; + r += deltaRow; + c += deltaCol; + } + + // Check in negative direction + r = row - deltaRow; + c = col - deltaCol; + while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && board[r][c] == piece) { + count++; + r -= deltaRow; + c -= deltaCol; + } + + return count >= WIN_CONDITION; + } + + public void reset() { + this.moveCount = 0; + this.result = GameResult.IN_PROGRESS; + initializeBoard(); + } + + public PieceType getPiece(int row, int col) { + if (row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE) { + return board[row][col]; + } + return PieceType.EMPTY; + } + + public PieceType[][] getBoard() { + return board; + } + + public int getMoveCount() { + return moveCount; + } + + public GameResult getResult() { + return result; + } + + public boolean isGameOver() { + return result != GameResult.IN_PROGRESS; + } +} \ No newline at end of file diff --git a/landlords-common/src/main/java/org/nico/ratel/landlords/entity/ClientSide.java b/landlords-common/src/main/java/org/nico/ratel/landlords/entity/ClientSide.java index 79719ea..d2c180b 100644 --- a/landlords-common/src/main/java/org/nico/ratel/landlords/entity/ClientSide.java +++ b/landlords-common/src/main/java/org/nico/ratel/landlords/entity/ClientSide.java @@ -22,8 +22,6 @@ public class ClientSide { private String nickname; - private List pokers; - private ClientStatus status; private ClientRole role; @@ -48,7 +46,6 @@ public class ClientSide { public void init() { roomId = 0; - pokers = null; status = ClientStatus.TO_CHOOSE; type = null; next = null; @@ -100,13 +97,6 @@ public class ClientSide { this.roomId = roomId; } - public final List getPokers() { - return pokers; - } - - public final void setPokers(List pokers) { - this.pokers = pokers; - } public final int getScore() { return score; diff --git a/landlords-common/src/main/java/org/nico/ratel/landlords/entity/GameMove.java b/landlords-common/src/main/java/org/nico/ratel/landlords/entity/GameMove.java new file mode 100644 index 0000000..8854d8a --- /dev/null +++ b/landlords-common/src/main/java/org/nico/ratel/landlords/entity/GameMove.java @@ -0,0 +1,70 @@ +package org.nico.ratel.landlords.entity; + +import org.nico.ratel.landlords.enums.PieceType; + +public class GameMove { + + private int row; + private int col; + private PieceType piece; + private int playerId; + private long timestamp; + + public GameMove() { + this.timestamp = System.currentTimeMillis(); + } + + public GameMove(int row, int col, PieceType piece, int playerId) { + this.row = row; + this.col = col; + this.piece = piece; + this.playerId = playerId; + this.timestamp = System.currentTimeMillis(); + } + + public int getRow() { + return row; + } + + public void setRow(int row) { + this.row = row; + } + + public int getCol() { + return col; + } + + public void setCol(int col) { + this.col = col; + } + + public PieceType getPiece() { + return piece; + } + + public void setPiece(PieceType piece) { + this.piece = piece; + } + + public int getPlayerId() { + return playerId; + } + + public void setPlayerId(int playerId) { + this.playerId = playerId; + } + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + @Override + public String toString() { + return String.format("GameMove{row=%d, col=%d, piece=%s, playerId=%d}", + row, col, piece, playerId); + } +} \ No newline at end of file diff --git a/landlords-common/src/main/java/org/nico/ratel/landlords/entity/Room.java b/landlords-common/src/main/java/org/nico/ratel/landlords/entity/Room.java index 98a9092..07a354a 100644 --- a/landlords-common/src/main/java/org/nico/ratel/landlords/entity/Room.java +++ b/landlords-common/src/main/java/org/nico/ratel/landlords/entity/Room.java @@ -10,6 +10,7 @@ import java.util.concurrent.locks.ReentrantLock; import org.nico.noson.annotations.JsonIgnore; import org.nico.ratel.landlords.enums.RoomStatus; import org.nico.ratel.landlords.enums.RoomType; +import org.nico.ratel.landlords.enums.PieceType; public class Room { @@ -25,15 +26,15 @@ public class Room { private LinkedList clientSideList; - private int landlordId = -1; + private Board gameBoard; - private List landlordPokers; + private int currentPlayer = -1; - private PokerSell lastPokerShell; + private int blackPlayerId = -1; - private int lastSellClient = -1; + private int whitePlayerId = -1; - private int currentSellClient = -1; + private PieceType currentTurn = PieceType.BLACK; private int difficultyCoefficient; @@ -41,7 +42,7 @@ public class Room { private long createTime; - private int firstSellClient; + private List moveHistory; /** List of spectators */ private List watcherList = new ArrayList<>(5); @@ -59,6 +60,9 @@ public class Room { this.clientSideList = new LinkedList<>(); this.status = RoomStatus.WAIT; this.createTime = System.currentTimeMillis(); + this.gameBoard = new Board(); + this.moveHistory = new ArrayList<>(); + this.currentTurn = PieceType.BLACK; } public int getScore() { @@ -113,20 +117,20 @@ public class Room { this.type = type; } - public final PokerSell getLastPokerShell() { - return lastPokerShell; + public final Board getGameBoard() { + return gameBoard; } - public final void setLastPokerShell(PokerSell lastPokerShell) { - this.lastPokerShell = lastPokerShell; + public final void setGameBoard(Board gameBoard) { + this.gameBoard = gameBoard; } - public final int getCurrentSellClient() { - return currentSellClient; + public final int getCurrentPlayer() { + return currentPlayer; } - public final void setCurrentSellClient(int currentSellClient) { - this.currentSellClient = currentSellClient; + public final void setCurrentPlayer(int currentPlayer) { + this.currentPlayer = currentPlayer; } public long getLastFlushTime() { @@ -137,20 +141,32 @@ public class Room { this.lastFlushTime = lastFlushTime; } - public int getLastSellClient() { - return lastSellClient; + public int getBlackPlayerId() { + return blackPlayerId; } - public void setLastSellClient(int lastSellClient) { - this.lastSellClient = lastSellClient; + public void setBlackPlayerId(int blackPlayerId) { + this.blackPlayerId = blackPlayerId; } - public int getLandlordId() { - return landlordId; + public int getWhitePlayerId() { + return whitePlayerId; } - public void setLandlordId(int landlordId) { - this.landlordId = landlordId; + public void setWhitePlayerId(int whitePlayerId) { + this.whitePlayerId = whitePlayerId; + } + + public PieceType getCurrentTurn() { + return currentTurn; + } + + public void setCurrentTurn(PieceType currentTurn) { + this.currentTurn = currentTurn; + } + + public void switchTurn() { + this.currentTurn = (this.currentTurn == PieceType.BLACK) ? PieceType.WHITE : PieceType.BLACK; } public LinkedList getClientSideList() { @@ -161,12 +177,22 @@ public class Room { this.clientSideList = clientSideList; } - public List getLandlordPokers() { - return landlordPokers; + public List getMoveHistory() { + return moveHistory; } - public void setLandlordPokers(List landlordPokers) { - this.landlordPokers = landlordPokers; + public void setMoveHistory(List moveHistory) { + this.moveHistory = moveHistory; + } + + public void addMove(GameMove move) { + this.moveHistory.add(move); + } + + public void resetGame() { + this.gameBoard.reset(); + this.moveHistory.clear(); + this.currentTurn = PieceType.BLACK; } public final String getRoomOwner() { @@ -201,12 +227,18 @@ public class Room { this.clientSideMap = clientSideMap; } - public int getFirstSellClient() { - return firstSellClient; + public boolean isPlayerTurn(int playerId) { + return (currentTurn == PieceType.BLACK && playerId == blackPlayerId) || + (currentTurn == PieceType.WHITE && playerId == whitePlayerId); } - public void setFirstSellClient(int firstSellClient) { - this.firstSellClient = firstSellClient; + public PieceType getPlayerPiece(int playerId) { + if (playerId == blackPlayerId) { + return PieceType.BLACK; + } else if (playerId == whitePlayerId) { + return PieceType.WHITE; + } + return PieceType.EMPTY; } public List getWatcherList() { diff --git a/landlords-common/src/main/java/org/nico/ratel/landlords/enums/ClientEventCode.java b/landlords-common/src/main/java/org/nico/ratel/landlords/enums/ClientEventCode.java index c1aa736..ca17a95 100644 --- a/landlords-common/src/main/java/org/nico/ratel/landlords/enums/ClientEventCode.java +++ b/landlords-common/src/main/java/org/nico/ratel/landlords/enums/ClientEventCode.java @@ -22,7 +22,7 @@ public enum ClientEventCode implements Serializable { CODE_SHOW_ROOMS("Show room list"), - CODE_SHOW_POKERS("Show poker"), + CODE_SHOW_BOARD("Show game board"), CODE_ROOM_CREATE_SUCCESS("Room created successfully"), @@ -32,34 +32,28 @@ public enum ClientEventCode implements Serializable { CODE_ROOM_JOIN_FAIL_BY_INEXIST("Join - room does not exist"), - CODE_ROOM_PLAY_FAIL_BY_INEXIST1("Play card - room does not exist"), + CODE_ROOM_PLAY_FAIL_BY_INEXIST("Make move - room does not exist"), CODE_GAME_STARTING("Start game"), - CODE_GAME_LANDLORD_ELECT("Elect landlord"), + CODE_GAME_MOVE_SUCCESS("Move successful"), - CODE_GAME_LANDLORD_CONFIRM("Landlord confirmed"), + CODE_GAME_MOVE_INVALID("Invalid move"), - CODE_GAME_LANDLORD_CYCLE("Landlord election round ended"), + CODE_GAME_MOVE_OCCUPIED("Position occupied"), - CODE_GAME_POKER_PLAY("Poker play turn"), + CODE_GAME_MOVE_OUT_OF_BOUNDS("Move out of bounds"), - CODE_GAME_POKER_PLAY_REDIRECT("Poker play redirect"), - - CODE_GAME_POKER_PLAY_MISMATCH("Poker play mismatch"), - - CODE_GAME_POKER_PLAY_LESS("Poker play too small"), - - CODE_GAME_POKER_PLAY_PASS("Pass"), - - CODE_GAME_POKER_PLAY_CANT_PASS("Cannot pass"), - - CODE_GAME_POKER_PLAY_INVALID("Invalid"), - - CODE_GAME_POKER_PLAY_ORDER_ERROR("Order error"), + CODE_GAME_MOVE_NOT_YOUR_TURN("Not your turn"), CODE_GAME_OVER("Game over"), + CODE_GAME_WIN("You win"), + + CODE_GAME_LOSE("You lose"), + + CODE_GAME_DRAW("Draw"), + CODE_PVE_DIFFICULTY_NOT_SUPPORT("AI difficulty not supported"), CODE_GAME_READY("Ready to start game"), diff --git a/landlords-common/src/main/java/org/nico/ratel/landlords/enums/ClientRole.java b/landlords-common/src/main/java/org/nico/ratel/landlords/enums/ClientRole.java index 38ba214..38a4c40 100644 --- a/landlords-common/src/main/java/org/nico/ratel/landlords/enums/ClientRole.java +++ b/landlords-common/src/main/java/org/nico/ratel/landlords/enums/ClientRole.java @@ -4,6 +4,10 @@ public enum ClientRole { PLAYER, - ROBOT + ROBOT, + + BLACK_PLAYER, + + WHITE_PLAYER } diff --git a/landlords-common/src/main/java/org/nico/ratel/landlords/enums/GameResult.java b/landlords-common/src/main/java/org/nico/ratel/landlords/enums/GameResult.java new file mode 100644 index 0000000..8b73c1e --- /dev/null +++ b/landlords-common/src/main/java/org/nico/ratel/landlords/enums/GameResult.java @@ -0,0 +1,28 @@ +package org.nico.ratel.landlords.enums; + +public enum GameResult { + + BLACK_WIN("Black wins"), + + WHITE_WIN("White wins"), + + DRAW("Draw"), + + IN_PROGRESS("Game in progress"), + ; + + private String msg; + + GameResult(String msg) { + this.msg = msg; + } + + public final String getMsg() { + return msg; + } + + public final void setMsg(String msg) { + this.msg = msg; + } + +} \ No newline at end of file diff --git a/landlords-common/src/main/java/org/nico/ratel/landlords/enums/PieceType.java b/landlords-common/src/main/java/org/nico/ratel/landlords/enums/PieceType.java new file mode 100644 index 0000000..e8b5de5 --- /dev/null +++ b/landlords-common/src/main/java/org/nico/ratel/landlords/enums/PieceType.java @@ -0,0 +1,26 @@ +package org.nico.ratel.landlords.enums; + +public enum PieceType { + + EMPTY("Empty"), + + BLACK("Black piece"), + + WHITE("White piece"), + ; + + private String msg; + + PieceType(String msg) { + this.msg = msg; + } + + public final String getMsg() { + return msg; + } + + public final void setMsg(String msg) { + this.msg = msg; + } + +} diff --git a/landlords-common/src/main/java/org/nico/ratel/landlords/enums/SellType.java b/landlords-common/src/main/java/org/nico/ratel/landlords/enums/SellType.java deleted file mode 100644 index 49edb30..0000000 --- a/landlords-common/src/main/java/org/nico/ratel/landlords/enums/SellType.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.nico.ratel.landlords.enums; - -public enum SellType { - - ILLEGAL("Illegal"), - - BOMB("Bomb"), - - KING_BOMB("King bomb"), - - SINGLE("Single card"), - - DOUBLE("Pair"), - - THREE("Three of a kind"), - - THREE_ZONES_SINGLE("Three with single"), - - THREE_ZONES_DOUBLE("Three with pair"), - - FOUR_ZONES_SINGLE("Four with single"), - - FOUR_ZONES_DOUBLE("Four with pair"), - - SINGLE_STRAIGHT("Single straight"), - - DOUBLE_STRAIGHT("Double straight"), - - THREE_STRAIGHT("Triple straight"), - - FOUR_STRAIGHT("Quadruple straight"), - - THREE_STRAIGHT_WITH_SINGLE("Airplane with single cards"), - - THREE_STRAIGHT_WITH_DOUBLE("Airplane with pairs"), - - FOUR_STRAIGHT_WITH_SINGLE("Quadruple straight with single"), - - FOUR_STRAIGHT_WITH_DOUBLE("Quadruple straight with pair"), - ; - - private String msg; - - SellType(String msg) { - this.msg = msg; - } - - public final String getMsg() { - return msg; - } - - public final void setMsg(String msg) { - this.msg = msg; - } - -} diff --git a/landlords-common/src/main/java/org/nico/ratel/landlords/enums/ServerEventCode.java b/landlords-common/src/main/java/org/nico/ratel/landlords/enums/ServerEventCode.java index a87c061..f47d475 100644 --- a/landlords-common/src/main/java/org/nico/ratel/landlords/enums/ServerEventCode.java +++ b/landlords-common/src/main/java/org/nico/ratel/landlords/enums/ServerEventCode.java @@ -12,7 +12,7 @@ public enum ServerEventCode implements Serializable { CODE_CLIENT_NICKNAME_SET("Set nickname"), - CODE_CLIENT_HEAD_BEAT("Pass"), + CODE_CLIENT_HEAD_BEAT("Heartbeat"), CODE_ROOM_CREATE("Create PVP room"), @@ -26,13 +26,9 @@ public enum ServerEventCode implements Serializable { CODE_GAME_READY("Player ready"), - CODE_GAME_LANDLORD_ELECT("Elect landlord"), + CODE_GAME_MOVE("Make move"), - CODE_GAME_POKER_PLAY("Poker play phase"), - - CODE_GAME_POKER_PLAY_REDIRECT("Poker play redirect"), - - CODE_GAME_POKER_PLAY_PASS("Pass"), + CODE_GAME_RESET("Reset game"), CODE_GAME_WATCH("Spectate"), diff --git a/landlords-common/src/main/java/org/nico/ratel/landlords/helper/GomokuHelper.java b/landlords-common/src/main/java/org/nico/ratel/landlords/helper/GomokuHelper.java new file mode 100644 index 0000000..c842a97 --- /dev/null +++ b/landlords-common/src/main/java/org/nico/ratel/landlords/helper/GomokuHelper.java @@ -0,0 +1,140 @@ +package org.nico.ratel.landlords.helper; + +import org.nico.ratel.landlords.entity.Board; +import org.nico.ratel.landlords.entity.GameMove; +import org.nico.ratel.landlords.entity.Room; +import org.nico.ratel.landlords.enums.PieceType; +import org.nico.ratel.landlords.enums.GameResult; + +import java.util.List; +import java.util.ArrayList; + +public class GomokuHelper { + + public static boolean isValidMove(Room room, int row, int col, int playerId) { + // Check if it's the player's turn + if (!room.isPlayerTurn(playerId)) { + return false; + } + + // Check if the position is valid and empty + return room.getGameBoard().isValidMove(row, col); + } + + public static GameResult makeMove(Room room, int row, int col, int playerId) { + PieceType piece = room.getPlayerPiece(playerId); + + if (piece == PieceType.EMPTY) { + return GameResult.IN_PROGRESS; + } + + // Make the move on the board + boolean success = room.getGameBoard().makeMove(row, col, piece); + + if (success) { + // Add to move history + GameMove move = new GameMove(row, col, piece, playerId); + room.addMove(move); + + // Switch turns + room.switchTurn(); + + return room.getGameBoard().getResult(); + } + + return GameResult.IN_PROGRESS; + } + + public static String formatBoardForDisplay(Board board) { + StringBuilder sb = new StringBuilder(); + sb.append(" "); + + // Add column headers + for (int i = 0; i < Board.BOARD_SIZE; i++) { + sb.append(String.format("%2d", i)); + } + sb.append("\n"); + + // Add rows + for (int row = 0; row < Board.BOARD_SIZE; row++) { + sb.append(String.format("%2d", row)); + + for (int col = 0; col < Board.BOARD_SIZE; col++) { + PieceType piece = board.getPiece(row, col); + char symbol = '.'; + + switch (piece) { + case BLACK: + symbol = 'B'; + break; + case WHITE: + symbol = 'W'; + break; + case EMPTY: + symbol = '.'; + break; + } + + sb.append(" ").append(symbol); + } + sb.append("\n"); + } + + return sb.toString(); + } + + public static String formatMoveHistory(List moves) { + if (moves.isEmpty()) { + return "No moves made yet."; + } + + StringBuilder sb = new StringBuilder(); + sb.append("Move History:\n"); + + for (int i = 0; i < moves.size(); i++) { + GameMove move = moves.get(i); + sb.append(String.format("%d. %s at (%d,%d)\n", + i + 1, + move.getPiece().getMsg(), + move.getRow(), + move.getCol())); + } + + return sb.toString(); + } + + public static List getValidMoves(Board board) { + List validMoves = new ArrayList<>(); + + for (int row = 0; row < Board.BOARD_SIZE; row++) { + for (int col = 0; col < Board.BOARD_SIZE; col++) { + if (board.isValidMove(row, col)) { + validMoves.add(row + "," + col); + } + } + } + + return validMoves; + } + + public static boolean isGameOver(Room room) { + return room.getGameBoard().isGameOver(); + } + + public static GameResult getGameResult(Room room) { + return room.getGameBoard().getResult(); + } + + public static String getWinnerMessage(GameResult result) { + switch (result) { + case BLACK_WIN: + return "Black player wins!"; + case WHITE_WIN: + return "White player wins!"; + case DRAW: + return "Game ended in a draw!"; + default: + return "Game in progress..."; + } + } +} \ No newline at end of file diff --git a/landlords-common/src/main/java/org/nico/ratel/landlords/robot/GomokuAI.java b/landlords-common/src/main/java/org/nico/ratel/landlords/robot/GomokuAI.java new file mode 100644 index 0000000..01daec8 --- /dev/null +++ b/landlords-common/src/main/java/org/nico/ratel/landlords/robot/GomokuAI.java @@ -0,0 +1,224 @@ +package org.nico.ratel.landlords.robot; + +import org.nico.ratel.landlords.entity.Board; +import org.nico.ratel.landlords.entity.GameMove; +import org.nico.ratel.landlords.enums.PieceType; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class GomokuAI { + + private final Random random = new Random(); + private final PieceType aiPiece; + private final PieceType opponentPiece; + + public GomokuAI(PieceType aiPiece) { + this.aiPiece = aiPiece; + this.opponentPiece = (aiPiece == PieceType.BLACK) ? PieceType.WHITE : PieceType.BLACK; + } + + public GameMove getNextMove(Board board, int difficulty) { + switch (difficulty) { + case 1: + return getEasyMove(board); + case 2: + return getMediumMove(board); + case 3: + return getHardMove(board); + default: + return getEasyMove(board); + } + } + + private GameMove getEasyMove(Board board) { + // Simple random move + List validMoves = getValidMoves(board); + if (validMoves.isEmpty()) { + return null; + } + + int[] move = validMoves.get(random.nextInt(validMoves.size())); + return new GameMove(move[0], move[1], aiPiece, -1); + } + + private GameMove getMediumMove(Board board) { + // Try to win first, then block opponent + GameMove winMove = findWinningMove(board, aiPiece); + if (winMove != null) { + return winMove; + } + + GameMove blockMove = findWinningMove(board, opponentPiece); + if (blockMove != null) { + blockMove.setPiece(aiPiece); + return blockMove; + } + + // Otherwise, make a strategic move + return getStrategicMove(board); + } + + private GameMove getHardMove(Board board) { + // Advanced AI with threat detection and strategic positioning + GameMove winMove = findWinningMove(board, aiPiece); + if (winMove != null) { + return winMove; + } + + GameMove blockMove = findWinningMove(board, opponentPiece); + if (blockMove != null) { + blockMove.setPiece(aiPiece); + return blockMove; + } + + // Look for threat patterns + GameMove threatMove = findThreatMove(board); + if (threatMove != null) { + return threatMove; + } + + return getStrategicMove(board); + } + + private GameMove findWinningMove(Board board, PieceType piece) { + for (int row = 0; row < Board.BOARD_SIZE; row++) { + for (int col = 0; col < Board.BOARD_SIZE; col++) { + if (board.isValidMove(row, col)) { + // Temporarily place piece + Board tempBoard = copyBoard(board); + tempBoard.makeMove(row, col, piece); + + if (tempBoard.getResult().toString().contains("WIN")) { + return new GameMove(row, col, aiPiece, -1); + } + } + } + } + return null; + } + + private GameMove findThreatMove(Board board) { + // Look for positions that create multiple threats + GameMove bestMove = null; + int maxThreats = 0; + + for (int row = 0; row < Board.BOARD_SIZE; row++) { + for (int col = 0; col < Board.BOARD_SIZE; col++) { + if (board.isValidMove(row, col)) { + int threats = countThreats(board, row, col, aiPiece); + if (threats > maxThreats) { + maxThreats = threats; + bestMove = new GameMove(row, col, aiPiece, -1); + } + } + } + } + + return bestMove; + } + + private int countThreats(Board board, int row, int col, PieceType piece) { + int threats = 0; + + // Check all 4 directions + int[][] directions = {{0, 1}, {1, 0}, {1, 1}, {1, -1}}; + + for (int[] dir : directions) { + int count = 1; // Count the piece we're about to place + + // Count in positive direction + int r = row + dir[0]; + int c = col + dir[1]; + while (r >= 0 && r < Board.BOARD_SIZE && c >= 0 && c < Board.BOARD_SIZE && + board.getPiece(r, c) == piece) { + count++; + r += dir[0]; + c += dir[1]; + } + + // Count in negative direction + r = row - dir[0]; + c = col - dir[1]; + while (r >= 0 && r < Board.BOARD_SIZE && c >= 0 && c < Board.BOARD_SIZE && + board.getPiece(r, c) == piece) { + count++; + r -= dir[0]; + c -= dir[1]; + } + + if (count >= 3) { + threats++; + } + } + + return threats; + } + + private GameMove getStrategicMove(Board board) { + // Prefer center positions and positions near existing pieces + GameMove bestMove = null; + int bestScore = -1; + + for (int row = 0; row < Board.BOARD_SIZE; row++) { + for (int col = 0; col < Board.BOARD_SIZE; col++) { + if (board.isValidMove(row, col)) { + int score = evaluatePosition(board, row, col); + if (score > bestScore) { + bestScore = score; + bestMove = new GameMove(row, col, aiPiece, -1); + } + } + } + } + + return bestMove != null ? bestMove : getEasyMove(board); + } + + private int evaluatePosition(Board board, int row, int col) { + int score = 0; + + // Prefer center positions + int centerDistance = Math.abs(row - Board.BOARD_SIZE / 2) + Math.abs(col - Board.BOARD_SIZE / 2); + score += (Board.BOARD_SIZE - centerDistance) * 2; + + // Prefer positions near existing pieces + for (int r = Math.max(0, row - 2); r <= Math.min(Board.BOARD_SIZE - 1, row + 2); r++) { + for (int c = Math.max(0, col - 2); c <= Math.min(Board.BOARD_SIZE - 1, col + 2); c++) { + if (board.getPiece(r, c) != PieceType.EMPTY) { + score += 10; + } + } + } + + return score; + } + + private List getValidMoves(Board board) { + List moves = new ArrayList<>(); + + for (int row = 0; row < Board.BOARD_SIZE; row++) { + for (int col = 0; col < Board.BOARD_SIZE; col++) { + if (board.isValidMove(row, col)) { + moves.add(new int[]{row, col}); + } + } + } + + return moves; + } + + private Board copyBoard(Board original) { + Board copy = new Board(); + for (int row = 0; row < Board.BOARD_SIZE; row++) { + for (int col = 0; col < Board.BOARD_SIZE; col++) { + PieceType piece = original.getPiece(row, col); + if (piece != PieceType.EMPTY) { + copy.makeMove(row, col, piece); + } + } + } + return copy; + } +} \ No newline at end of file diff --git a/landlords-common/src/main/resources/messages_zh_CN.properties b/landlords-common/src/main/resources/messages_zh_CN.properties index 3f5e4f8..373422e 100644 --- a/landlords-common/src/main/resources/messages_zh_CN.properties +++ b/landlords-common/src/main/resources/messages_zh_CN.properties @@ -1,3 +1,16 @@ pls_select_srv=Please select a server: srv_addr_not_exist=Server address does not exist! -try_connect_%s_failed_%s=Failed to connect to server %s, reason: %s \ No newline at end of file +try_connect_%s_failed_%s=Failed to connect to server %s, reason: %s +game_move_invalid=Invalid move! Please try again. +game_move_occupied=Position already occupied! +game_move_out_of_bounds=Move out of bounds! +game_move_not_your_turn=It's not your turn! +game_black_win=Black player wins! +game_white_win=White player wins! +game_draw=Game ended in a draw! +board_display_title=Current Board: +move_history_title=Move History: +waiting_for_opponent=Waiting for opponent... +your_turn=Your turn! +spectator_joined=Spectator joined the game. +spectator_left=Spectator left the game. \ No newline at end of file diff --git a/landlords-common/src/test/java/org/nico/ratel/landlords/helper/tests/GomokuHelperTest.java b/landlords-common/src/test/java/org/nico/ratel/landlords/helper/tests/GomokuHelperTest.java new file mode 100644 index 0000000..5a85b52 --- /dev/null +++ b/landlords-common/src/test/java/org/nico/ratel/landlords/helper/tests/GomokuHelperTest.java @@ -0,0 +1,143 @@ +package org.nico.ratel.landlords.helper.tests; + +import org.junit.Test; +import org.nico.ratel.landlords.entity.Board; +import org.nico.ratel.landlords.entity.Room; +import org.nico.ratel.landlords.enums.PieceType; +import org.nico.ratel.landlords.enums.GameResult; +import org.nico.ratel.landlords.helper.GomokuHelper; + +import static org.junit.Assert.*; + +public class GomokuHelperTest { + + @Test + public void testHorizontalWin() { + Board board = new Board(); + + // Place 5 black pieces horizontally + for (int col = 0; col < 5; col++) { + assertTrue(board.makeMove(7, col, PieceType.BLACK)); + } + + assertEquals(GameResult.BLACK_WIN, board.getResult()); + } + + @Test + public void testVerticalWin() { + Board board = new Board(); + + // Place 5 black pieces vertically + for (int row = 0; row < 5; row++) { + assertTrue(board.makeMove(row, 7, PieceType.BLACK)); + } + + assertEquals(GameResult.BLACK_WIN, board.getResult()); + } + + @Test + public void testDiagonalWin() { + Board board = new Board(); + + // Place 5 black pieces diagonally + for (int i = 0; i < 5; i++) { + assertTrue(board.makeMove(i, i, PieceType.BLACK)); + } + + assertEquals(GameResult.BLACK_WIN, board.getResult()); + } + + @Test + public void testInvalidMove() { + Board board = new Board(); + + // Place a piece + assertTrue(board.makeMove(7, 7, PieceType.BLACK)); + + // Try to place another piece in the same position + assertFalse(board.makeMove(7, 7, PieceType.WHITE)); + } + + @Test + public void testOutOfBounds() { + Board board = new Board(); + + // Try to place piece outside board + assertFalse(board.makeMove(-1, 7, PieceType.BLACK)); + assertFalse(board.makeMove(7, -1, PieceType.BLACK)); + assertFalse(board.makeMove(15, 7, PieceType.BLACK)); + assertFalse(board.makeMove(7, 15, PieceType.BLACK)); + } + + @Test + public void testRoomPlayerTurn() { + Room room = new Room(1); + room.setBlackPlayerId(100); + room.setWhitePlayerId(200); + + // Initially black player's turn + assertTrue(room.isPlayerTurn(100)); + assertFalse(room.isPlayerTurn(200)); + + // Switch turn + room.switchTurn(); + assertFalse(room.isPlayerTurn(100)); + assertTrue(room.isPlayerTurn(200)); + } + + @Test + public void testGameMoveValidation() { + Room room = new Room(1); + room.setBlackPlayerId(100); + room.setWhitePlayerId(200); + + // Valid move for black player + assertTrue(GomokuHelper.isValidMove(room, 7, 7, 100)); + + // Invalid move for white player (not their turn) + assertFalse(GomokuHelper.isValidMove(room, 7, 8, 200)); + + // Make move and switch turn + GomokuHelper.makeMove(room, 7, 7, 100); + + // Now valid move for white player + assertTrue(GomokuHelper.isValidMove(room, 7, 8, 200)); + + // Invalid move for black player (not their turn) + assertFalse(GomokuHelper.isValidMove(room, 8, 7, 100)); + } + + @Test + public void testBoardFormatting() { + Board board = new Board(); + board.makeMove(7, 7, PieceType.BLACK); + board.makeMove(7, 8, PieceType.WHITE); + + String display = GomokuHelper.formatBoardForDisplay(board); + + // Check that display contains the board + assertNotNull(display); + assertTrue(display.contains("B")); + assertTrue(display.contains("W")); + } + + @Test + public void testGameReset() { + Room room = new Room(1); + room.setBlackPlayerId(100); + room.setWhitePlayerId(200); + + // Make some moves + GomokuHelper.makeMove(room, 7, 7, 100); + GomokuHelper.makeMove(room, 7, 8, 200); + + assertEquals(2, room.getMoveHistory().size()); + + // Reset game + room.resetGame(); + + assertEquals(0, room.getMoveHistory().size()); + assertEquals(GameResult.IN_PROGRESS, room.getGameBoard().getResult()); + assertEquals(PieceType.BLACK, room.getCurrentTurn()); + } +} \ No newline at end of file