+ add random bot names

This commit is contained in:
Mittani
2024-12-22 18:51:05 +01:00
committed by evanpelle
parent eb261fe103
commit aa01ea7694
9 changed files with 172 additions and 63 deletions
+77 -10
View File
@@ -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<boolean> {
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}`)
+46 -29
View File
@@ -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`
<input
type="text"
.value=${this.username}
@input=${this.handleInput}
placeholder="Enter your username"
maxlength="18"
>
<div>
<input
type="text"
.value=${this.username}
@input=${this.handleInput}
placeholder="Enter your username"
maxlength="${MAX_USERNAME_LENGTH}"
>
${this.validationError
? html`<div class="error">${this.validationError}</div>`
: null}
</div>
`;
}
@@ -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');
}
}
}
+1 -1
View File
@@ -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);
}
}
+2 -2
View File
@@ -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
}
}
}
+1 -1
View File
@@ -105,4 +105,4 @@
<!-- End Cloudflare Web Analytics -->
</body>
</html>
</html>
+6 -2
View File
@@ -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()
}
}
+25 -16
View File
@@ -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())
)
);
}
}
+11
View File
@@ -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 {
+3 -2
View File
@@ -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) => {
],
},
};
};
};