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
|
||||
`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.
|
||||
- [ ] 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.
|
||||
- [ ] 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.
|
||||
|
||||
@ -106,4 +106,13 @@ export const en = {
|
||||
StoreLabel: "{{name}}'s store, {{count}} stones",
|
||||
OpenGame: "Open the game",
|
||||
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ş",
|
||||
OpenGame: "Oyunu aç",
|
||||
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 './GameHistory'
|
||||
export * from './GameClock'
|
||||
export * from './Bot'
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
export const channel_new_game = "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_game_move = "game_move"
|
||||
export const channel_on_game_update = "on_game_update"
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
import { getBotLevel, getBotLevelTextKey } from "../models/Bot";
|
||||
|
||||
const GUEST_USERNAME_PREFIX = "guest-";
|
||||
|
||||
// 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(
|
||||
player: { name?: string, isGuest?: boolean } | undefined,
|
||||
player: { userId?: string, name?: string, isGuest?: boolean } | undefined,
|
||||
t: (key: string) => string
|
||||
): string | undefined {
|
||||
const botLevel = getBotLevel(player?.userId);
|
||||
if (botLevel) return `${t("Bot")} · ${t(getBotLevelTextKey(botLevel))}`;
|
||||
if (!player?.name) return undefined;
|
||||
if (player.isGuest) {
|
||||
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