From aa01ea7694d280eb2d2dd296327e5c1b014fc10e Mon Sep 17 00:00:00 2001 From: Mittani Date: Sun, 22 Dec 2024 18:51:05 +0100 Subject: [PATCH] + add random bot names --- src/client/Main.ts | 87 +++++++++++++++++++++--- src/client/UsernameInput.ts | 75 ++++++++++++-------- src/client/graphics/NameBoxCalculator.ts | 2 +- src/client/graphics/layers/NameLayer.ts | 4 +- src/client/index.html | 2 +- src/core/Util.ts | 8 ++- src/core/execution/BotSpawner.ts | 41 ++++++----- src/server/Server.ts | 11 +++ webpack.config.js | 5 +- 9 files changed, 172 insertions(+), 63 deletions(-) diff --git a/src/client/Main.ts b/src/client/Main.ts index cf4cb4d5f..5ffb4d508 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -24,6 +24,7 @@ class Client { initialize(): void { this.usernameInput = document.querySelector('username-input') as UsernameInput; + const usernameValidation = document.getElementById('username-error'); if (!this.usernameInput) { consolex.warn('Username input element not found'); } @@ -39,26 +40,92 @@ class Client { document.addEventListener('leave-lobby', this.handleLeaveLobby.bind(this)); document.addEventListener('single-player', this.handleSinglePlayer.bind(this)); - const spModal = document.querySelector('single-player-modal') as SinglePlayerModal; + spModal instanceof SinglePlayerModal - document.getElementById('single-player').addEventListener('click', () => { - spModal.open(); - }) + + document.getElementById('single-player').addEventListener('click', async () => { + const username = this.usernameInput?.getCurrentUsername(); + + if (!username) { + usernameValidation.textContent = 'Username is required'; + return; + } + + const isValid = await this.validateUsername(username); + if (isValid) { + spModal.open(); + } else { + return; + } + }); const hostModal = document.querySelector('host-lobby-modal') as HostPrivateLobbyModal; hostModal instanceof HostPrivateLobbyModal - document.getElementById('host-lobby-button').addEventListener('click', () => { - hostModal.open(); - }) + document.getElementById('host-lobby-button').addEventListener('click', async () => { + const username = this.usernameInput?.getCurrentUsername(); + + if (!username) { + usernameValidation.textContent = 'Username is required'; + return; + } + + const isValid = await this.validateUsername(username); + + if (isValid) { + hostModal.open(); + } else { + return; + } + }); this.joinModal = document.querySelector('join-private-lobby-modal') as JoinPrivateLobbyModal; this.joinModal instanceof JoinPrivateLobbyModal - document.getElementById('join-private-lobby-button').addEventListener('click', () => { - this.joinModal.open(); - }) + + document.getElementById('join-private-lobby-button').addEventListener('click', async () => { + const username = this.usernameInput?.getCurrentUsername(); + + if (!username) { + usernameValidation.textContent = 'Username is required'; + return; + } + + const isValid = await this.validateUsername(username); + + if (isValid) { + this.joinModal.open(); + }else { + return; + } + }); } + private async validateUsername(username: string): Promise { + this.usernameInput.validationError = ''; + + try { + const response = await fetch('/validate-username', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username }), + }); + + const result = await response.json(); + + if (!response.ok || !result.success) { + this.usernameInput.validationError = result.error || 'Failed to validate username.'; + return false; + } + + return true; + } catch (error) { + consolex.error('Error validating username:', error); + this.usernameInput.validationError = 'An error occurred while validating the username. Please try again.'; + return false; + } + } + + private async handleJoinLobby(event: CustomEvent) { const lobby = event.detail.lobby consolex.log(`joining lobby ${lobby.id}`) diff --git a/src/client/UsernameInput.ts b/src/client/UsernameInput.ts index 4ce7f82ba..7c5eb5857 100644 --- a/src/client/UsernameInput.ts +++ b/src/client/UsernameInput.ts @@ -1,33 +1,44 @@ import {LitElement, html, css} from 'lit'; import {customElement, property, state} from 'lit/decorators.js'; import {v4 as uuidv4} from 'uuid'; +import {MAX_USERNAME_LENGTH} from "../core/Util"; const usernameKey: string = 'username'; @customElement('username-input') export class UsernameInput extends LitElement { @state() private username: string = ''; + @property({ type: String }) validationError: string = ''; static styles = css` - input { - width: 100%; - padding: 0.75rem; - background-color: white; - border: 1px solid #d1d5db; - border-radius: 0.375rem; - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); - font-size: 1rem; - line-height: 1.5; - color: #111827; - } + input { + width: 100%; + padding: 0.75rem; + background-color: white; + border: 1px solid #d1d5db; + border-radius: 0.375rem; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + font-size: 1rem; + line-height: 1.5; + color: #111827; + } - input:focus { - outline: none; - ring: 2px; - ring-color: #3b82f6; - border-color: #3b82f6; - } - `; + input:focus { + outline: none; + ring: 2px; + ring-color: #3b82f6; + border-color: #3b82f6; + } + + .error { + color: #dc2626; + background-color: #fff; + padding: 4px; + font-size: 0.875rem; + border: 1px solid #dc2626; + margin-top: 0.5rem; + } + `; public getCurrentUsername(): string { return this.username; @@ -36,18 +47,23 @@ export class UsernameInput extends LitElement { connectedCallback() { super.connectedCallback(); this.username = this.getStoredUsername(); - this.dispatchUsernameEvent() + this.dispatchUsernameEvent(); } render() { return html` - +
+ + ${this.validationError + ? html`
${this.validationError}
` + : null} +
`; } @@ -55,7 +71,8 @@ export class UsernameInput extends LitElement { const input = e.target as HTMLInputElement; this.username = input.value.trim(); this.storeUsername(this.username); - this.dispatchUsernameEvent() + this.validationError = ''; + this.dispatchUsernameEvent(); } private getStoredUsername(): string { @@ -74,7 +91,7 @@ export class UsernameInput extends LitElement { private dispatchUsernameEvent() { this.dispatchEvent(new CustomEvent('username-change', { - detail: {username: this.username}, + detail: { username: this.username }, bubbles: true, composed: true })); @@ -93,4 +110,4 @@ export class UsernameInput extends LitElement { const threeDigits = decimal % 1000n; return threeDigits.toString().padStart(3, '0'); } -} \ No newline at end of file +} diff --git a/src/client/graphics/NameBoxCalculator.ts b/src/client/graphics/NameBoxCalculator.ts index 0d890210c..7e1a2c80b 100644 --- a/src/client/graphics/NameBoxCalculator.ts +++ b/src/client/graphics/NameBoxCalculator.ts @@ -133,4 +133,4 @@ export function calculateFontSize(rectangle: Rectangle, name: string): number { const widthConstrained = rectangle.width / name.length * 2; const heightConstrained = rectangle.height / 3; return Math.min(widthConstrained, heightConstrained); -} \ No newline at end of file +} diff --git a/src/client/graphics/layers/NameLayer.ts b/src/client/graphics/layers/NameLayer.ts index a687cd392..8975e98b4 100644 --- a/src/client/graphics/layers/NameLayer.ts +++ b/src/client/graphics/layers/NameLayer.ts @@ -227,7 +227,7 @@ export class NameLayer implements Layer { if (myPlayer != null) { - const emojis = render.player.outgoingEmojis().filter(e => e.recipient == AllPlayers || e.recipient == myPlayer) + const emojis = render.player.outgoingEmojis().filter(e => e.recipient == AllPlayers || e.recipient == myPlayer); if (emojis.length > 0) { context.font = `${render.fontSize * 4}px ${this.theme.font()}`; context.fillStyle = this.theme.playerInfoColor(render.player.id()).toHex(); @@ -246,4 +246,4 @@ export class NameLayer implements Layer { this.myPlayer = this.game.players().find(p => p.clientID() == this.clientID) return this.myPlayer } -} \ No newline at end of file +} diff --git a/src/client/index.html b/src/client/index.html index 958f55308..6ed77a187 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -105,4 +105,4 @@ - \ No newline at end of file + diff --git a/src/core/Util.ts b/src/core/Util.ts index 9bc1af5cd..ba8c06da1 100644 --- a/src/core/Util.ts +++ b/src/core/Util.ts @@ -8,6 +8,10 @@ import { number } from 'zod'; import { GameConfig, GameID, GameRecord, PlayerRecord, Turn } from './Schemas'; import { customAlphabet, nanoid } from 'nanoid'; + +export const MIN_USERNAME_LENGTH = 3; +export const MAX_USERNAME_LENGTH = 12; + export function manhattanDist(c1: Cell, c2: Cell): number { return Math.abs(c1.x - c2.x) + Math.abs(c1.y - c2.y); } @@ -185,7 +189,7 @@ export function getMode(list: string[]): string { } export function sanitize(name: string): string { - return Array.from(name).slice(0, 10).join('').replace(/[^\p{L}\p{N}\s\p{Emoji}\p{Emoji_Component}]/gu, ''); + return Array.from(name).join('').replace(/[^\p{L}\p{N}\s\p{Emoji}\p{Emoji_Component}]/gu, ''); } export function processName(name: string): string { @@ -277,4 +281,4 @@ export function assertNever(x: never): never { export function generateID(): GameID { const nanoid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 8) return nanoid() -} \ No newline at end of file +} diff --git a/src/core/execution/BotSpawner.ts b/src/core/execution/BotSpawner.ts index 7c86491c4..166c5ff99 100644 --- a/src/core/execution/BotSpawner.ts +++ b/src/core/execution/BotSpawner.ts @@ -3,45 +3,53 @@ import {Cell, Game, PlayerType, Tile, TileEvent} from "../game/Game"; import {PseudoRandom} from "../PseudoRandom"; import {GameID, SpawnIntent} from "../Schemas"; import {bfs, dist as dist, manhattanDist, simpleHash} from "../Util"; +import {BOT_NAME_PREFIXES, BOT_NAME_SUFFIXES} from "./utils/BotNames"; export class BotSpawner { - private random: PseudoRandom + private random: PseudoRandom; private bots: SpawnIntent[] = []; constructor(private gs: Game, gameID: GameID) { - this.random = new PseudoRandom(simpleHash(gameID)) + this.random = new PseudoRandom(simpleHash(gameID)); } spawnBots(numBots: number): SpawnIntent[] { - let tries = 0 + let tries = 0; while (this.bots.length < numBots) { if (tries > 10000) { - consolex.log('too many retries while spawning bots, giving up') - return this.bots + consolex.log("too many retries while spawning bots, giving up"); + return this.bots; } - const spawn = this.spawnBot("Bot" + this.bots.length) + const botName = this.randomBotName(); + const spawn = this.spawnBot(botName); if (spawn != null) { this.bots.push(spawn); } else { - tries++ + tries++; } } return this.bots; } + private randomBotName(): string { + const prefixIndex = this.random.nextInt(0, BOT_NAME_PREFIXES.length); + const suffixIndex = this.random.nextInt(0, BOT_NAME_SUFFIXES.length); + return `${BOT_NAME_PREFIXES[prefixIndex]} ${BOT_NAME_SUFFIXES[suffixIndex]}`; + } + spawnBot(botName: string): SpawnIntent | null { - const tile = this.randTile() + const tile = this.randTile(); if (!tile.isLand()) { - return null + return null; } for (const spawn of this.bots) { if (manhattanDist(new Cell(spawn.x, spawn.y), tile.cell()) < 30) { - return null + return null; } } return { - type: 'spawn', + type: "spawn", playerID: this.random.nextID(), name: botName, playerType: PlayerType.Bot, @@ -51,10 +59,11 @@ export class BotSpawner { } private randTile(): Tile { - return this.gs.tile(new Cell( - this.random.nextInt(0, this.gs.width()), - this.random.nextInt(0, this.gs.height()) - )) + return this.gs.tile( + new Cell( + this.random.nextInt(0, this.gs.width()), + this.random.nextInt(0, this.gs.height()) + ) + ); } } - diff --git a/src/server/Server.ts b/src/server/Server.ts index f5a51f23c..5493a16e7 100644 --- a/src/server/Server.ts +++ b/src/server/Server.ts @@ -11,6 +11,7 @@ import { Client } from './Client'; import { GamePhase, GameServer } from './GameServer'; import { archive } from './Archive'; import { DiscordBot } from './DiscordBot'; +import {MAX_USERNAME_LENGTH, MIN_USERNAME_LENGTH} from "../core/Util"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -119,6 +120,16 @@ app.get('/private_lobby/:id', (req, res) => { }); }); +app.post('/validate-username', (req, res) => { + const { username } = req.body; + + if (!username || username.length < MIN_USERNAME_LENGTH || username.length > MAX_USERNAME_LENGTH) { + return res.status(400).json({ success: false, error: `Username must be between ${MIN_USERNAME_LENGTH} and ${MAX_USERNAME_LENGTH} characters.` }); + } + + res.json({ success: true, message: 'Username is valid.' }); +}); + wss.on('connection', (ws, req) => { ws.on('message', (message: string) => { try { diff --git a/webpack.config.js b/webpack.config.js index af1fc8a07..33d4e3998 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -95,7 +95,8 @@ export default (env, argv) => { ws: true, }, { - context: ['/lobbies', '/join_game', '/join_lobby', '/private_lobby', '/start_private_lobby', '/lobby', '/archive_singleplayer_game'], + context: ['/lobbies', '/join_game', '/join_lobby', '/private_lobby', '/start_private_lobby', + '/lobby', '/archive_singleplayer_game', '/validate-username'], target: 'http://localhost:3000', secure: false, changeOrigin: true, @@ -103,4 +104,4 @@ export default (env, argv) => { ], }, }; -}; \ No newline at end of file +};