first commit
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
package com.xuezhanmaster.game.controller;
|
||||
|
||||
import com.xuezhanmaster.common.api.ApiResponse;
|
||||
import com.xuezhanmaster.game.dto.GameTableSnapshot;
|
||||
import com.xuezhanmaster.game.service.DemoGameService;
|
||||
import com.xuezhanmaster.teaching.dto.TeachingAdviceResponse;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/demo")
|
||||
public class DemoGameController {
|
||||
|
||||
private final DemoGameService demoGameService;
|
||||
|
||||
public DemoGameController(DemoGameService demoGameService) {
|
||||
this.demoGameService = demoGameService;
|
||||
}
|
||||
|
||||
@GetMapping("/table")
|
||||
public ApiResponse<GameTableSnapshot> table() {
|
||||
return ApiResponse.success(demoGameService.createDemoTableSnapshot());
|
||||
}
|
||||
|
||||
@GetMapping("/advice")
|
||||
public ApiResponse<TeachingAdviceResponse> advice() {
|
||||
return ApiResponse.success(demoGameService.createDemoTeachingAdvice());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.xuezhanmaster.game.controller;
|
||||
|
||||
import com.xuezhanmaster.common.api.ApiResponse;
|
||||
import com.xuezhanmaster.game.dto.DiscardTileRequest;
|
||||
import com.xuezhanmaster.game.dto.GameActionRequest;
|
||||
import com.xuezhanmaster.game.dto.GameStateResponse;
|
||||
import com.xuezhanmaster.game.dto.SelectLackSuitRequest;
|
||||
import com.xuezhanmaster.game.dto.StartGameRequest;
|
||||
import com.xuezhanmaster.game.service.GameSessionService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class GameSessionController {
|
||||
|
||||
private final GameSessionService gameSessionService;
|
||||
|
||||
public GameSessionController(GameSessionService gameSessionService) {
|
||||
this.gameSessionService = gameSessionService;
|
||||
}
|
||||
|
||||
@PostMapping("/rooms/{roomId}/start")
|
||||
public ApiResponse<GameStateResponse> start(
|
||||
@PathVariable String roomId,
|
||||
@Valid @RequestBody StartGameRequest request
|
||||
) {
|
||||
return ApiResponse.success(gameSessionService.startGame(roomId, request.operatorUserId()));
|
||||
}
|
||||
|
||||
@GetMapping("/games/{gameId}/state")
|
||||
public ApiResponse<GameStateResponse> state(
|
||||
@PathVariable String gameId,
|
||||
@RequestParam String userId
|
||||
) {
|
||||
return ApiResponse.success(gameSessionService.getState(gameId, userId));
|
||||
}
|
||||
|
||||
@PostMapping("/games/{gameId}/actions")
|
||||
public ApiResponse<GameStateResponse> action(
|
||||
@PathVariable String gameId,
|
||||
@Valid @RequestBody GameActionRequest request
|
||||
) {
|
||||
return ApiResponse.success(gameSessionService.performAction(gameId, request));
|
||||
}
|
||||
|
||||
@PostMapping("/games/{gameId}/lack")
|
||||
public ApiResponse<GameStateResponse> selectLackSuit(
|
||||
@PathVariable String gameId,
|
||||
@Valid @RequestBody SelectLackSuitRequest request
|
||||
) {
|
||||
return ApiResponse.success(gameSessionService.selectLackSuit(gameId, request.userId(), request.lackSuit()));
|
||||
}
|
||||
|
||||
@PostMapping("/games/{gameId}/discard")
|
||||
public ApiResponse<GameStateResponse> discard(
|
||||
@PathVariable String gameId,
|
||||
@Valid @RequestBody DiscardTileRequest request
|
||||
) {
|
||||
return ApiResponse.success(gameSessionService.discardTile(gameId, request.userId(), request.tile()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.xuezhanmaster.game.domain;
|
||||
|
||||
public enum ActionType {
|
||||
SELECT_LACK_SUIT,
|
||||
DRAW,
|
||||
DISCARD,
|
||||
PENG,
|
||||
GANG,
|
||||
HU,
|
||||
PASS
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.xuezhanmaster.game.domain;
|
||||
|
||||
public enum GamePhase {
|
||||
WAITING,
|
||||
LACK_SELECTION,
|
||||
PLAYING,
|
||||
FINISHED
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.xuezhanmaster.game.domain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class GameSeat {
|
||||
|
||||
private final int seatNo;
|
||||
private final boolean ai;
|
||||
private final String playerId;
|
||||
private final String nickname;
|
||||
private final List<Tile> handTiles = new ArrayList<>();
|
||||
private final List<Tile> discardTiles = new ArrayList<>();
|
||||
private TileSuit lackSuit;
|
||||
|
||||
public GameSeat(int seatNo, boolean ai, String nickname) {
|
||||
this(seatNo, ai, UUID.randomUUID().toString(), nickname);
|
||||
}
|
||||
|
||||
public GameSeat(int seatNo, boolean ai, String playerId, String nickname) {
|
||||
this.seatNo = seatNo;
|
||||
this.ai = ai;
|
||||
this.playerId = playerId;
|
||||
this.nickname = nickname;
|
||||
}
|
||||
|
||||
public int getSeatNo() {
|
||||
return seatNo;
|
||||
}
|
||||
|
||||
public boolean isAi() {
|
||||
return ai;
|
||||
}
|
||||
|
||||
public String getPlayerId() {
|
||||
return playerId;
|
||||
}
|
||||
|
||||
public String getNickname() {
|
||||
return nickname;
|
||||
}
|
||||
|
||||
public List<Tile> getHandTiles() {
|
||||
return handTiles;
|
||||
}
|
||||
|
||||
public List<Tile> getDiscardTiles() {
|
||||
return discardTiles;
|
||||
}
|
||||
|
||||
public TileSuit getLackSuit() {
|
||||
return lackSuit;
|
||||
}
|
||||
|
||||
public void setLackSuit(TileSuit lackSuit) {
|
||||
this.lackSuit = lackSuit;
|
||||
}
|
||||
|
||||
public void receiveTile(Tile tile) {
|
||||
handTiles.add(tile);
|
||||
}
|
||||
|
||||
public void discard(Tile tile) {
|
||||
handTiles.remove(tile);
|
||||
discardTiles.add(tile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.xuezhanmaster.game.domain;
|
||||
|
||||
import com.xuezhanmaster.game.event.GameEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class GameSession {
|
||||
|
||||
private final String gameId;
|
||||
private final String roomId;
|
||||
private final Instant createdAt;
|
||||
private final GameTable table;
|
||||
private final List<GameEvent> events;
|
||||
|
||||
public GameSession(String roomId, GameTable table) {
|
||||
this.gameId = UUID.randomUUID().toString();
|
||||
this.roomId = roomId;
|
||||
this.createdAt = Instant.now();
|
||||
this.table = table;
|
||||
this.events = new ArrayList<>();
|
||||
}
|
||||
|
||||
public String getGameId() {
|
||||
return gameId;
|
||||
}
|
||||
|
||||
public String getRoomId() {
|
||||
return roomId;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public GameTable getTable() {
|
||||
return table;
|
||||
}
|
||||
|
||||
public List<GameEvent> getEvents() {
|
||||
return events;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.xuezhanmaster.game.domain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class GameTable {
|
||||
|
||||
private final String tableId;
|
||||
private final List<GameSeat> seats;
|
||||
private final List<Tile> wallTiles;
|
||||
private GamePhase phase;
|
||||
private int dealerSeatNo;
|
||||
private int currentSeatNo;
|
||||
|
||||
public GameTable(List<GameSeat> seats, List<Tile> wallTiles) {
|
||||
this.tableId = UUID.randomUUID().toString();
|
||||
this.seats = new ArrayList<>(seats);
|
||||
this.wallTiles = new ArrayList<>(wallTiles);
|
||||
this.phase = GamePhase.WAITING;
|
||||
this.dealerSeatNo = 0;
|
||||
this.currentSeatNo = 0;
|
||||
}
|
||||
|
||||
public String getTableId() {
|
||||
return tableId;
|
||||
}
|
||||
|
||||
public List<GameSeat> getSeats() {
|
||||
return seats;
|
||||
}
|
||||
|
||||
public List<Tile> getWallTiles() {
|
||||
return wallTiles;
|
||||
}
|
||||
|
||||
public GamePhase getPhase() {
|
||||
return phase;
|
||||
}
|
||||
|
||||
public void setPhase(GamePhase phase) {
|
||||
this.phase = phase;
|
||||
}
|
||||
|
||||
public int getDealerSeatNo() {
|
||||
return dealerSeatNo;
|
||||
}
|
||||
|
||||
public void setDealerSeatNo(int dealerSeatNo) {
|
||||
this.dealerSeatNo = dealerSeatNo;
|
||||
}
|
||||
|
||||
public int getCurrentSeatNo() {
|
||||
return currentSeatNo;
|
||||
}
|
||||
|
||||
public void setCurrentSeatNo(int currentSeatNo) {
|
||||
this.currentSeatNo = currentSeatNo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.xuezhanmaster.game.domain;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class Tile {
|
||||
|
||||
private final TileSuit suit;
|
||||
private final int rank;
|
||||
|
||||
public Tile(TileSuit suit, int rank) {
|
||||
if (rank < 1 || rank > 9) {
|
||||
throw new IllegalArgumentException("牌面点数必须在 1 到 9 之间");
|
||||
}
|
||||
this.suit = Objects.requireNonNull(suit, "花色不能为空");
|
||||
this.rank = rank;
|
||||
}
|
||||
|
||||
public TileSuit getSuit() {
|
||||
return suit;
|
||||
}
|
||||
|
||||
public int getRank() {
|
||||
return rank;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return rank + suit.getLabel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getDisplayName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof Tile tile)) {
|
||||
return false;
|
||||
}
|
||||
return rank == tile.rank && suit == tile.suit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(suit, rank);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.xuezhanmaster.game.domain;
|
||||
|
||||
public enum TileSuit {
|
||||
WAN("万"),
|
||||
TONG("筒"),
|
||||
TIAO("条");
|
||||
|
||||
private final String label;
|
||||
|
||||
TileSuit(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.xuezhanmaster.game.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record DiscardTileRequest(
|
||||
@NotBlank String userId,
|
||||
@NotBlank String tile
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.xuezhanmaster.game.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record GameActionRequest(
|
||||
@NotBlank String userId,
|
||||
@NotBlank String actionType,
|
||||
String tile,
|
||||
Integer sourceSeatNo
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.xuezhanmaster.game.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record GameSeatView(
|
||||
int seatNo,
|
||||
String nickname,
|
||||
boolean ai,
|
||||
String lackSuit,
|
||||
List<String> handTiles,
|
||||
List<String> discardTiles
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.xuezhanmaster.game.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record GameStateResponse(
|
||||
String gameId,
|
||||
String phase,
|
||||
int dealerSeatNo,
|
||||
int currentSeatNo,
|
||||
int remainingWallCount,
|
||||
SelfSeatView selfSeat,
|
||||
List<PublicSeatView> seats
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.xuezhanmaster.game.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record GameTableSnapshot(
|
||||
String tableId,
|
||||
String phase,
|
||||
int dealerSeatNo,
|
||||
int currentSeatNo,
|
||||
int remainingWallCount,
|
||||
List<GameSeatView> seats
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.xuezhanmaster.game.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record PublicSeatView(
|
||||
int seatNo,
|
||||
String playerId,
|
||||
String nickname,
|
||||
boolean ai,
|
||||
String lackSuit,
|
||||
int handCount,
|
||||
List<String> discardTiles
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.xuezhanmaster.game.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record SelectLackSuitRequest(
|
||||
@NotBlank String userId,
|
||||
@NotBlank String lackSuit
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.xuezhanmaster.game.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record SelfSeatView(
|
||||
int seatNo,
|
||||
String playerId,
|
||||
String nickname,
|
||||
String lackSuit,
|
||||
List<String> handTiles,
|
||||
List<String> discardTiles
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.xuezhanmaster.game.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record StartGameRequest(
|
||||
@NotBlank String operatorUserId
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.xuezhanmaster.game.engine;
|
||||
|
||||
import com.xuezhanmaster.game.domain.GamePhase;
|
||||
import com.xuezhanmaster.game.domain.GameSeat;
|
||||
import com.xuezhanmaster.game.domain.GameTable;
|
||||
import com.xuezhanmaster.game.domain.Tile;
|
||||
import com.xuezhanmaster.game.domain.TileSuit;
|
||||
import com.xuezhanmaster.game.service.DeckFactory;
|
||||
import com.xuezhanmaster.room.domain.Room;
|
||||
import com.xuezhanmaster.room.domain.RoomSeat;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class GameEngine {
|
||||
|
||||
private final DeckFactory deckFactory;
|
||||
|
||||
public GameEngine(DeckFactory deckFactory) {
|
||||
this.deckFactory = deckFactory;
|
||||
}
|
||||
|
||||
public GameTable createDemoTable() {
|
||||
List<GameSeat> seats = List.of(
|
||||
new GameSeat(0, false, "Player"),
|
||||
new GameSeat(1, true, "AI-Jifeng"),
|
||||
new GameSeat(2, true, "AI-Duanyao"),
|
||||
new GameSeat(3, true, "AI-Shouzhuo")
|
||||
);
|
||||
|
||||
List<Tile> wallTiles = new ArrayList<>(deckFactory.createShuffledWall());
|
||||
GameTable table = new GameTable(seats, wallTiles);
|
||||
table.setPhase(GamePhase.LACK_SELECTION);
|
||||
table.setDealerSeatNo(0);
|
||||
table.setCurrentSeatNo(0);
|
||||
|
||||
dealInitialHands(table);
|
||||
assignDemoLackSuits(table);
|
||||
return table;
|
||||
}
|
||||
|
||||
public GameTable createTableFromRoom(Room room) {
|
||||
List<GameSeat> seats = room.getSeats().stream()
|
||||
.map(this::toGameSeat)
|
||||
.toList();
|
||||
|
||||
List<Tile> wallTiles = new ArrayList<>(deckFactory.createShuffledWall());
|
||||
GameTable table = new GameTable(seats, wallTiles);
|
||||
table.setPhase(GamePhase.LACK_SELECTION);
|
||||
table.setDealerSeatNo(0);
|
||||
table.setCurrentSeatNo(0);
|
||||
dealInitialHands(table);
|
||||
autoAssignBotLackSuits(table);
|
||||
return table;
|
||||
}
|
||||
|
||||
private void dealInitialHands(GameTable table) {
|
||||
for (int round = 0; round < 13; round++) {
|
||||
for (GameSeat seat : table.getSeats()) {
|
||||
seat.receiveTile(drawOne(table));
|
||||
}
|
||||
}
|
||||
table.getSeats().get(table.getDealerSeatNo()).receiveTile(drawOne(table));
|
||||
}
|
||||
|
||||
private Tile drawOne(GameTable table) {
|
||||
return table.getWallTiles().remove(0);
|
||||
}
|
||||
|
||||
private void assignDemoLackSuits(GameTable table) {
|
||||
TileSuit[] lackOrder = {TileSuit.TIAO, TileSuit.WAN, TileSuit.TONG, TileSuit.TIAO};
|
||||
for (int i = 0; i < table.getSeats().size(); i++) {
|
||||
table.getSeats().get(i).setLackSuit(lackOrder[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private GameSeat toGameSeat(RoomSeat roomSeat) {
|
||||
return new GameSeat(
|
||||
roomSeat.getSeatNo(),
|
||||
roomSeat.getParticipantType().name().equals("BOT"),
|
||||
roomSeat.getUserId() == null ? "bot-" + roomSeat.getSeatNo() : roomSeat.getUserId(),
|
||||
roomSeat.getDisplayName()
|
||||
);
|
||||
}
|
||||
|
||||
private void autoAssignBotLackSuits(GameTable table) {
|
||||
for (GameSeat seat : table.getSeats()) {
|
||||
if (seat.isAi()) {
|
||||
seat.setLackSuit(chooseLackSuit(seat));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private TileSuit chooseLackSuit(GameSeat seat) {
|
||||
int wanCount = countSuit(seat, TileSuit.WAN);
|
||||
int tongCount = countSuit(seat, TileSuit.TONG);
|
||||
int tiaoCount = countSuit(seat, TileSuit.TIAO);
|
||||
|
||||
TileSuit lackSuit = TileSuit.WAN;
|
||||
int minCount = wanCount;
|
||||
if (tongCount < minCount) {
|
||||
lackSuit = TileSuit.TONG;
|
||||
minCount = tongCount;
|
||||
}
|
||||
if (tiaoCount < minCount) {
|
||||
lackSuit = TileSuit.TIAO;
|
||||
}
|
||||
return lackSuit;
|
||||
}
|
||||
|
||||
private int countSuit(GameSeat seat, TileSuit suit) {
|
||||
int count = 0;
|
||||
for (Tile tile : seat.getHandTiles()) {
|
||||
if (tile.getSuit() == suit) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.xuezhanmaster.game.event;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public record GameEvent(
|
||||
String gameId,
|
||||
GameEventType eventType,
|
||||
Integer seatNo,
|
||||
Map<String, Object> payload,
|
||||
Instant createdAt
|
||||
) {
|
||||
public static GameEvent of(String gameId, GameEventType eventType, Integer seatNo, Map<String, Object> payload) {
|
||||
return new GameEvent(gameId, eventType, seatNo, payload, Instant.now());
|
||||
}
|
||||
|
||||
public static GameEvent gameStarted(String gameId, String roomId) {
|
||||
return of(gameId, GameEventType.GAME_STARTED, null, Map.of(
|
||||
"roomId", roomId
|
||||
));
|
||||
}
|
||||
|
||||
public static GameEvent lackSelected(String gameId, int seatNo, String lackSuit) {
|
||||
return of(gameId, GameEventType.LACK_SELECTED, seatNo, Map.of(
|
||||
"lackSuit", lackSuit
|
||||
));
|
||||
}
|
||||
|
||||
public static GameEvent phaseChanged(String gameId, String phase) {
|
||||
return of(gameId, GameEventType.GAME_PHASE_CHANGED, null, Map.of(
|
||||
"phase", phase
|
||||
));
|
||||
}
|
||||
|
||||
public static GameEvent tileDiscarded(String gameId, int seatNo, String tile) {
|
||||
return of(gameId, GameEventType.TILE_DISCARDED, seatNo, Map.of(
|
||||
"tile", tile
|
||||
));
|
||||
}
|
||||
|
||||
public static GameEvent tileDrawn(String gameId, int seatNo, int remainingWallCount) {
|
||||
return of(gameId, GameEventType.TILE_DRAWN, seatNo, Map.of(
|
||||
"remainingWallCount", remainingWallCount
|
||||
));
|
||||
}
|
||||
|
||||
public static GameEvent turnSwitched(String gameId, int seatNo) {
|
||||
return of(gameId, GameEventType.TURN_SWITCHED, seatNo, Map.of(
|
||||
"currentSeatNo", seatNo
|
||||
));
|
||||
}
|
||||
|
||||
public static GameEvent responseWindowOpened(String gameId, int seatNo, int sourceSeatNo, String triggerTile) {
|
||||
return of(gameId, GameEventType.RESPONSE_WINDOW_OPENED, seatNo, buildActionPayload(
|
||||
"RESPONSE_WINDOW_OPENED",
|
||||
sourceSeatNo,
|
||||
triggerTile
|
||||
));
|
||||
}
|
||||
|
||||
public static GameEvent responseWindowClosed(String gameId, Integer seatNo, int sourceSeatNo, String resolvedActionType) {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("sourceSeatNo", sourceSeatNo);
|
||||
payload.put("resolvedActionType", resolvedActionType);
|
||||
return of(gameId, GameEventType.RESPONSE_WINDOW_CLOSED, seatNo, payload);
|
||||
}
|
||||
|
||||
public static GameEvent responseActionDeclared(
|
||||
String gameId,
|
||||
GameEventType eventType,
|
||||
int seatNo,
|
||||
int sourceSeatNo,
|
||||
String tile
|
||||
) {
|
||||
return of(gameId, eventType, seatNo, buildActionPayload(
|
||||
toActionType(eventType),
|
||||
sourceSeatNo,
|
||||
tile
|
||||
));
|
||||
}
|
||||
|
||||
private static Map<String, Object> buildActionPayload(String actionType, int sourceSeatNo, String tile) {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("actionType", actionType);
|
||||
payload.put("sourceSeatNo", sourceSeatNo);
|
||||
if (tile != null && !tile.isBlank()) {
|
||||
payload.put("tile", tile);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
private static String toActionType(GameEventType eventType) {
|
||||
return switch (eventType) {
|
||||
case PENG_DECLARED -> "PENG";
|
||||
case GANG_DECLARED -> "GANG";
|
||||
case HU_DECLARED -> "HU";
|
||||
case PASS_DECLARED -> "PASS";
|
||||
default -> eventType.name();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.xuezhanmaster.game.event;
|
||||
|
||||
public enum GameEventType {
|
||||
GAME_STARTED,
|
||||
LACK_SELECTED,
|
||||
GAME_PHASE_CHANGED,
|
||||
TILE_DISCARDED,
|
||||
TILE_DRAWN,
|
||||
TURN_SWITCHED,
|
||||
RESPONSE_WINDOW_OPENED,
|
||||
RESPONSE_WINDOW_CLOSED,
|
||||
PENG_DECLARED,
|
||||
GANG_DECLARED,
|
||||
HU_DECLARED,
|
||||
PASS_DECLARED,
|
||||
ACTION_REQUIRED
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.xuezhanmaster.game.service;
|
||||
|
||||
import com.xuezhanmaster.game.domain.Tile;
|
||||
import com.xuezhanmaster.game.domain.TileSuit;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
@Component
|
||||
public class DeckFactory {
|
||||
|
||||
public List<Tile> createShuffledWall() {
|
||||
List<Tile> wall = new ArrayList<>(108);
|
||||
for (TileSuit suit : TileSuit.values()) {
|
||||
for (int rank = 1; rank <= 9; rank++) {
|
||||
for (int copy = 0; copy < 4; copy++) {
|
||||
wall.add(new Tile(suit, rank));
|
||||
}
|
||||
}
|
||||
}
|
||||
Collections.shuffle(wall, new Random());
|
||||
return wall;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.xuezhanmaster.game.service;
|
||||
|
||||
import com.xuezhanmaster.game.domain.GameTable;
|
||||
import com.xuezhanmaster.game.domain.Tile;
|
||||
import com.xuezhanmaster.game.dto.GameSeatView;
|
||||
import com.xuezhanmaster.game.dto.GameTableSnapshot;
|
||||
import com.xuezhanmaster.game.engine.GameEngine;
|
||||
import com.xuezhanmaster.strategy.domain.StrategyDecision;
|
||||
import com.xuezhanmaster.strategy.service.StrategyService;
|
||||
import com.xuezhanmaster.teaching.domain.PlayerVisibleGameState;
|
||||
import com.xuezhanmaster.teaching.domain.TeachingMode;
|
||||
import com.xuezhanmaster.teaching.dto.TeachingAdviceResponse;
|
||||
import com.xuezhanmaster.teaching.service.PlayerVisibilityService;
|
||||
import com.xuezhanmaster.teaching.service.TeachingService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class DemoGameService {
|
||||
|
||||
private final GameEngine gameEngine;
|
||||
private final StrategyService strategyService;
|
||||
private final PlayerVisibilityService playerVisibilityService;
|
||||
private final TeachingService teachingService;
|
||||
|
||||
public DemoGameService(
|
||||
GameEngine gameEngine,
|
||||
StrategyService strategyService,
|
||||
PlayerVisibilityService playerVisibilityService,
|
||||
TeachingService teachingService
|
||||
) {
|
||||
this.gameEngine = gameEngine;
|
||||
this.strategyService = strategyService;
|
||||
this.playerVisibilityService = playerVisibilityService;
|
||||
this.teachingService = teachingService;
|
||||
}
|
||||
|
||||
public GameTableSnapshot createDemoTableSnapshot() {
|
||||
GameTable table = gameEngine.createDemoTable();
|
||||
return new GameTableSnapshot(
|
||||
table.getTableId(),
|
||||
table.getPhase().name(),
|
||||
table.getDealerSeatNo(),
|
||||
table.getCurrentSeatNo(),
|
||||
table.getWallTiles().size(),
|
||||
table.getSeats().stream()
|
||||
.map(seat -> new GameSeatView(
|
||||
seat.getSeatNo(),
|
||||
seat.getNickname(),
|
||||
seat.isAi(),
|
||||
seat.getLackSuit() == null ? null : seat.getLackSuit().name(),
|
||||
seat.getHandTiles().stream().map(Tile::getDisplayName).toList(),
|
||||
seat.getDiscardTiles().stream().map(Tile::getDisplayName).toList()
|
||||
))
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
|
||||
public TeachingAdviceResponse createDemoTeachingAdvice() {
|
||||
GameTable table = gameEngine.createDemoTable();
|
||||
PlayerVisibleGameState visibleState = playerVisibilityService.buildVisibleState(table, table.getSeats().get(0));
|
||||
StrategyDecision decision = strategyService.evaluateDiscard(visibleState);
|
||||
return teachingService.buildAdvice(decision, TeachingMode.STANDARD);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package com.xuezhanmaster.game.service;
|
||||
|
||||
import com.xuezhanmaster.common.exception.BusinessException;
|
||||
import com.xuezhanmaster.game.domain.ActionType;
|
||||
import com.xuezhanmaster.game.domain.GamePhase;
|
||||
import com.xuezhanmaster.game.domain.GameSeat;
|
||||
import com.xuezhanmaster.game.domain.GameSession;
|
||||
import com.xuezhanmaster.game.domain.GameTable;
|
||||
import com.xuezhanmaster.game.domain.Tile;
|
||||
import com.xuezhanmaster.game.domain.TileSuit;
|
||||
import com.xuezhanmaster.game.dto.GameActionRequest;
|
||||
import com.xuezhanmaster.game.event.GameEvent;
|
||||
import com.xuezhanmaster.game.event.GameEventType;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class GameActionProcessor {
|
||||
|
||||
public List<GameEvent> process(GameSession session, GameActionRequest request) {
|
||||
ActionType actionType = parseActionType(request.actionType());
|
||||
return switch (actionType) {
|
||||
case SELECT_LACK_SUIT -> selectLackSuit(session, request.userId(), request.tile());
|
||||
case DISCARD -> discard(session, request.userId(), request.tile());
|
||||
case PENG -> peng(session, request.userId(), request.tile(), request.sourceSeatNo());
|
||||
case GANG -> gang(session, request.userId(), request.tile(), request.sourceSeatNo());
|
||||
case HU -> hu(session, request.userId(), request.tile(), request.sourceSeatNo());
|
||||
case PASS -> pass(session, request.userId(), request.sourceSeatNo());
|
||||
default -> throw new BusinessException("GAME_ACTION_UNSUPPORTED", "当前动作尚未实现");
|
||||
};
|
||||
}
|
||||
|
||||
private List<GameEvent> peng(GameSession session, String userId, String tileDisplayName, Integer sourceSeatNo) {
|
||||
GameTable table = session.getTable();
|
||||
GameSeat seat = findSeatByUserId(table, userId);
|
||||
validateResponseAction(table, seat, sourceSeatNo, tileDisplayName, "碰");
|
||||
return unsupportedAction(ActionType.PENG);
|
||||
}
|
||||
|
||||
private List<GameEvent> gang(GameSession session, String userId, String tileDisplayName, Integer sourceSeatNo) {
|
||||
GameTable table = session.getTable();
|
||||
GameSeat seat = findSeatByUserId(table, userId);
|
||||
validateResponseAction(table, seat, sourceSeatNo, tileDisplayName, "杠");
|
||||
return unsupportedAction(ActionType.GANG);
|
||||
}
|
||||
|
||||
private List<GameEvent> hu(GameSession session, String userId, String tileDisplayName, Integer sourceSeatNo) {
|
||||
GameTable table = session.getTable();
|
||||
GameSeat seat = findSeatByUserId(table, userId);
|
||||
validateResponseAction(table, seat, sourceSeatNo, tileDisplayName, "胡");
|
||||
return unsupportedAction(ActionType.HU);
|
||||
}
|
||||
|
||||
private List<GameEvent> pass(GameSession session, String userId, Integer sourceSeatNo) {
|
||||
GameTable table = session.getTable();
|
||||
GameSeat seat = findSeatByUserId(table, userId);
|
||||
validateResponseAction(table, seat, sourceSeatNo, null, "过", false);
|
||||
return unsupportedAction(ActionType.PASS);
|
||||
}
|
||||
|
||||
private List<GameEvent> unsupportedAction(ActionType actionType) {
|
||||
throw new BusinessException("GAME_ACTION_UNSUPPORTED", "当前动作尚未实现: " + actionType.name());
|
||||
}
|
||||
|
||||
private List<GameEvent> selectLackSuit(GameSession session, String userId, String lackSuitValue) {
|
||||
GameTable table = session.getTable();
|
||||
if (table.getPhase() != GamePhase.LACK_SELECTION) {
|
||||
throw new BusinessException("GAME_PHASE_INVALID", "当前阶段不允许选择定缺");
|
||||
}
|
||||
|
||||
GameSeat seat = findSeatByUserId(table, userId);
|
||||
TileSuit lackSuit = parseSuit(lackSuitValue);
|
||||
seat.setLackSuit(lackSuit);
|
||||
|
||||
List<GameEvent> events = new ArrayList<>();
|
||||
events.add(GameEvent.lackSelected(session.getGameId(), seat.getSeatNo(), lackSuit.name()));
|
||||
|
||||
if (allSeatsSelectedLack(table)) {
|
||||
table.setPhase(GamePhase.PLAYING);
|
||||
events.add(GameEvent.phaseChanged(session.getGameId(), table.getPhase().name()));
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
private List<GameEvent> discard(GameSession session, String userId, String tileDisplayName) {
|
||||
GameTable table = session.getTable();
|
||||
if (table.getPhase() != GamePhase.PLAYING) {
|
||||
throw new BusinessException("GAME_PHASE_INVALID", "当前阶段不允许出牌");
|
||||
}
|
||||
|
||||
GameSeat seat = findSeatByUserId(table, userId);
|
||||
if (seat.getSeatNo() != table.getCurrentSeatNo()) {
|
||||
throw new BusinessException("GAME_TURN_INVALID", "当前不是该玩家的出牌回合");
|
||||
}
|
||||
|
||||
Tile tile = findTileInHand(seat, tileDisplayName);
|
||||
seat.discard(tile);
|
||||
|
||||
List<GameEvent> events = new ArrayList<>();
|
||||
events.add(GameEvent.tileDiscarded(session.getGameId(), seat.getSeatNo(), tile.getDisplayName()));
|
||||
return events;
|
||||
}
|
||||
|
||||
private ActionType parseActionType(String value) {
|
||||
try {
|
||||
return ActionType.valueOf(value.toUpperCase());
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new BusinessException("GAME_ACTION_INVALID", "动作类型不合法");
|
||||
}
|
||||
}
|
||||
|
||||
private GameSeat findSeatByUserId(GameTable table, String userId) {
|
||||
return table.getSeats().stream()
|
||||
.filter(seat -> seat.getPlayerId().equals(userId))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new BusinessException("GAME_SEAT_NOT_FOUND", "当前玩家不在对局中"));
|
||||
}
|
||||
|
||||
private TileSuit parseSuit(String value) {
|
||||
try {
|
||||
return TileSuit.valueOf(value.toUpperCase());
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new BusinessException("LACK_SUIT_INVALID", "定缺花色不合法");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean allSeatsSelectedLack(GameTable table) {
|
||||
for (GameSeat seat : table.getSeats()) {
|
||||
if (seat.getLackSuit() == null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Tile findTileInHand(GameSeat seat, String tileDisplayName) {
|
||||
return seat.getHandTiles().stream()
|
||||
.filter(tile -> tile.getDisplayName().equals(tileDisplayName))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new BusinessException("GAME_TILE_NOT_FOUND", "指定牌不在当前手牌中"));
|
||||
}
|
||||
|
||||
private void validateResponseAction(
|
||||
GameTable table,
|
||||
GameSeat actorSeat,
|
||||
Integer sourceSeatNo,
|
||||
String tileDisplayName,
|
||||
String actionName
|
||||
) {
|
||||
validateResponseAction(table, actorSeat, sourceSeatNo, tileDisplayName, actionName, true);
|
||||
}
|
||||
|
||||
private void validateResponseAction(
|
||||
GameTable table,
|
||||
GameSeat actorSeat,
|
||||
Integer sourceSeatNo,
|
||||
String tileDisplayName,
|
||||
String actionName,
|
||||
boolean requiresTile
|
||||
) {
|
||||
if (table.getPhase() != GamePhase.PLAYING) {
|
||||
throw new BusinessException("GAME_PHASE_INVALID", "当前阶段不允许" + actionName);
|
||||
}
|
||||
if (sourceSeatNo == null) {
|
||||
throw new BusinessException("GAME_ACTION_PARAM_INVALID", actionName + "动作缺少来源座位");
|
||||
}
|
||||
if (sourceSeatNo < 0 || sourceSeatNo >= table.getSeats().size()) {
|
||||
throw new BusinessException("GAME_ACTION_PARAM_INVALID", actionName + "动作来源座位不合法");
|
||||
}
|
||||
if (sourceSeatNo == actorSeat.getSeatNo()) {
|
||||
throw new BusinessException("GAME_ACTION_PARAM_INVALID", actionName + "动作来源座位不能是自己");
|
||||
}
|
||||
if (requiresTile && isBlank(tileDisplayName)) {
|
||||
throw new BusinessException("GAME_ACTION_PARAM_INVALID", actionName + "动作缺少目标牌");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isBlank(String value) {
|
||||
return value == null || value.isBlank();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
package com.xuezhanmaster.game.service;
|
||||
|
||||
import com.xuezhanmaster.common.exception.BusinessException;
|
||||
import com.xuezhanmaster.game.domain.ActionType;
|
||||
import com.xuezhanmaster.game.domain.GamePhase;
|
||||
import com.xuezhanmaster.game.domain.GameSeat;
|
||||
import com.xuezhanmaster.game.domain.GameSession;
|
||||
import com.xuezhanmaster.game.domain.GameTable;
|
||||
import com.xuezhanmaster.game.domain.Tile;
|
||||
import com.xuezhanmaster.game.dto.GameActionRequest;
|
||||
import com.xuezhanmaster.game.dto.GameStateResponse;
|
||||
import com.xuezhanmaster.game.dto.PublicSeatView;
|
||||
import com.xuezhanmaster.game.dto.SelfSeatView;
|
||||
import com.xuezhanmaster.game.engine.GameEngine;
|
||||
import com.xuezhanmaster.game.event.GameEvent;
|
||||
import com.xuezhanmaster.game.event.GameEventType;
|
||||
import com.xuezhanmaster.room.domain.Room;
|
||||
import com.xuezhanmaster.room.service.RoomService;
|
||||
import com.xuezhanmaster.strategy.domain.StrategyDecision;
|
||||
import com.xuezhanmaster.strategy.service.StrategyService;
|
||||
import com.xuezhanmaster.teaching.domain.PlayerVisibleGameState;
|
||||
import com.xuezhanmaster.teaching.domain.TeachingMode;
|
||||
import com.xuezhanmaster.teaching.dto.TeachingAdviceResponse;
|
||||
import com.xuezhanmaster.teaching.service.PlayerVisibilityService;
|
||||
import com.xuezhanmaster.teaching.service.TeachingService;
|
||||
import com.xuezhanmaster.ws.service.GameMessagePublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Service
|
||||
public class GameSessionService {
|
||||
|
||||
private final RoomService roomService;
|
||||
private final GameEngine gameEngine;
|
||||
private final GameActionProcessor gameActionProcessor;
|
||||
private final StrategyService strategyService;
|
||||
private final PlayerVisibilityService playerVisibilityService;
|
||||
private final TeachingService teachingService;
|
||||
private final GameMessagePublisher gameMessagePublisher;
|
||||
private final Map<String, GameSession> sessions = new ConcurrentHashMap<>();
|
||||
|
||||
public GameSessionService(
|
||||
RoomService roomService,
|
||||
GameEngine gameEngine,
|
||||
GameActionProcessor gameActionProcessor,
|
||||
StrategyService strategyService,
|
||||
PlayerVisibilityService playerVisibilityService,
|
||||
TeachingService teachingService,
|
||||
GameMessagePublisher gameMessagePublisher
|
||||
) {
|
||||
this.roomService = roomService;
|
||||
this.gameEngine = gameEngine;
|
||||
this.gameActionProcessor = gameActionProcessor;
|
||||
this.strategyService = strategyService;
|
||||
this.playerVisibilityService = playerVisibilityService;
|
||||
this.teachingService = teachingService;
|
||||
this.gameMessagePublisher = gameMessagePublisher;
|
||||
}
|
||||
|
||||
public GameStateResponse startGame(String roomId, String operatorUserId) {
|
||||
Room room = roomService.prepareRoomForGame(roomId, operatorUserId);
|
||||
GameTable table = gameEngine.createTableFromRoom(room);
|
||||
GameSession session = new GameSession(room.getRoomId(), table);
|
||||
sessions.put(session.getGameId(), session);
|
||||
|
||||
appendAndPublish(session, GameEvent.gameStarted(session.getGameId(), roomId));
|
||||
notifyActionIfHumanTurn(session);
|
||||
return toStateResponse(session, operatorUserId);
|
||||
}
|
||||
|
||||
public GameStateResponse getState(String gameId, String userId) {
|
||||
GameSession session = getRequiredSession(gameId);
|
||||
return toStateResponse(session, userId);
|
||||
}
|
||||
|
||||
public GameStateResponse performAction(String gameId, GameActionRequest request) {
|
||||
GameSession session = getRequiredSession(gameId);
|
||||
ActionType actionType = parseActionType(request.actionType());
|
||||
List<GameEvent> events = gameActionProcessor.process(session, request);
|
||||
appendAndPublish(session, events);
|
||||
handlePostActionEffects(session, actionType);
|
||||
return toStateResponse(session, request.userId());
|
||||
}
|
||||
|
||||
public GameStateResponse selectLackSuit(String gameId, String userId, String lackSuitValue) {
|
||||
return performAction(gameId, new GameActionRequest(userId, "SELECT_LACK_SUIT", lackSuitValue, null));
|
||||
}
|
||||
|
||||
public GameStateResponse discardTile(String gameId, String userId, String tileDisplayName) {
|
||||
return performAction(gameId, new GameActionRequest(userId, "DISCARD", tileDisplayName, null));
|
||||
}
|
||||
|
||||
private GameSession getRequiredSession(String gameId) {
|
||||
GameSession session = sessions.get(gameId);
|
||||
if (session == null) {
|
||||
throw new BusinessException("GAME_NOT_FOUND", "对局不存在");
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
private GameSeat findSeatByUserId(GameTable table, String userId) {
|
||||
return table.getSeats().stream()
|
||||
.filter(seat -> seat.getPlayerId().equals(userId))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new BusinessException("GAME_SEAT_NOT_FOUND", "当前玩家不在对局中"));
|
||||
}
|
||||
|
||||
private GameStateResponse toStateResponse(GameSession session, String userId) {
|
||||
GameTable table = session.getTable();
|
||||
GameSeat self = findSeatByUserId(table, userId);
|
||||
|
||||
return new GameStateResponse(
|
||||
session.getGameId(),
|
||||
table.getPhase().name(),
|
||||
table.getDealerSeatNo(),
|
||||
table.getCurrentSeatNo(),
|
||||
table.getWallTiles().size(),
|
||||
new SelfSeatView(
|
||||
self.getSeatNo(),
|
||||
self.getPlayerId(),
|
||||
self.getNickname(),
|
||||
self.getLackSuit() == null ? null : self.getLackSuit().name(),
|
||||
self.getHandTiles().stream().map(Tile::getDisplayName).toList(),
|
||||
self.getDiscardTiles().stream().map(Tile::getDisplayName).toList()
|
||||
),
|
||||
buildPublicSeats(table)
|
||||
);
|
||||
}
|
||||
|
||||
private void moveToNextSeat(GameTable table, String gameId) {
|
||||
int nextSeatNo = (table.getCurrentSeatNo() + 1) % table.getSeats().size();
|
||||
GameSeat nextSeat = table.getSeats().get(nextSeatNo);
|
||||
if (table.getWallTiles().isEmpty()) {
|
||||
table.setPhase(GamePhase.FINISHED);
|
||||
gameMessagePublisher.publishPublicEvent(GameEvent.phaseChanged(gameId, table.getPhase().name()));
|
||||
return;
|
||||
}
|
||||
|
||||
Tile drawnTile = table.getWallTiles().remove(0);
|
||||
nextSeat.receiveTile(drawnTile);
|
||||
table.setCurrentSeatNo(nextSeatNo);
|
||||
gameMessagePublisher.publishPublicEvent(GameEvent.tileDrawn(gameId, nextSeatNo, table.getWallTiles().size()));
|
||||
gameMessagePublisher.publishPublicEvent(GameEvent.turnSwitched(gameId, nextSeatNo));
|
||||
}
|
||||
|
||||
private void autoPlayBots(GameSession session) {
|
||||
GameTable table = session.getTable();
|
||||
while (table.getPhase() == GamePhase.PLAYING) {
|
||||
GameSeat currentSeat = table.getSeats().get(table.getCurrentSeatNo());
|
||||
if (!currentSeat.isAi()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerVisibleGameState visibleState = playerVisibilityService.buildVisibleState(table, currentSeat);
|
||||
StrategyDecision decision = strategyService.evaluateDiscard(visibleState);
|
||||
List<GameEvent> events = gameActionProcessor.process(
|
||||
session,
|
||||
new GameActionRequest(
|
||||
currentSeat.getPlayerId(),
|
||||
"DISCARD",
|
||||
decision.recommendedAction().getTile().getDisplayName(),
|
||||
null
|
||||
)
|
||||
);
|
||||
appendAndPublish(session, events);
|
||||
moveToNextSeat(table, session.getGameId());
|
||||
if (table.getPhase() == GamePhase.FINISHED) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void appendAndPublish(GameSession session, GameEvent event) {
|
||||
session.getEvents().add(event);
|
||||
gameMessagePublisher.publishPublicEvent(event);
|
||||
}
|
||||
|
||||
private void appendAndPublish(GameSession session, List<GameEvent> events) {
|
||||
for (GameEvent event : events) {
|
||||
appendAndPublish(session, event);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyActionIfHumanTurn(GameSession session) {
|
||||
if (session.getTable().getPhase() != GamePhase.PLAYING) {
|
||||
return;
|
||||
}
|
||||
|
||||
GameSeat currentSeat = session.getTable().getSeats().get(session.getTable().getCurrentSeatNo());
|
||||
if (currentSeat.isAi()) {
|
||||
return;
|
||||
}
|
||||
|
||||
gameMessagePublisher.publishPrivateActionRequired(
|
||||
session.getGameId(),
|
||||
currentSeat.getPlayerId(),
|
||||
List.of("DISCARD"),
|
||||
currentSeat.getSeatNo()
|
||||
);
|
||||
|
||||
PlayerVisibleGameState visibleState = playerVisibilityService.buildVisibleState(session.getTable(), currentSeat);
|
||||
StrategyDecision decision = strategyService.evaluateDiscard(visibleState);
|
||||
TeachingAdviceResponse advice = teachingService.buildAdvice(decision, TeachingMode.BRIEF);
|
||||
gameMessagePublisher.publishPrivateTeaching(
|
||||
session.getGameId(),
|
||||
currentSeat.getPlayerId(),
|
||||
advice.teachingMode(),
|
||||
advice.recommendedAction(),
|
||||
advice.explanation()
|
||||
);
|
||||
}
|
||||
|
||||
private void handlePostActionEffects(GameSession session, ActionType actionType) {
|
||||
switch (actionType) {
|
||||
case SELECT_LACK_SUIT -> {
|
||||
if (session.getTable().getPhase() == GamePhase.PLAYING) {
|
||||
notifyActionIfHumanTurn(session);
|
||||
}
|
||||
}
|
||||
case DISCARD -> {
|
||||
moveToNextSeat(session.getTable(), session.getGameId());
|
||||
autoPlayBots(session);
|
||||
notifyActionIfHumanTurn(session);
|
||||
}
|
||||
default -> {
|
||||
// 其余动作在后续 Sprint 中补充对应副作用
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ActionType parseActionType(String actionType) {
|
||||
try {
|
||||
return ActionType.valueOf(actionType.toUpperCase(Locale.ROOT));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new BusinessException("GAME_ACTION_INVALID", "动作类型不合法");
|
||||
}
|
||||
}
|
||||
|
||||
private List<PublicSeatView> buildPublicSeats(GameTable table) {
|
||||
return table.getSeats().stream()
|
||||
.map(seat -> new PublicSeatView(
|
||||
seat.getSeatNo(),
|
||||
seat.getPlayerId(),
|
||||
seat.getNickname(),
|
||||
seat.isAi(),
|
||||
seat.getLackSuit() == null ? null : seat.getLackSuit().name(),
|
||||
seat.getHandTiles().size(),
|
||||
seat.getDiscardTiles().stream().map(Tile::getDisplayName).toList()
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user