first commit

This commit is contained in:
hujun
2026-03-20 12:50:41 +08:00
commit 24fce055fd
88 changed files with 7655 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
package com.xuezhanmaster.game.event;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class GameEventTest {
@Test
void shouldBuildResponseActionPayloadForFutureResponseEvents() {
GameEvent event = GameEvent.responseActionDeclared(
"game-1",
GameEventType.PENG_DECLARED,
2,
1,
"三万"
);
assertThat(event.eventType()).isEqualTo(GameEventType.PENG_DECLARED);
assertThat(event.seatNo()).isEqualTo(2);
assertThat(event.payload())
.containsEntry("actionType", "PENG")
.containsEntry("sourceSeatNo", 1)
.containsEntry("tile", "三万");
}
@Test
void shouldBuildStandardPublicTableEvents() {
GameEvent started = GameEvent.gameStarted("game-1", "room-1");
GameEvent phaseChanged = GameEvent.phaseChanged("game-1", "PLAYING");
GameEvent switched = GameEvent.turnSwitched("game-1", 3);
assertThat(started.eventType()).isEqualTo(GameEventType.GAME_STARTED);
assertThat(started.payload()).containsEntry("roomId", "room-1");
assertThat(phaseChanged.eventType()).isEqualTo(GameEventType.GAME_PHASE_CHANGED);
assertThat(phaseChanged.payload()).containsEntry("phase", "PLAYING");
assertThat(switched.eventType()).isEqualTo(GameEventType.TURN_SWITCHED);
assertThat(switched.payload()).containsEntry("currentSeatNo", 3);
}
}

View File

@@ -0,0 +1,26 @@
package com.xuezhanmaster.game.service;
import com.xuezhanmaster.game.domain.Tile;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
class DeckFactoryTest {
private final DeckFactory deckFactory = new DeckFactory();
@Test
void shouldCreate108TilesForSichuanVariant() {
List<Tile> wall = deckFactory.createShuffledWall();
assertThat(wall).hasSize(108);
Map<Tile, Long> counts = wall.stream()
.collect(Collectors.groupingBy(tile -> tile, Collectors.counting()));
assertThat(counts.values()).allMatch(count -> count == 4L);
}
}

View File

@@ -0,0 +1,40 @@
package com.xuezhanmaster.game.service;
import com.xuezhanmaster.game.dto.GameTableSnapshot;
import com.xuezhanmaster.game.engine.GameEngine;
import com.xuezhanmaster.strategy.service.StrategyService;
import com.xuezhanmaster.teaching.dto.TeachingAdviceResponse;
import com.xuezhanmaster.teaching.service.PlayerVisibilityService;
import com.xuezhanmaster.teaching.service.TeachingService;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class DemoGameServiceTest {
private final DemoGameService demoGameService = new DemoGameService(
new GameEngine(new DeckFactory()),
new StrategyService(),
new PlayerVisibilityService(),
new TeachingService()
);
@Test
void shouldCreateDemoTableSnapshot() {
GameTableSnapshot snapshot = demoGameService.createDemoTableSnapshot();
assertThat(snapshot.seats()).hasSize(4);
assertThat(snapshot.remainingWallCount()).isEqualTo(55);
assertThat(snapshot.seats().get(0).handTiles()).hasSize(14);
assertThat(snapshot.seats().get(1).handTiles()).hasSize(13);
}
@Test
void shouldReturnAdviceCandidates() {
TeachingAdviceResponse advice = demoGameService.createDemoTeachingAdvice();
assertThat(advice.recommendedAction()).isNotBlank();
assertThat(advice.candidates()).hasSizeLessThanOrEqualTo(3);
assertThat(advice.explanation()).isNotBlank();
}
}

View File

@@ -0,0 +1,158 @@
package com.xuezhanmaster.game.service;
import com.xuezhanmaster.common.exception.BusinessException;
import com.xuezhanmaster.game.dto.GameActionRequest;
import com.xuezhanmaster.game.domain.TileSuit;
import com.xuezhanmaster.game.dto.GameStateResponse;
import com.xuezhanmaster.game.service.GameActionProcessor;
import com.xuezhanmaster.strategy.service.StrategyService;
import com.xuezhanmaster.teaching.service.PlayerVisibilityService;
import com.xuezhanmaster.teaching.service.TeachingService;
import com.xuezhanmaster.room.dto.CreateRoomRequest;
import com.xuezhanmaster.room.dto.RoomSummaryResponse;
import com.xuezhanmaster.room.dto.ToggleReadyRequest;
import com.xuezhanmaster.room.service.RoomService;
import com.xuezhanmaster.game.engine.GameEngine;
import org.junit.jupiter.api.Test;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import com.xuezhanmaster.ws.service.GameMessagePublisher;
import static org.mockito.Mockito.mock;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class GameSessionServiceTest {
private final RoomService roomService = new RoomService();
private final GameSessionService gameSessionService = new GameSessionService(
roomService,
new GameEngine(new DeckFactory()),
new GameActionProcessor(),
new StrategyService(),
new PlayerVisibilityService(),
new TeachingService(),
new GameMessagePublisher(mock(SimpMessagingTemplate.class))
);
@Test
void shouldStartGameAndEnterPlayingAfterHumanSelectsLackSuit() {
RoomSummaryResponse room = roomService.createRoom(new CreateRoomRequest("host-1", true));
roomService.toggleReady(room.roomId(), new ToggleReadyRequest("host-1", true));
GameStateResponse started = gameSessionService.startGame(room.roomId(), "host-1");
assertThat(started.phase()).isEqualTo("LACK_SELECTION");
assertThat(started.seats()).hasSize(4);
assertThat(started.selfSeat().handTiles()).hasSize(14);
GameStateResponse afterLack = gameSessionService.selectLackSuit(
started.gameId(),
"host-1",
TileSuit.WAN.name()
);
assertThat(afterLack.phase()).isEqualTo("PLAYING");
assertThat(afterLack.selfSeat().lackSuit()).isEqualTo(TileSuit.WAN.name());
}
@Test
void shouldDiscardAndLoopBackToHumanAfterBotsAutoPlay() {
RoomSummaryResponse room = roomService.createRoom(new CreateRoomRequest("host-1", true));
roomService.toggleReady(room.roomId(), new ToggleReadyRequest("host-1", true));
GameStateResponse started = gameSessionService.startGame(room.roomId(), "host-1");
GameStateResponse playing = gameSessionService.selectLackSuit(started.gameId(), "host-1", TileSuit.WAN.name());
String discardTile = playing.selfSeat().handTiles().get(0);
GameStateResponse afterDiscard = gameSessionService.discardTile(playing.gameId(), "host-1", discardTile);
assertThat(afterDiscard.phase()).isEqualTo("PLAYING");
assertThat(afterDiscard.currentSeatNo()).isEqualTo(0);
assertThat(afterDiscard.selfSeat().handTiles()).hasSize(14);
assertThat(afterDiscard.remainingWallCount()).isEqualTo(51);
}
@Test
void shouldRouteNewActionTypesThroughUnifiedActionEntry() {
RoomSummaryResponse room = roomService.createRoom(new CreateRoomRequest("host-1", true));
roomService.toggleReady(room.roomId(), new ToggleReadyRequest("host-1", true));
GameStateResponse started = gameSessionService.startGame(room.roomId(), "host-1");
gameSessionService.selectLackSuit(started.gameId(), "host-1", TileSuit.WAN.name());
assertThatThrownBy(() -> gameSessionService.performAction(
started.gameId(),
new GameActionRequest("host-1", "PENG", "三万", 1)
))
.isInstanceOf(BusinessException.class)
.extracting(throwable -> ((BusinessException) throwable).getCode())
.isEqualTo("GAME_ACTION_UNSUPPORTED");
}
@Test
void shouldRejectResponseActionWithoutSourceSeatNo() {
RoomSummaryResponse room = roomService.createRoom(new CreateRoomRequest("host-1", true));
roomService.toggleReady(room.roomId(), new ToggleReadyRequest("host-1", true));
GameStateResponse started = gameSessionService.startGame(room.roomId(), "host-1");
gameSessionService.selectLackSuit(started.gameId(), "host-1", TileSuit.WAN.name());
assertThatThrownBy(() -> gameSessionService.performAction(
started.gameId(),
new GameActionRequest("host-1", "PENG", "三万", null)
))
.isInstanceOf(BusinessException.class)
.extracting(throwable -> ((BusinessException) throwable).getCode())
.isEqualTo("GAME_ACTION_PARAM_INVALID");
}
@Test
void shouldRejectResponseActionOutsidePlayingPhase() {
RoomSummaryResponse room = roomService.createRoom(new CreateRoomRequest("host-1", true));
roomService.toggleReady(room.roomId(), new ToggleReadyRequest("host-1", true));
GameStateResponse started = gameSessionService.startGame(room.roomId(), "host-1");
assertThatThrownBy(() -> gameSessionService.performAction(
started.gameId(),
new GameActionRequest("host-1", "PASS", null, 1)
))
.isInstanceOf(BusinessException.class)
.extracting(throwable -> ((BusinessException) throwable).getCode())
.isEqualTo("GAME_PHASE_INVALID");
}
@Test
void shouldRejectResponseActionFromSelfSeat() {
RoomSummaryResponse room = roomService.createRoom(new CreateRoomRequest("host-1", true));
roomService.toggleReady(room.roomId(), new ToggleReadyRequest("host-1", true));
GameStateResponse started = gameSessionService.startGame(room.roomId(), "host-1");
gameSessionService.selectLackSuit(started.gameId(), "host-1", TileSuit.WAN.name());
assertThatThrownBy(() -> gameSessionService.performAction(
started.gameId(),
new GameActionRequest("host-1", "PASS", null, 0)
))
.isInstanceOf(BusinessException.class)
.extracting(throwable -> ((BusinessException) throwable).getCode())
.isEqualTo("GAME_ACTION_PARAM_INVALID");
}
@Test
void shouldRejectUnknownActionTypeFromUnifiedActionEntry() {
RoomSummaryResponse room = roomService.createRoom(new CreateRoomRequest("host-1", true));
roomService.toggleReady(room.roomId(), new ToggleReadyRequest("host-1", true));
GameStateResponse started = gameSessionService.startGame(room.roomId(), "host-1");
assertThatThrownBy(() -> gameSessionService.performAction(
started.gameId(),
new GameActionRequest("host-1", "UNKNOWN_ACTION", null, null)
))
.isInstanceOf(BusinessException.class)
.extracting(throwable -> ((BusinessException) throwable).getCode())
.isEqualTo("GAME_ACTION_INVALID");
}
}

View File

@@ -0,0 +1,35 @@
package com.xuezhanmaster.room.service;
import com.xuezhanmaster.room.dto.CreateRoomRequest;
import com.xuezhanmaster.room.dto.JoinRoomRequest;
import com.xuezhanmaster.room.dto.RoomSummaryResponse;
import com.xuezhanmaster.room.dto.ToggleReadyRequest;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class RoomServiceTest {
private final RoomService roomService = new RoomService();
@Test
void shouldCreateAndQueryRoomFromMemory() {
RoomSummaryResponse created = roomService.createRoom(new CreateRoomRequest("host-1", true));
RoomSummaryResponse queried = roomService.getRoomSummary(created.roomId());
assertThat(queried.roomId()).isEqualTo(created.roomId());
assertThat(queried.seats()).hasSize(1);
assertThat(queried.allowBotFill()).isTrue();
}
@Test
void shouldJoinRoomAndBecomeReady() {
RoomSummaryResponse created = roomService.createRoom(new CreateRoomRequest("host-1", true));
roomService.toggleReady(created.roomId(), new ToggleReadyRequest("host-1", true));
RoomSummaryResponse joined = roomService.joinRoom(created.roomId(), new JoinRoomRequest("player-2", "Player-2"));
RoomSummaryResponse ready = roomService.toggleReady(created.roomId(), new ToggleReadyRequest("player-2", true));
assertThat(joined.seats()).hasSize(2);
assertThat(ready.status()).isEqualTo("READY");
}
}