Release: Quarkus backend, Keycloak login, game history, clock, bot, tutorial and rematch #3
@ -152,7 +152,7 @@ public final class GameSerialization {
|
|||||||
publicSummary(game, mancalaGame.getPlayer2Id()), boxed(mancalaGame.getBoard().getStoneArray()),
|
publicSummary(game, mancalaGame.getPlayer2Id()), boxed(mancalaGame.getBoard().getStoneArray()),
|
||||||
getResult(game, ending), ending.endReason(),
|
getResult(game, ending), ending.endReason(),
|
||||||
ending.leftUserId() == null ? null : PublicIds.publicUserId(ending.leftUserId()), game.startedAt, endedAt,
|
ending.leftUserId() == null ? null : PublicIds.publicUserId(ending.leftUserId()), game.startedAt, endedAt,
|
||||||
moves.size(), game.peakSpectators, game.clock.initialMs(), moves, game.allSpectatorIds.size());
|
moves.size(), game.peakSpectators, game.clock.initialMs(), moves, game.allSpectatorIds.size(), game.bot);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The player with a public id, for anything saved or listed.
|
// The player with a public id, for anything saved or listed.
|
||||||
|
|||||||
@ -23,7 +23,7 @@ public final class Migrations {
|
|||||||
private static final long MIGRATION_LOCK_ID = 727274;
|
private static final long MIGRATION_LOCK_ID = 727274;
|
||||||
|
|
||||||
// Applied in this order; never change a migration that has been released, add a new one instead.
|
// Applied in this order; never change a migration that has been released, add a new one instead.
|
||||||
public static final List<String> VERSIONS = List.of("001_games", "002_clock");
|
public static final List<String> VERSIONS = List.of("001_games", "002_clock", "003_bot");
|
||||||
|
|
||||||
private Migrations() {
|
private Migrations() {
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import javax.sql.DataSource;
|
|||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
|
||||||
|
import app.playmancala.bot.BotSettings;
|
||||||
import app.playmancala.model.EndReason;
|
import app.playmancala.model.EndReason;
|
||||||
import app.playmancala.model.FinishedGameSummary;
|
import app.playmancala.model.FinishedGameSummary;
|
||||||
import app.playmancala.model.GameRecord;
|
import app.playmancala.model.GameRecord;
|
||||||
@ -58,9 +59,9 @@ public class PostgresGameHistoryRepository implements GameHistoryRepository {
|
|||||||
int inserted;
|
int inserted;
|
||||||
try (PreparedStatement statement = connection.prepareStatement("""
|
try (PreparedStatement statement = connection.prepareStatement("""
|
||||||
INSERT INTO games (id, player1_id, player2_id, result, end_reason, left_user_id, started_at, ended_at,
|
INSERT INTO games (id, player1_id, player2_id, result, end_reason, left_user_id, started_at, ended_at,
|
||||||
move_count, final_board, moves, peak_spectators, total_spectators, clock_initial_ms)
|
move_count, final_board, moves, peak_spectators, total_spectators, clock_initial_ms, bot)
|
||||||
VALUES (?::uuid, ?, ?, ?, ?, ?, to_timestamp(? / 1000.0), to_timestamp(? / 1000.0),
|
VALUES (?::uuid, ?, ?, ?, ?, ?, to_timestamp(? / 1000.0), to_timestamp(? / 1000.0),
|
||||||
?, ?::jsonb, ?::jsonb, ?, ?, ?)
|
?, ?::jsonb, ?::jsonb, ?, ?, ?, ?::jsonb)
|
||||||
ON CONFLICT (id) DO NOTHING""")) {
|
ON CONFLICT (id) DO NOTHING""")) {
|
||||||
statement.setString(1, record.id());
|
statement.setString(1, record.id());
|
||||||
statement.setString(2, record.player1().userId());
|
statement.setString(2, record.player1().userId());
|
||||||
@ -77,6 +78,7 @@ public class PostgresGameHistoryRepository implements GameHistoryRepository {
|
|||||||
statement.setInt(13, record.totalSpectators());
|
statement.setInt(13, record.totalSpectators());
|
||||||
if (record.clockInitialMs() == null) statement.setNull(14, Types.INTEGER);
|
if (record.clockInitialMs() == null) statement.setNull(14, Types.INTEGER);
|
||||||
else statement.setInt(14, Math.toIntExact(record.clockInitialMs()));
|
else statement.setInt(14, Math.toIntExact(record.clockInitialMs()));
|
||||||
|
statement.setString(15, record.bot() == null ? null : Json.write(record.bot()));
|
||||||
inserted = statement.executeUpdate();
|
inserted = statement.executeUpdate();
|
||||||
}
|
}
|
||||||
if (inserted == 0) return null;
|
if (inserted == 0) return null;
|
||||||
@ -103,7 +105,7 @@ public class PostgresGameHistoryRepository implements GameHistoryRepository {
|
|||||||
if (!UUID_PATTERN.matcher(id).matches()) return null;
|
if (!UUID_PATTERN.matcher(id).matches()) return null;
|
||||||
return query(connection -> {
|
return query(connection -> {
|
||||||
try (PreparedStatement statement = connection.prepareStatement(
|
try (PreparedStatement statement = connection.prepareStatement(
|
||||||
"SELECT " + SUMMARY_COLUMNS + ", g.moves, g.total_spectators " + FROM_GAMES + " WHERE g.id = ?::uuid")) {
|
"SELECT " + SUMMARY_COLUMNS + ", g.moves, g.total_spectators, g.bot " + FROM_GAMES + " WHERE g.id = ?::uuid")) {
|
||||||
statement.setString(1, id);
|
statement.setString(1, id);
|
||||||
try (ResultSet row = statement.executeQuery()) {
|
try (ResultSet row = statement.executeQuery()) {
|
||||||
if (!row.next()) return null;
|
if (!row.next()) return null;
|
||||||
@ -111,7 +113,8 @@ public class PostgresGameHistoryRepository implements GameHistoryRepository {
|
|||||||
return new GameRecord(summary.id(), summary.player1(), summary.player2(), summary.board(),
|
return new GameRecord(summary.id(), summary.player1(), summary.player2(), summary.board(),
|
||||||
summary.result(), summary.endReason(), summary.leftUserId(), summary.startedAt(),
|
summary.result(), summary.endReason(), summary.leftUserId(), summary.startedAt(),
|
||||||
summary.endedAt(), summary.moveCount(), summary.peakSpectators(), summary.clockInitialMs(),
|
summary.endedAt(), summary.moveCount(), summary.peakSpectators(), summary.clockInitialMs(),
|
||||||
Json.MAPPER.readValue(row.getString("moves"), MOVES), row.getInt("total_spectators"));
|
Json.MAPPER.readValue(row.getString("moves"), MOVES), row.getInt("total_spectators"),
|
||||||
|
row.getString("bot") == null ? null : Json.MAPPER.readValue(row.getString("bot"), BotSettings.class));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,10 +5,12 @@ import java.util.List;
|
|||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||||
|
|
||||||
|
import app.playmancala.bot.BotSettings;
|
||||||
|
|
||||||
// A saved, finished game: a FinishedGameSummary plus its moves and the number of distinct spectators. Every id in it
|
// A saved, finished game: a FinishedGameSummary plus its moves and the number of distinct spectators. Every id in it
|
||||||
// is a public id.
|
// is a public id. `bot` is how the bot played, in games against the bot.
|
||||||
@JsonPropertyOrder({ "id", "player1", "player2", "board", "result", "endReason", "leftUserId", "startedAt", "endedAt",
|
@JsonPropertyOrder({ "id", "player1", "player2", "board", "result", "endReason", "leftUserId", "startedAt", "endedAt",
|
||||||
"moveCount", "peakSpectators", "clockInitialMs", "moves", "totalSpectators" })
|
"moveCount", "peakSpectators", "clockInitialMs", "moves", "totalSpectators", "bot" })
|
||||||
public record GameRecord(
|
public record GameRecord(
|
||||||
String id,
|
String id,
|
||||||
PlayerSummary player1,
|
PlayerSummary player1,
|
||||||
@ -23,7 +25,16 @@ public record GameRecord(
|
|||||||
int peakSpectators,
|
int peakSpectators,
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL) Long clockInitialMs,
|
@JsonInclude(JsonInclude.Include.NON_NULL) Long clockInitialMs,
|
||||||
List<MoveRecord> moves,
|
List<MoveRecord> moves,
|
||||||
int totalSpectators) {
|
int totalSpectators,
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL) BotSettings bot) {
|
||||||
|
|
||||||
|
// A game between people.
|
||||||
|
public GameRecord(String id, PlayerSummary player1, PlayerSummary player2, List<Integer> board, GameResult result,
|
||||||
|
EndReason endReason, String leftUserId, long startedAt, long endedAt, int moveCount, int peakSpectators,
|
||||||
|
Long clockInitialMs, List<MoveRecord> moves, int totalSpectators) {
|
||||||
|
this(id, player1, player2, board, result, endReason, leftUserId, startedAt, endedAt, moveCount, peakSpectators,
|
||||||
|
clockInitialMs, moves, totalSpectators, null);
|
||||||
|
}
|
||||||
|
|
||||||
public FinishedGameSummary toSummary() {
|
public FinishedGameSummary toSummary() {
|
||||||
return new FinishedGameSummary(id, player1, player2, board, result, endReason, leftUserId, startedAt, endedAt,
|
return new FinishedGameSummary(id, player1, player2, board, result, endReason, leftUserId, startedAt, endedAt,
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
-- How the bot played in a game against the bot: { level, strength, seed }; NULL for games between people.
|
||||||
|
ALTER TABLE games ADD COLUMN bot jsonb;
|
||||||
@ -209,6 +209,7 @@ class BotPlayerTest {
|
|||||||
assertNotNull(record, "the game did not end");
|
assertNotNull(record, "the game did not end");
|
||||||
assertEquals(EndReason.COMPLETED, record.endReason());
|
assertEquals(EndReason.COMPLETED, record.endReason());
|
||||||
assertEquals(new PlayerSummary("bot:medium", "Bot", false), record.player2());
|
assertEquals(new PlayerSummary("bot:medium", "Bot", false), record.player2());
|
||||||
|
assertEquals(new BotSettings(BotLevel.MEDIUM, 40, 11), record.bot());
|
||||||
assertTrue(record.moves().stream().anyMatch(move -> move.playerId().equals("bot:medium")));
|
assertTrue(record.moves().stream().anyMatch(move -> move.playerId().equals("bot:medium")));
|
||||||
assertTrue(rtmt.sent.stream().noneMatch(message -> message.clientId().startsWith("bot:")));
|
assertTrue(rtmt.sent.stream().noneMatch(message -> message.clientId().startsWith("bot:")));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,8 @@ import org.junit.jupiter.api.Test;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
|
||||||
|
import app.playmancala.bot.BotLevel;
|
||||||
|
import app.playmancala.bot.BotSettings;
|
||||||
import app.playmancala.engine.GameStep;
|
import app.playmancala.engine.GameStep;
|
||||||
import app.playmancala.model.EndReason;
|
import app.playmancala.model.EndReason;
|
||||||
import app.playmancala.model.FinishedGameSummary;
|
import app.playmancala.model.FinishedGameSummary;
|
||||||
@ -88,7 +90,7 @@ class PostgresGameHistoryRepositoryTest {
|
|||||||
ResultSet rows = statement.executeQuery("SELECT version FROM schema_migrations ORDER BY version")) {
|
ResultSet rows = statement.executeQuery("SELECT version FROM schema_migrations ORDER BY version")) {
|
||||||
while (rows.next()) versions.add(rows.getString(1));
|
while (rows.next()) versions.add(rows.getString(1));
|
||||||
}
|
}
|
||||||
assertEquals(List.of("001_games", "002_clock"), versions);
|
assertEquals(List.of("001_games", "002_clock", "003_bot"), versions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -115,6 +117,20 @@ class PostgresGameHistoryRepositoryTest {
|
|||||||
assertFalse(saved.at("/moves/0").has("remainingMs"));
|
assertFalse(saved.at("/moves/0").has("remainingMs"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void keepsHowTheBotPlayed() {
|
||||||
|
GameRecord game = record(ALICE, new PlayerSummary("bot:hard", "Bot", false));
|
||||||
|
GameRecord botGame = new GameRecord(game.id(), game.player1(), game.player2(), game.board(), game.result(),
|
||||||
|
game.endReason(), null, game.startedAt(), game.endedAt(), game.moveCount(), game.peakSpectators(), null,
|
||||||
|
game.moves(), game.totalSpectators(), new BotSettings(BotLevel.HARD, 72, -5_123_456_789_012L));
|
||||||
|
repository.save(botGame);
|
||||||
|
assertEquals(Json.tree(botGame), Json.tree(repository.get(game.id())));
|
||||||
|
assertEquals("bot:hard", repository.getRecentForUser(ALICE.userId(), 1).getFirst().player2().userId());
|
||||||
|
GameRecord betweenPeople = record();
|
||||||
|
repository.save(betweenPeople);
|
||||||
|
assertFalse(Json.tree(repository.get(betweenPeople.id())).has("bot"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void keepsLeftUserIdForGamesAPlayerLeft() {
|
void keepsLeftUserIdForGamesAPlayerLeft() {
|
||||||
GameRecord game = record(ALICE, BOB, GameResult.PLAYER2, EndReason.LEFT, ALICE.userId(), 0, 0, null, null);
|
GameRecord game = record(ALICE, BOB, GameResult.PLAYER2, EndReason.LEFT, ALICE.userId(), 0, 0, null, null);
|
||||||
|
|||||||
@ -11,7 +11,7 @@ Work through the tasks in order; each ends with its tests passing and a commit.
|
|||||||
- [x] 4. **Bot games in the backend**: `new_bot_game`, `BackendGame.bot`, `playMove` split out of `onGameMove`,
|
- [x] 4. **Bot games in the backend**: `new_bot_game`, `BackendGame.bot`, `playMove` split out of `onGameMove`,
|
||||||
`onTurnStarted`, bot ids in `GameStore`, `PublicIds` and the connection info; `BotPlayer` and its executor in
|
`onTurnStarted`, bot ids in `GameStore`, `PublicIds` and the connection info; `BotPlayer` and its executor in
|
||||||
`Backend`, `BOT_SEARCH_THREADS`; `bot` in snapshots and their restore; unit tests.
|
`Backend`, `BOT_SEARCH_THREADS`; `bot` in snapshots and their restore; unit tests.
|
||||||
- [ ] 5. **Saving**: `bot` in `GameRecord`, migration `003_bot`, the repository; integration tests.
|
- [x] 5. **Saving**: `bot` in `GameRecord`, migration `003_bot`, the repository; integration tests.
|
||||||
- [ ] 6. **Core**: `channel_new_bot_game`, `BotLevel`, `BOT_LEVELS`, `BOT_OFFER_DELAY_MS`, `isBotId`,
|
- [ ] 6. **Core**: `channel_new_bot_game`, `BotLevel`, `BOT_LEVELS`, `BOT_OFFER_DELAY_MS`, `isBotId`,
|
||||||
`getBotLevel`, `getPlayerDisplayName` for bots, texts in English and Turkish; unit tests.
|
`getBotLevel`, `getPlayerDisplayName` for bots, texts in English and Turkish; unit tests.
|
||||||
- [ ] 7. **Web app**: `BotLevelPicker`, the home page button, the lobby offer and `?bot=`, "Play again" after a bot
|
- [ ] 7. **Web app**: `BotLevelPicker`, the home page button, the lobby offer and `?bot=`, "Play again" after a bot
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user