Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0eed26966c | ||
|
|
49219e34cd | ||
|
|
499ee525a8 | ||
|
|
843b45b14e | ||
|
|
76a8472f12 | ||
|
|
bf02bc14fd | ||
|
|
ffb626f403 | ||
|
|
860fd607be | ||
|
|
3bae00b9a9 | ||
|
|
ff791409cf |
38
CHANGELOG.md
38
CHANGELOG.md
@@ -1,3 +1,41 @@
|
||||
## [3.20.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.19.0...v3.20.0) (2025-10-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* adjust tier probabilities for more abacus flavor ([49219e3](https://github.com/antialias/soroban-abacus-flashcards/commit/49219e34cde32736155a11929d10581e783cba69))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* use per-word-type tier selection for name generation ([499ee52](https://github.com/antialias/soroban-abacus-flashcards/commit/499ee525a835249b439044cf602bf9f0ff322cec))
|
||||
|
||||
## [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,12 @@
|
||||
"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:*)",
|
||||
"Bash(for i in {1..30})",
|
||||
"Bash(do gh run list --limit 1 --json conclusion,status,name,databaseId --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\") - Run ID: \\(.databaseId)\"\"')"
|
||||
],
|
||||
"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,14 @@
|
||||
/**
|
||||
* 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 } from './themedWords'
|
||||
|
||||
// Generic abacus-themed words (used as ultimate fallback)
|
||||
const ADJECTIVES = [
|
||||
// Abacus-themed adjectives
|
||||
'Ancient',
|
||||
@@ -114,32 +120,96 @@ const NOUNS = [
|
||||
]
|
||||
|
||||
/**
|
||||
* Generate a random player name by combining an adjective and noun
|
||||
* Select a word list tier using weighted random selection
|
||||
* Balanced mix: emoji-specific (50%), category (25%), global abacus (25%)
|
||||
*/
|
||||
export function generatePlayerName(): string {
|
||||
const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]
|
||||
const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)]
|
||||
function selectWordListTier(emoji: string, wordType: 'adjectives' | 'nouns'): string[] {
|
||||
// Collect available tiers
|
||||
const availableTiers: Array<{ weight: number; words: string[] }> = []
|
||||
|
||||
// Emoji-specific tier (50% preference)
|
||||
const emojiSpecific = EMOJI_SPECIFIC_WORDS[emoji]
|
||||
if (emojiSpecific) {
|
||||
availableTiers.push({ weight: 50, words: emojiSpecific[wordType] })
|
||||
}
|
||||
|
||||
// Category tier (25% preference)
|
||||
const category = EMOJI_TO_THEME[emoji]
|
||||
if (category) {
|
||||
const categoryTheme = THEMED_WORD_LISTS[category]
|
||||
if (categoryTheme) {
|
||||
availableTiers.push({ weight: 25, words: categoryTheme[wordType] })
|
||||
}
|
||||
}
|
||||
|
||||
// Global abacus tier (25% preference)
|
||||
availableTiers.push({ weight: 25, words: wordType === 'adjectives' ? ADJECTIVES : 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 wordType === 'adjectives' ? ADJECTIVES : NOUNS
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random player name by combining an adjective and noun
|
||||
* Optionally themed based on avatar emoji for ultra-personalized names!
|
||||
* Uses per-word-type probabilistic tier selection for natural variety
|
||||
*
|
||||
* @param emoji - Optional emoji avatar to theme the name around
|
||||
* @returns A creative player name like "Grinning Calculator" or "Lightning Smiler"
|
||||
*/
|
||||
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}`
|
||||
}
|
||||
|
||||
// Select tier independently for each word type
|
||||
// This creates natural mixing: adjective might be emoji-specific while noun is global
|
||||
const adjectiveList = selectWordListTier(emoji, 'adjectives')
|
||||
const nounList = selectWordListTier(emoji, 'nouns')
|
||||
|
||||
const adjective = adjectiveList[Math.floor(Math.random() * adjectiveList.length)]
|
||||
const noun = nounList[Math.floor(Math.random() * nounList.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 +220,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.20.0",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user