Release: Quarkus backend, Keycloak login, game history, clock, bot, tutorial and rematch #3
@ -12,7 +12,7 @@ Work through the tasks in order; each ends with its tests passing and a commit.
|
|||||||
`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.
|
||||||
- [x] 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`,
|
- [x] 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
|
||||||
game, bot names everywhere players are shown.
|
game, bot names everywhere players are shown.
|
||||||
|
|||||||
@ -106,4 +106,13 @@ export const en = {
|
|||||||
StoreLabel: "{{name}}'s store, {{count}} stones",
|
StoreLabel: "{{name}}'s store, {{count}} stones",
|
||||||
OpenGame: "Open the game",
|
OpenGame: "Open the game",
|
||||||
Score: "Score",
|
Score: "Score",
|
||||||
|
PlayWithBot: "Play with bot",
|
||||||
|
NoOpponentYet: "No opponent yet. Play with the bot?",
|
||||||
|
ChooseBotLevel: "Choose how strong the bot is",
|
||||||
|
Bot: "Bot",
|
||||||
|
BotLevelEasy: "Easy",
|
||||||
|
BotLevelMedium: "Medium",
|
||||||
|
BotLevelHard: "Hard",
|
||||||
|
BotLevelMaster: "Master",
|
||||||
|
PlayAgain: "Play again",
|
||||||
}
|
}
|
||||||
@ -106,4 +106,13 @@ export const tr = {
|
|||||||
StoreLabel: "{{name}} hazinesi, {{count}} taş",
|
StoreLabel: "{{name}} hazinesi, {{count}} taş",
|
||||||
OpenGame: "Oyunu aç",
|
OpenGame: "Oyunu aç",
|
||||||
Score: "Skor",
|
Score: "Skor",
|
||||||
|
PlayWithBot: "Bota karşı oyna",
|
||||||
|
NoOpponentYet: "Henüz rakip bulunamadı. Bota karşı oynamak ister misin?",
|
||||||
|
ChooseBotLevel: "Botun ne kadar güçlü olacağını seç",
|
||||||
|
Bot: "Bot",
|
||||||
|
BotLevelEasy: "Kolay",
|
||||||
|
BotLevelMedium: "Orta",
|
||||||
|
BotLevelHard: "Zor",
|
||||||
|
BotLevelMaster: "Usta",
|
||||||
|
PlayAgain: "Tekrar oyna",
|
||||||
};
|
};
|
||||||
42
packages/core/src/models/Bot.ts
Normal file
42
packages/core/src/models/Bot.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
// Games against the bot (docs/specs/game-bot). The bot plays with the user id `bot:<level>`.
|
||||||
|
export type BotLevel = "easy" | "medium" | "hard" | "master";
|
||||||
|
|
||||||
|
// Weakest first.
|
||||||
|
export const BOT_LEVELS: BotLevel[] = ["easy", "medium", "hard", "master"];
|
||||||
|
|
||||||
|
// How long the lobby searches for an opponent before it offers the bot.
|
||||||
|
export const BOT_OFFER_DELAY_MS = 10_000;
|
||||||
|
|
||||||
|
const BOT_ID_PREFIX = "bot:";
|
||||||
|
|
||||||
|
export function isBotId(userId: string | undefined): boolean {
|
||||||
|
return userId?.startsWith(BOT_ID_PREFIX) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The level of a bot id, or undefined if it is not one.
|
||||||
|
export function getBotLevel(userId: string | undefined): BotLevel | undefined {
|
||||||
|
if (!userId || !isBotId(userId)) return undefined;
|
||||||
|
const level = userId.slice(BOT_ID_PREFIX.length);
|
||||||
|
return (BOT_LEVELS as string[]).includes(level) ? level as BotLevel : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBotLevel(value: unknown): value is BotLevel {
|
||||||
|
return typeof value === "string" && (BOT_LEVELS as string[]).includes(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translation key of a level's name, e.g. BotLevelMedium.
|
||||||
|
export function getBotLevelTextKey(level: BotLevel): string {
|
||||||
|
return `BotLevel${level.charAt(0).toUpperCase()}${level.slice(1)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LAST_BOT_LEVEL_KEY = "botLevel";
|
||||||
|
|
||||||
|
// The level chosen last on this device; Medium at first.
|
||||||
|
export function getLastBotLevel(storage: { getString(key: string): string | undefined }): BotLevel {
|
||||||
|
const saved = storage.getString(LAST_BOT_LEVEL_KEY);
|
||||||
|
return isBotLevel(saved) ? saved : "medium";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setLastBotLevel(storage: { setString(key: string, value: string): void }, level: BotLevel): void {
|
||||||
|
storage.setString(LAST_BOT_LEVEL_KEY, level);
|
||||||
|
}
|
||||||
@ -6,3 +6,4 @@ export * from './UserConnectionInfo'
|
|||||||
export * from './AuthUser'
|
export * from './AuthUser'
|
||||||
export * from './GameHistory'
|
export * from './GameHistory'
|
||||||
export * from './GameClock'
|
export * from './GameClock'
|
||||||
|
export * from './Bot'
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
export const channel_new_game = "new_game"
|
export const channel_new_game = "new_game"
|
||||||
export const channel_cancel_new_game = "cancel_new_game"
|
export const channel_cancel_new_game = "cancel_new_game"
|
||||||
|
// client -> server: { level: BotLevel }, starts a game against the bot; ends a search for an opponent.
|
||||||
|
export const channel_new_bot_game = "new_bot_game"
|
||||||
export const channel_on_game_start = "on_game_start"
|
export const channel_on_game_start = "on_game_start"
|
||||||
export const channel_game_move = "game_move"
|
export const channel_game_move = "game_move"
|
||||||
export const channel_on_game_update = "on_game_update"
|
export const channel_on_game_update = "on_game_update"
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
|
import { getBotLevel, getBotLevelTextKey } from "../models/Bot";
|
||||||
|
|
||||||
const GUEST_USERNAME_PREFIX = "guest-";
|
const GUEST_USERNAME_PREFIX = "guest-";
|
||||||
|
|
||||||
// Name to show for a player, or undefined if the server sent none (legacy userKey players).
|
// Name to show for a player, or undefined if the server sent none (legacy userKey players).
|
||||||
// Guests show as e.g. "Guest 4F2A" instead of their generated username.
|
// Guests show as e.g. "Guest 4F2A" instead of their generated username, the bot as e.g. "Bot · Medium".
|
||||||
export function getPlayerDisplayName(
|
export function getPlayerDisplayName(
|
||||||
player: { name?: string, isGuest?: boolean } | undefined,
|
player: { userId?: string, name?: string, isGuest?: boolean } | undefined,
|
||||||
t: (key: string) => string
|
t: (key: string) => string
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
|
const botLevel = getBotLevel(player?.userId);
|
||||||
|
if (botLevel) return `${t("Bot")} · ${t(getBotLevelTextKey(botLevel))}`;
|
||||||
if (!player?.name) return undefined;
|
if (!player?.name) return undefined;
|
||||||
if (player.isGuest) {
|
if (player.isGuest) {
|
||||||
const suffix = player.name.startsWith(GUEST_USERNAME_PREFIX) ? player.name.slice(GUEST_USERNAME_PREFIX.length) : player.name;
|
const suffix = player.name.startsWith(GUEST_USERNAME_PREFIX) ? player.name.slice(GUEST_USERNAME_PREFIX.length) : player.name;
|
||||||
|
|||||||
55
packages/core/tests/models/Bot.test.ts
Normal file
55
packages/core/tests/models/Bot.test.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { BOT_LEVELS, getBotLevel, getBotLevelTextKey, getLastBotLevel, isBotId, isBotLevel, setLastBotLevel } from "../../src/models";
|
||||||
|
import { en, tr } from "../../src/localization";
|
||||||
|
import { getPlayerDisplayName } from "../../src/util";
|
||||||
|
|
||||||
|
describe("bot ids", () => {
|
||||||
|
it("recognises bot ids and their levels", () => {
|
||||||
|
expect(isBotId("bot:medium")).toBe(true);
|
||||||
|
expect(isBotId("kc:alice")).toBe(false);
|
||||||
|
expect(isBotId(undefined)).toBe(false);
|
||||||
|
expect(getBotLevel("bot:master")).toBe("master");
|
||||||
|
expect(getBotLevel("bot:impossible")).toBeUndefined();
|
||||||
|
expect(getBotLevel("kc:alice")).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("checks levels", () => {
|
||||||
|
expect(isBotLevel("hard")).toBe(true);
|
||||||
|
expect(isBotLevel("impossible")).toBe(false);
|
||||||
|
expect(isBotLevel(3)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has a name for every level in both languages", () => {
|
||||||
|
for (const level of BOT_LEVELS) {
|
||||||
|
const key = getBotLevelTextKey(level) as keyof typeof en;
|
||||||
|
expect(en[key]).toBeTruthy();
|
||||||
|
expect(tr[key as keyof typeof tr]).toBeTruthy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("last bot level", () => {
|
||||||
|
it("is Medium until one is chosen, then the chosen one", () => {
|
||||||
|
const values = new Map<string, string>();
|
||||||
|
const storage = { getString: (key: string) => values.get(key), setString: (key: string, value: string) => { values.set(key, value); } };
|
||||||
|
expect(getLastBotLevel(storage)).toBe("medium");
|
||||||
|
setLastBotLevel(storage, "master");
|
||||||
|
expect(getLastBotLevel(storage)).toBe("master");
|
||||||
|
values.set("botLevel", "impossible");
|
||||||
|
expect(getLastBotLevel(storage)).toBe("medium");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getPlayerDisplayName", () => {
|
||||||
|
const t = (key: string) => (tr as Record<string, string>)[key] ?? key;
|
||||||
|
|
||||||
|
it("shows the bot with its level", () => {
|
||||||
|
expect(getPlayerDisplayName({ userId: "bot:medium", name: "Bot", isGuest: false }, t)).toBe("Bot · Orta");
|
||||||
|
expect(getPlayerDisplayName({ userId: "bot:easy" }, t)).toBe("Bot · Kolay");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still shows people and guests as before", () => {
|
||||||
|
expect(getPlayerDisplayName({ userId: "kc:alice", name: "alice", isGuest: false }, t)).toBe("alice");
|
||||||
|
expect(getPlayerDisplayName({ userId: "kc:g", name: "guest-4f2a9b", isGuest: true }, t)).toBe("Misafir 4F2A");
|
||||||
|
expect(getPlayerDisplayName({ userId: "0b9f6d1e" }, t)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user