Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
843b45b14e | ||
|
|
76a8472f12 | ||
|
|
bf02bc14fd | ||
|
|
ffb626f403 | ||
|
|
860fd607be | ||
|
|
3bae00b9a9 | ||
|
|
ff791409cf |
26
CHANGELOG.md
26
CHANGELOG.md
@@ -1,3 +1,29 @@
|
||||
## [3.19.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.18.1...v3.19.0) (2025-10-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* implement avatar-themed name generation with probabilistic mixing ([76a8472](https://github.com/antialias/soroban-abacus-flashcards/commit/76a8472f12d251071b97f2288f62f0b358576232))
|
||||
|
||||
## [3.18.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.18.0...v3.18.1) (2025-10-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **arcade:** prevent empty update in settings API when only gameConfig changes ([ffb626f](https://github.com/antialias/soroban-abacus-flashcards/commit/ffb626f4038fd32d0f40dba8d83ae4d881d698d0))
|
||||
|
||||
## [3.18.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.17.14...v3.18.0) (2025-10-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add drizzle migration for room_game_configs table ([3bae00b](https://github.com/antialias/soroban-abacus-flashcards/commit/3bae00b9a9dc925039a02fe07d036a2fc5e0fb79))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* document manual migration of room_game_configs table ([ff79140](https://github.com/antialias/soroban-abacus-flashcards/commit/ff791409cf4bae1a5df43eb974eacbc7612d8eec))
|
||||
|
||||
## [3.17.14](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.17.13...v3.17.14) (2025-10-15)
|
||||
|
||||
|
||||
|
||||
@@ -370,22 +370,28 @@ Manual test procedure:
|
||||
|
||||
**Old Schema:**
|
||||
- Settings stored in `arcade_rooms.game_config` JSON column
|
||||
- Structure: `{ matching: {...}, memory-quiz: {...} }`
|
||||
- Config stored directly for currently selected game only
|
||||
- Config lost when switching games
|
||||
|
||||
**New Schema:**
|
||||
- Settings stored in `room_game_configs` table
|
||||
- One row per game per room
|
||||
- Old column preserved temporarily for safety
|
||||
- Unique constraint on (room_id, game_name)
|
||||
- Configs persist when switching between games
|
||||
|
||||
**Migration SQL:** `drizzle/0011_add_room_game_configs.sql`
|
||||
- Extracts matching configs from old column
|
||||
- Extracts memory-quiz configs from old column
|
||||
- Uses `json_extract()` to parse nested structure
|
||||
**Migration:** See `.claude/MANUAL_MIGRATION_0011.md` for complete details
|
||||
|
||||
**Summary:**
|
||||
- Manual migration applied on 2025-10-15
|
||||
- Created `room_game_configs` table via sqlite3 CLI
|
||||
- Migrated 6000 existing configs (5991 matching, 9 memory-quiz)
|
||||
- Table created directly instead of through drizzle migration system
|
||||
|
||||
**Rollback Plan:**
|
||||
- Old `gameConfig` column still exists
|
||||
- Can revert to reading from it if needed
|
||||
- Will be dropped in future release after validation
|
||||
- Old `game_config` column still exists in `arcade_rooms` table
|
||||
- Old data preserved (was only read, not deleted)
|
||||
- Can revert to reading from old column if needed
|
||||
- New table can be dropped: `DROP TABLE room_game_configs`
|
||||
|
||||
## Architecture Benefits
|
||||
|
||||
|
||||
120
apps/web/.claude/MANUAL_MIGRATION_0011.md
Normal file
120
apps/web/.claude/MANUAL_MIGRATION_0011.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Manual Migration: room_game_configs Table
|
||||
|
||||
**Date:** 2025-10-15
|
||||
**Migration:** Create `room_game_configs` table (equivalent to drizzle migration 0011)
|
||||
|
||||
## Context
|
||||
|
||||
This migration was applied manually using sqlite3 CLI instead of through drizzle-kit's migration system, because the interactive prompt from `drizzle-kit push` cannot be automated in the deployment pipeline.
|
||||
|
||||
## What Was Done
|
||||
|
||||
### 1. Created Table
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS room_game_configs (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
room_id TEXT NOT NULL,
|
||||
game_name TEXT NOT NULL,
|
||||
config TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (room_id) REFERENCES arcade_rooms(id) ON UPDATE NO ACTION ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
### 2. Created Index
|
||||
|
||||
```sql
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS room_game_idx ON room_game_configs (room_id, game_name);
|
||||
```
|
||||
|
||||
### 3. Migrated Existing Data
|
||||
|
||||
Migrated 6000 game configs from the old `arcade_rooms.game_config` column to the new normalized table:
|
||||
|
||||
```sql
|
||||
INSERT OR IGNORE INTO room_game_configs (id, room_id, game_name, config, created_at, updated_at)
|
||||
SELECT
|
||||
lower(hex(randomblob(16))) as id,
|
||||
id as room_id,
|
||||
game_name,
|
||||
game_config as config,
|
||||
created_at,
|
||||
last_activity as updated_at
|
||||
FROM arcade_rooms
|
||||
WHERE game_config IS NOT NULL
|
||||
AND game_name IS NOT NULL;
|
||||
```
|
||||
|
||||
**Results:**
|
||||
- 5991 matching game configs migrated
|
||||
- 9 memory-quiz game configs migrated
|
||||
- Total: 6000 configs
|
||||
|
||||
## Old vs New Schema
|
||||
|
||||
**Old Schema:**
|
||||
- `arcade_rooms.game_config` (TEXT/JSON) - stored config for currently selected game only
|
||||
- Config was lost when switching games
|
||||
|
||||
**New Schema:**
|
||||
- `room_game_configs` table - one row per game per room
|
||||
- Unique constraint on (room_id, game_name)
|
||||
- Configs persist when switching between games
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# Verify table exists
|
||||
sqlite3 data/sqlite.db ".tables" | grep room_game_configs
|
||||
|
||||
# Verify schema
|
||||
sqlite3 data/sqlite.db ".schema room_game_configs"
|
||||
|
||||
# Count migrated data
|
||||
sqlite3 data/sqlite.db "SELECT COUNT(*) FROM room_game_configs;"
|
||||
# Expected: 6000
|
||||
|
||||
# Check data distribution
|
||||
sqlite3 data/sqlite.db "SELECT game_name, COUNT(*) FROM room_game_configs GROUP BY game_name;"
|
||||
# Expected: matching: 5991, memory-quiz: 9
|
||||
```
|
||||
|
||||
## Related Files
|
||||
|
||||
This migration supports the refactoring documented in:
|
||||
- `.claude/GAME_SETTINGS_PERSISTENCE.md` - Architecture documentation
|
||||
- `src/lib/arcade/game-configs.ts` - Shared config types
|
||||
- `src/lib/arcade/game-config-helpers.ts` - Database access helpers
|
||||
|
||||
## Note on Drizzle Migration Tracking
|
||||
|
||||
This migration was NOT recorded in drizzle's `__drizzle_migrations` table because it was applied manually. This is acceptable because:
|
||||
|
||||
1. The schema definition exists in code (`src/db/schema/room-game-configs.ts`)
|
||||
2. The table was created with the exact schema drizzle would generate
|
||||
3. Future schema changes will go through proper drizzle migrations
|
||||
4. The `arcade_rooms.game_config` column is preserved for rollback safety
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If issues arise, the old system can be restored by:
|
||||
|
||||
1. Reverting code changes (game-config-helpers.ts, API routes, validators)
|
||||
2. The old `game_config` column still exists in `arcade_rooms` table
|
||||
3. Data is still there (we only read from it, didn't delete it)
|
||||
|
||||
The new `room_game_configs` table can be dropped if needed:
|
||||
|
||||
```sql
|
||||
DROP TABLE IF EXISTS room_game_configs;
|
||||
```
|
||||
|
||||
## Future Work
|
||||
|
||||
Once this migration is stable in production:
|
||||
|
||||
1. Consider dropping the old `arcade_rooms.game_config` column
|
||||
2. Add this migration to drizzle's migration journal for tracking (optional)
|
||||
3. Monitor for any issues with settings persistence
|
||||
@@ -70,7 +70,10 @@
|
||||
"Bash(lsof:*)",
|
||||
"Bash(killall:*)",
|
||||
"Bash(echo:*)",
|
||||
"Bash(git restore:*)"
|
||||
"Bash(git restore:*)",
|
||||
"Bash(timeout 10 npm run dev:*)",
|
||||
"Bash(timeout 30 npm run dev)",
|
||||
"Bash(pkill:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
-- Create room_game_configs table for per-game settings storage
|
||||
-- This replaces the monolithic gameConfig JSON column with a normalized table
|
||||
CREATE TABLE `room_game_configs` (
|
||||
-- Create room_game_configs table for normalized game settings storage
|
||||
-- This migration is safe to run multiple times (uses IF NOT EXISTS)
|
||||
|
||||
-- Create the table
|
||||
CREATE TABLE IF NOT EXISTS `room_game_configs` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`room_id` text NOT NULL,
|
||||
`game_name` text NOT NULL,
|
||||
@@ -10,30 +12,22 @@ CREATE TABLE `room_game_configs` (
|
||||
FOREIGN KEY (`room_id`) REFERENCES `arcade_rooms`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `room_game_idx` ON `room_game_configs` (`room_id`, `game_name`);
|
||||
|
||||
-- Create unique index
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS `room_game_idx` ON `room_game_configs` (`room_id`,`game_name`);
|
||||
--> statement-breakpoint
|
||||
|
||||
-- Migrate existing 'matching' configs from arcade_rooms.game_config
|
||||
INSERT INTO `room_game_configs` (`id`, `room_id`, `game_name`, `config`, `created_at`, `updated_at`)
|
||||
-- Migrate existing game configs from arcade_rooms.game_config column
|
||||
-- This INSERT will only run if data hasn't been migrated yet
|
||||
INSERT OR IGNORE INTO room_game_configs (id, room_id, game_name, config, created_at, updated_at)
|
||||
SELECT
|
||||
lower(hex(randomblob(16))),
|
||||
`id` as room_id,
|
||||
'matching' as game_name,
|
||||
json_extract(`game_config`, '$.matching') as config,
|
||||
`created_at`,
|
||||
`last_activity` as updated_at
|
||||
FROM `arcade_rooms`
|
||||
WHERE json_extract(`game_config`, '$.matching') IS NOT NULL;
|
||||
--> statement-breakpoint
|
||||
|
||||
-- Migrate existing 'memory-quiz' configs from arcade_rooms.game_config
|
||||
INSERT INTO `room_game_configs` (`id`, `room_id`, `game_name`, `config`, `created_at`, `updated_at`)
|
||||
SELECT
|
||||
lower(hex(randomblob(16))),
|
||||
`id` as room_id,
|
||||
'memory-quiz' as game_name,
|
||||
json_extract(`game_config`, '$."memory-quiz"') as config,
|
||||
`created_at`,
|
||||
`last_activity` as updated_at
|
||||
FROM `arcade_rooms`
|
||||
WHERE json_extract(`game_config`, '$."memory-quiz"') IS NOT NULL;
|
||||
lower(hex(randomblob(16))) as id,
|
||||
id as room_id,
|
||||
game_name,
|
||||
game_config as config,
|
||||
created_at,
|
||||
last_activity as updated_at
|
||||
FROM arcade_rooms
|
||||
WHERE game_config IS NOT NULL
|
||||
AND game_name IS NOT NULL
|
||||
AND game_name IN ('matching', 'memory-quiz', 'complement-race');
|
||||
|
||||
@@ -78,6 +78,13 @@
|
||||
"when": 1760700000000,
|
||||
"tag": "0010_make_game_name_nullable",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 11,
|
||||
"version": "7",
|
||||
"when": 1760800000000,
|
||||
"tag": "0011_add_room_game_configs",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -148,12 +148,15 @@ export async function PATCH(req: NextRequest, context: RouteContext) {
|
||||
await db.delete(schema.arcadeSessions).where(eq(schema.arcadeSessions.roomId, roomId))
|
||||
}
|
||||
|
||||
// Update room settings
|
||||
const [updatedRoom] = await db
|
||||
.update(schema.arcadeRooms)
|
||||
.set(updateData)
|
||||
.where(eq(schema.arcadeRooms.id, roomId))
|
||||
.returning()
|
||||
// Update room settings (only if there's something to update)
|
||||
let updatedRoom = currentRoom
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
;[updatedRoom] = await db
|
||||
.update(schema.arcadeRooms)
|
||||
.set(updateData)
|
||||
.where(eq(schema.arcadeRooms.id, roomId))
|
||||
.returning()
|
||||
}
|
||||
|
||||
// Get aggregated game configs from new table
|
||||
const gameConfig = await getAllGameConfigs(roomId)
|
||||
|
||||
@@ -52,7 +52,7 @@ export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProp
|
||||
const handleGenerateNewName = () => {
|
||||
const allPlayers = Array.from(players.values())
|
||||
const existingNames = allPlayers.filter((p) => p.id !== playerId).map((p) => p.name)
|
||||
const newName = generateUniquePlayerName(existingNames)
|
||||
const newName = generateUniquePlayerName(existingNames, player.emoji)
|
||||
|
||||
setLocalName(newName)
|
||||
updatePlayer(playerId, { name: newName })
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from '@/hooks/useUserPlayers'
|
||||
import { useViewerId } from '@/hooks/useViewerId'
|
||||
import { getNextPlayerColor } from '../types/player'
|
||||
import { generateUniquePlayerName, generateUniquePlayerNames } from '../utils/playerNames'
|
||||
import { generateUniquePlayerName } from '../utils/playerNames'
|
||||
|
||||
// Client-side Player type (compatible with old type)
|
||||
export interface Player {
|
||||
@@ -141,8 +141,13 @@ export function GameModeProvider({ children }: { children: ReactNode }) {
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isInitialized) {
|
||||
if (dbPlayers.length === 0) {
|
||||
// Generate unique names for default players
|
||||
const generatedNames = generateUniquePlayerNames(DEFAULT_PLAYER_CONFIGS.length)
|
||||
// Generate unique names for default players, themed by their emoji
|
||||
const existingNames: string[] = []
|
||||
const generatedNames = DEFAULT_PLAYER_CONFIGS.map((config) => {
|
||||
const name = generateUniquePlayerName(existingNames, config.emoji)
|
||||
existingNames.push(name)
|
||||
return name
|
||||
})
|
||||
|
||||
// Create default players with generated names
|
||||
DEFAULT_PLAYER_CONFIGS.forEach((config, index) => {
|
||||
@@ -167,10 +172,11 @@ export function GameModeProvider({ children }: { children: ReactNode }) {
|
||||
const addPlayer = (playerData?: Partial<Player>) => {
|
||||
const playerList = Array.from(players.values())
|
||||
const existingNames = playerList.map((p) => p.name)
|
||||
const emoji = playerData?.emoji ?? '🎮'
|
||||
|
||||
const newPlayer = {
|
||||
name: playerData?.name ?? generateUniquePlayerName(existingNames),
|
||||
emoji: playerData?.emoji ?? '🎮',
|
||||
name: playerData?.name ?? generateUniquePlayerName(existingNames, emoji),
|
||||
emoji,
|
||||
color: playerData?.color ?? getNextPlayerColor(playerList),
|
||||
isActive: playerData?.isActive ?? false,
|
||||
}
|
||||
@@ -254,8 +260,13 @@ export function GameModeProvider({ children }: { children: ReactNode }) {
|
||||
deletePlayer(player.id)
|
||||
})
|
||||
|
||||
// Generate unique names for default players
|
||||
const generatedNames = generateUniquePlayerNames(DEFAULT_PLAYER_CONFIGS.length)
|
||||
// Generate unique names for default players, themed by their emoji
|
||||
const existingNames: string[] = []
|
||||
const generatedNames = DEFAULT_PLAYER_CONFIGS.map((config) => {
|
||||
const name = generateUniquePlayerName(existingNames, config.emoji)
|
||||
existingNames.push(name)
|
||||
return name
|
||||
})
|
||||
|
||||
// Create default players with generated names
|
||||
DEFAULT_PLAYER_CONFIGS.forEach((config, index) => {
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('playerNames', () => {
|
||||
it('should append number if all combinations are exhausted', () => {
|
||||
// Create a mock with limited attempts
|
||||
const existingNames = ['Swift Ninja']
|
||||
const name = generateUniquePlayerName(existingNames, 1)
|
||||
const name = generateUniquePlayerName(existingNames, undefined, 1)
|
||||
|
||||
// Should either be unique or have a number appended
|
||||
expect(name).toBeTruthy()
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
/**
|
||||
* Fun automatic player name generation system
|
||||
* Generates creative names by combining adjectives with nouns/roles
|
||||
*
|
||||
* Supports avatar-specific theming! Each emoji can have its own personality-matched words.
|
||||
* Falls back gracefully: emoji-specific → category → generic abacus theme
|
||||
*/
|
||||
|
||||
import {
|
||||
EMOJI_SPECIFIC_WORDS,
|
||||
EMOJI_TO_THEME,
|
||||
THEMED_WORD_LISTS,
|
||||
type ThemedWordList,
|
||||
} from './themedWords'
|
||||
|
||||
// Generic abacus-themed words (used as ultimate fallback)
|
||||
const ADJECTIVES = [
|
||||
// Abacus-themed adjectives
|
||||
'Ancient',
|
||||
@@ -114,32 +125,115 @@ const NOUNS = [
|
||||
]
|
||||
|
||||
/**
|
||||
* Generate a random player name by combining an adjective and noun
|
||||
* Get themed word list for an emoji avatar using probabilistic tier selection
|
||||
* Strongly prefers emoji-specific (70%), then category (20%), then global (10%)
|
||||
* This adds variety while maintaining personality-matched theming
|
||||
*/
|
||||
export function generatePlayerName(): string {
|
||||
const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]
|
||||
const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)]
|
||||
function getThemedWordsForEmoji(emoji: string): ThemedWordList {
|
||||
// Collect available tiers
|
||||
const availableTiers: Array<{ weight: number; words: ThemedWordList }> = []
|
||||
|
||||
// Emoji-specific tier (70% preference)
|
||||
const emojiSpecific = EMOJI_SPECIFIC_WORDS[emoji]
|
||||
if (emojiSpecific) {
|
||||
availableTiers.push({ weight: 70, words: emojiSpecific })
|
||||
}
|
||||
|
||||
// Category tier (20% preference)
|
||||
const category = EMOJI_TO_THEME[emoji]
|
||||
if (category) {
|
||||
const categoryTheme = THEMED_WORD_LISTS[category]
|
||||
if (categoryTheme) {
|
||||
availableTiers.push({ weight: 20, words: categoryTheme })
|
||||
}
|
||||
}
|
||||
|
||||
// Global abacus tier (10% preference)
|
||||
availableTiers.push({ weight: 10, words: { adjectives: ADJECTIVES, nouns: NOUNS } })
|
||||
|
||||
// Weighted random selection
|
||||
const totalWeight = availableTiers.reduce((sum, tier) => sum + tier.weight, 0)
|
||||
let random = Math.random() * totalWeight
|
||||
|
||||
for (const tier of availableTiers) {
|
||||
random -= tier.weight
|
||||
if (random <= 0) {
|
||||
return tier.words
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback (should never reach here)
|
||||
return { adjectives: ADJECTIVES, nouns: NOUNS }
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random player name by combining an adjective and noun
|
||||
* Optionally themed based on avatar emoji for ultra-personalized names!
|
||||
* Uses probabilistic tier selection and cross-tier mixing for abacus flavor
|
||||
*
|
||||
* @param emoji - Optional emoji avatar to theme the name around
|
||||
* @returns A creative player name like "Grinning Calculator" or "Lightning Abacist"
|
||||
*/
|
||||
export function generatePlayerName(emoji?: string): string {
|
||||
if (!emoji) {
|
||||
// No emoji provided, use pure abacus theme
|
||||
const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]
|
||||
const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)]
|
||||
return `${adjective} ${noun}`
|
||||
}
|
||||
|
||||
// Get themed words using probabilistic tier selection
|
||||
const themedWords = getThemedWordsForEmoji(emoji)
|
||||
const abacusWords = { adjectives: ADJECTIVES, nouns: NOUNS }
|
||||
|
||||
// 60% chance: Use themed words for both adjective and noun
|
||||
// 30% chance: Mix themed adjective with abacus noun (infuses abacus flavor)
|
||||
// 10% chance: Mix abacus adjective with themed noun (infuses abacus flavor)
|
||||
const mixStrategy = Math.random()
|
||||
|
||||
let adjective: string
|
||||
let noun: string
|
||||
|
||||
if (mixStrategy < 0.6) {
|
||||
// Pure themed
|
||||
adjective = themedWords.adjectives[Math.floor(Math.random() * themedWords.adjectives.length)]
|
||||
noun = themedWords.nouns[Math.floor(Math.random() * themedWords.nouns.length)]
|
||||
} else if (mixStrategy < 0.9) {
|
||||
// Themed adjective + abacus noun
|
||||
adjective = themedWords.adjectives[Math.floor(Math.random() * themedWords.adjectives.length)]
|
||||
noun = abacusWords.nouns[Math.floor(Math.random() * abacusWords.nouns.length)]
|
||||
} else {
|
||||
// Abacus adjective + themed noun
|
||||
adjective = abacusWords.adjectives[Math.floor(Math.random() * abacusWords.adjectives.length)]
|
||||
noun = themedWords.nouns[Math.floor(Math.random() * themedWords.nouns.length)]
|
||||
}
|
||||
|
||||
return `${adjective} ${noun}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique player name that doesn't conflict with existing players
|
||||
* @param existingNames - Array of names already in use
|
||||
* @param emoji - Optional emoji avatar to theme the name around
|
||||
* @param maxAttempts - Maximum attempts to find a unique name (default: 50)
|
||||
* @returns A unique player name
|
||||
*/
|
||||
export function generateUniquePlayerName(existingNames: string[], maxAttempts = 50): string {
|
||||
export function generateUniquePlayerName(
|
||||
existingNames: string[],
|
||||
emoji?: string,
|
||||
maxAttempts = 50
|
||||
): string {
|
||||
const existingNamesSet = new Set(existingNames.map((name) => name.toLowerCase()))
|
||||
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
const name = generatePlayerName()
|
||||
const name = generatePlayerName(emoji)
|
||||
if (!existingNamesSet.has(name.toLowerCase())) {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if we can't find a unique name, append a number
|
||||
const baseName = generatePlayerName()
|
||||
const baseName = generatePlayerName(emoji)
|
||||
let counter = 1
|
||||
while (existingNamesSet.has(`${baseName} ${counter}`.toLowerCase())) {
|
||||
counter++
|
||||
@@ -150,12 +244,13 @@ export function generateUniquePlayerName(existingNames: string[], maxAttempts =
|
||||
/**
|
||||
* Generate a batch of unique player names
|
||||
* @param count - Number of names to generate
|
||||
* @param emoji - Optional emoji avatar to theme the names around
|
||||
* @returns Array of unique player names
|
||||
*/
|
||||
export function generateUniquePlayerNames(count: number): string[] {
|
||||
export function generateUniquePlayerNames(count: number, emoji?: string): string[] {
|
||||
const names: string[] = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
const name = generateUniquePlayerName(names)
|
||||
const name = generateUniquePlayerName(names, emoji)
|
||||
names.push(name)
|
||||
}
|
||||
return names
|
||||
|
||||
6664
apps/web/src/utils/themedWords.ts
Normal file
6664
apps/web/src/utils/themedWords.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "3.17.14",
|
||||
"version": "3.19.0",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user