Compare commits

...

57 Commits

Author SHA1 Message Date
semantic-release-bot
bb682ed79e chore(release): 4.14.0 [skip ci]
## [4.14.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.15...v4.14.0) (2025-10-19)

### Features

* **card-sorting:** add spectator mode UX enhancements ([4ab093a](4ab093a9d8))

### Documentation

* **arcade:** add comprehensive spectator mode documentation ([1eb6cec](1eb6ceca19))
* **arcade:** spec spectator mode UX enhancements for card sorting ([aafba77](aafba77d62))
2025-10-19 02:22:01 +00:00
Thomas Hallock
4ab093a9d8 feat(card-sorting): add spectator mode UX enhancements
Implement visual indicators and disabled states for spectator mode to
make it clear when users are watching vs playing.

**Provider.tsx**:
- Expose `localPlayerId` and `isSpectating` in context
- `isSpectating = !localPlayerId` (room members without active players)

**GameComponent.tsx**:
- Add spectator banner ("👀 Spectating [Player]'s game")
- Shows during playing/results phases for spectators
- Yellow gradient background with clear visual feedback

**PlayingPhase.tsx**:
- Disable all interactive elements for spectators:
  - Available cards (opacity: 0.5, cursor: not-allowed)
  - Position slots (opacity: 0.5, cursor: not-allowed)
  - Insert buttons (disabled, visual feedback)
  - Action buttons (Reveal, Check Solution, End Game)
- Block handlers early: `if (isSpectating) return`
- Remove hover effects when spectating

**User Experience**:
- Spectators see real-time game state updates
- All controls visually disabled (grayed out, not-allowed cursor)
- Cannot interact with game (click handlers blocked)
- Clear banner indicates spectator role

Completes spectator mode implementation per ARCADE_ARCHITECTURE.md.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 21:20:51 -05:00
Thomas Hallock
aafba77d62 docs(arcade): spec spectator mode UX enhancements for card sorting
Created comprehensive specification for optional spectator mode UI/UX
improvements to make spectator status clear and controls visually disabled.

**Five Enhancement Areas**:

1. **Context Exposure**
   - Add `localPlayerId` and `isSpectating` to context
   - Enable components to make spectator-aware UI decisions

2. **Spectator Indicator Banner**
   - "👀 Spectating [Player]'s game" during playing/results
   - "👤 Add a Player to Start" during setup
   - Soft blue styling, non-intrusive

3. **Visual Disabled States**
   - All buttons: opacity 0.5, cursor not-allowed
   - All cards: opacity 0.6, pointer-events none
   - Setup, playing, and results phases

4. **Spectator Mode Tests**
   - Banner visibility tests
   - Disabled control interaction tests
   - State synchronization tests
   - Context exposure tests

5. **Player Ownership Tests**
   - Validate correct player can move
   - Reject moves from non-active players
   - Reject moves when user doesn't own player
   - Reject spectator move attempts

**Includes**:
- Detailed code examples for all changes
- Visual mockups of UI states
- Implementation checklist (20 tasks)
- Success criteria
- Questions for user before implementation

Game is production-ready without these - enhancements are for improved UX.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 21:16:57 -05:00
Thomas Hallock
feecda78d0 chore: update Claude Code permissions
Auto-updated permissions during session.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 19:49:17 -05:00
Thomas Hallock
1eb6ceca19 docs(arcade): add comprehensive spectator mode documentation
Document spectator mode as a first-class feature in the arcade architecture.

**ARCADE_ARCHITECTURE.md**:
- Added "SPECTATOR" to core terminology
- Restructured sync modes to three patterns:
  - Local Play (No Network Sync)
  - Room-Based with Spectator Mode (RECOMMENDED)
  - Pure Multiplayer (Room-Only)
- Added complete "Spectator Mode" section (297-603):
  - Implementation patterns
  - UI/UX considerations (indicators, disabled controls)
  - When to use spectator mode
  - Example scenarios (Family Game Night, Classroom)
  - Server-side validation
  - Testing requirements
  - Migration path

**CARD_SORTING_AUDIT.md**:
- Updated to reflect room-based sync is CORRECT for spectator mode
- Changed from "CRITICAL ISSUE" to "CORRECT IMPLEMENTATION"
- Removed incorrect recommendation to use `roomId: undefined`
- Added enhancement recommendations (UI indicators, tests)
- Updated compliance checklist: 9/13 items passing

Card Sorting correctly enables spectator mode - room members without active
players can watch games in real-time, creating social/collaborative experiences.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 19:49:17 -05:00
semantic-release-bot
e72839e0f3 chore(release): 4.13.15 [skip ci]
## [4.13.15](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.14...v4.13.15) (2025-10-19)

### Bug Fixes

* **docker:** bypass PEP 668 externally-managed-environment error ([bb59c61](bb59c61638))
2025-10-19 00:35:28 +00:00
Thomas Hallock
bb59c61638 fix(docker): bypass PEP 668 externally-managed-environment error
Python 3.11+ prevents global pip installs by default. Since this is a
controlled Docker environment, use --break-system-packages to allow installing
the flashcard generation dependencies.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 19:34:23 -05:00
semantic-release-bot
593aed81cc chore(release): 4.13.14 [skip ci]
## [4.13.14](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.13...v4.13.14) (2025-10-19)

### Bug Fixes

* **docker:** install py3-pip for Python dependency installation ([0f55909](0f55909533))

### Documentation

* add Panda CSS styling framework documentation ([c92d7d9](c92d7d9d89))
* **arcade:** fix incorrect Tailwind references - use Panda CSS ([34a377d](34a377d91b))
2025-10-19 00:30:55 +00:00
Thomas Hallock
0f55909533 fix(docker): install py3-pip for Python dependency installation
The pip3 command was not available, causing the Docker build to fail when
trying to install Python dependencies from requirements.txt. Added py3-pip
to the Alpine package installation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 19:29:42 -05:00
Thomas Hallock
c92d7d9d89 docs: add Panda CSS styling framework documentation
Added critical section to .claude/CLAUDE.md documenting that this
project uses Panda CSS, NOT Tailwind CSS.

This prevents future confusion and incorrect references to Tailwind
in code, comments, or documentation.

Includes:
- Framework identification and configuration locations
- Import patterns and token syntax
- Common mistakes to avoid with examples
- Reference to GAME_THEMES.md for arcade styling

This mistake was made earlier in this session when documenting the
game theme system, so documenting it now to prevent recurrence.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 19:28:38 -05:00
Thomas Hallock
34a377d91b docs(arcade): fix incorrect Tailwind references - use Panda CSS
Corrected documentation and comments that incorrectly referenced
Tailwind CSS. This project uses Panda CSS for styling.

Fixed in:
- `/src/lib/arcade/game-themes.ts` - Updated all comments
- `.claude/GAME_THEMES.md` - Fixed documentation references

The color system uses Panda CSS's preset colors (blue.100, blue.200, etc.)
not Tailwind. The hex values are the same, but we should be accurate
about which framework we're using.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 19:03:25 -05:00
semantic-release-bot
3dcdfb4986 chore(release): 4.13.13 [skip ci]
## [4.13.13](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.12...v4.13.13) (2025-10-19)

### Bug Fixes

* **docker:** install Python dependencies for flashcard generation ([c9b7e92](c9b7e92f39))

### Code Refactoring

* **arcade:** standardize game card themes with preset system ([0209975](0209975af6)), closes [#99f6e4](https://github.com/antialias/soroban-abacus-flashcards/issues/99f6e4) [#5eead4](https://github.com/antialias/soroban-abacus-flashcards/issues/5eead4)
2025-10-19 00:01:40 +00:00
Thomas Hallock
0209975af6 refactor(arcade): standardize game card themes with preset system
Create a centralized theme system to prevent inconsistent game card
styling. All games now use standard color presets instead of manually
specifying gradients.

## Changes

**New Theme System:**
- Created `/src/lib/arcade/game-themes.ts` with 10 standard themes
- All themes use Tailwind 100-200 colors for consistent pastel appearance
- Exported `getGameTheme()` helper and `GAME_THEMES` constants via SDK
- Added comprehensive documentation in `.claude/GAME_THEMES.md`

**Migrated All Games:**
- card-sorting: Uses `getGameTheme('teal')`
- memory-quiz: Uses `getGameTheme('blue')`
- matching: Uses `getGameTheme('purple')`
- complement-race: Uses `getGameTheme('blue')`

**Benefits:**
-  Prevents future styling inconsistencies
-  One-line theme setup instead of three properties
-  TypeScript autocomplete for available themes
-  Centralized maintenance - update all games by changing theme definition
-  Clear documentation prevents mistakes

**Before:**
```typescript
color: 'teal',
gradient: 'linear-gradient(135deg, #99f6e4, #5eead4)',  // Manual, error-prone
borderColor: 'teal.200',
```

**After:**
```typescript
...getGameTheme('teal')  // Simple, consistent, discoverable
```

This fixes the root cause where card-sorting needed manual gradient
adjustments - now all games automatically get professional styling.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 19:00:38 -05:00
Thomas Hallock
c9b7e92f39 fix(docker): install Python dependencies for flashcard generation
The Python scripts in packages/core require pyyaml, pillow, imagehash, and
other dependencies to generate flashcards. Install these from requirements.txt
during Docker build.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 19:00:25 -05:00
semantic-release-bot
c56a47cb60 chore(release): 4.13.12 [skip ci]
## [4.13.12](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.11...v4.13.12) (2025-10-18)

### Bug Fixes

* **card-sorting:** use blue gradient matching other game cards ([bdb84f5](bdb84f5d90))
2025-10-18 23:47:18 +00:00
Thomas Hallock
bdb84f5d90 fix(card-sorting): use blue gradient matching other game cards
Change card sorting game to use the same blue gradient as Memory Lightning
and Speed Complement Race to ensure consistent appearance on game chooser.

Updated to: linear-gradient(135deg, #dbeafe, #bfdbfe) (blue-100 to blue-200)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 18:46:06 -05:00
semantic-release-bot
33838b7fa7 chore(release): 4.13.11 [skip ci]
## [4.13.11](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.10...v4.13.11) (2025-10-18)

### Bug Fixes

* **card-sorting:** match game selector background to other games ([db62519](db62519f9b)), closes [#ccfbf1](https://github.com/antialias/soroban-abacus-flashcards/issues/ccfbf1) [#99f6e4](https://github.com/antialias/soroban-abacus-flashcards/issues/99f6e4)
* **docker:** copy core package with Python scripts to production image ([33e9ad2](33e9ad2f79))
2025-10-18 23:43:33 +00:00
Thomas Hallock
33e9ad2f79 fix(docker): copy core package with Python scripts to production image
The flashcard generator was failing in production because the packages/core
directory (which contains the Python scripts for PDF generation) wasn't being
copied to the production Docker image. The SorobanGenerator class needs these
scripts at runtime to generate flashcards.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 18:42:24 -05:00
Thomas Hallock
db62519f9b fix(card-sorting): match game selector background to other games
Change card sorting game gradient from bright teal (teal-200 to teal-300)
to softer pastel teal (teal-100 to teal-200) to match the lighter gradient
style used by other arcade games (Memory Lightning, Matching Pairs).

Updated gradient: linear-gradient(135deg, #ccfbf1, #99f6e4)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 18:42:20 -05:00
semantic-release-bot
ec978de0b3 chore(release): 4.13.10 [skip ci]
## [4.13.10](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.9...v4.13.10) (2025-10-18)

### Bug Fixes

* add Typst to Docker image for flashcard generation ([d9a7694](d9a7694031))

### Code Refactoring

* remove 'Complete Soroban Learning Platform' section ([42dcbff](42dcbff857))
2025-10-18 23:27:21 +00:00
Thomas Hallock
d9a7694031 fix: add Typst to Docker image for flashcard generation
The flashcard generator requires both Python and Typst to work.
Added typst to the runtime dependencies in the Dockerfile.

This fixes the issue where flashcards work in dev but fail in production.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 18:26:07 -05:00
Thomas Hallock
42dcbff857 refactor: remove 'Complete Soroban Learning Platform' section
Removed stats banner section as platform is still in development.
Also removed unused StatItem component.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 18:14:43 -05:00
semantic-release-bot
5923d341a0 chore(release): 4.13.9 [skip ci]
## [4.13.9](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.8...v4.13.9) (2025-10-18)

### Bug Fixes

* set color on abacus container div for numeral visibility ([cd47960](cd4796024e)), closes [#1f2937](https://github.com/antialias/soroban-abacus-flashcards/issues/1f2937)
2025-10-18 23:12:51 +00:00
Thomas Hallock
cd4796024e fix: set color on abacus container div for numeral visibility
Removed customStyles approach that wasn't working.
Added color: #1f2937 to container div - numerals inherit from parent.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 18:11:43 -05:00
semantic-release-bot
cff948708f chore(release): 4.13.8 [skip ci]
## [4.13.8](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.7...v4.13.8) (2025-10-18)

### Bug Fixes

* use color instead of fill for numeral styling ([ea10c16](ea10c16811))
2025-10-18 23:10:18 +00:00
Thomas Hallock
ea10c16811 fix: use color instead of fill for numeral styling
SVG foreign objects use CSS color property, not fill.
Changed numerals customStyle from fill to color for dark gray text.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 18:09:12 -05:00
semantic-release-bot
474d31576f chore(release): 4.13.7 [skip ci]
## [4.13.7](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.6...v4.13.7) (2025-10-18)

### Bug Fixes

* add dark color for abacus numerals ([73ff32c](73ff32c243)), closes [#1f2937](https://github.com/antialias/soroban-abacus-flashcards/issues/1f2937)
* use app-wide abacus config and remove instruction text ([0a50c73](0a50c733b0))
2025-10-18 23:07:53 +00:00
Thomas Hallock
73ff32c243 fix: add dark color for abacus numerals
Added customStyles to make numerals dark gray (#1f2937) with bold weight
for good contrast on white background.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 18:06:47 -05:00
Thomas Hallock
0a50c733b0 fix: use app-wide abacus config and remove instruction text
- Added useAbacusConfig() hook to get app-wide settings
- AbacusReact now respects beadShape, colorScheme, hideInactiveBeads from app config
- Removed "Click the beads to interact!" instruction text
- Simplified pane layout (just centered)

Now matches the styling/settings used throughout the rest of the app.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 18:06:47 -05:00
semantic-release-bot
1386378ca1 chore(release): 4.13.6 [skip ci]
## [4.13.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.5...v4.13.6) (2025-10-18)

### Bug Fixes

* simplify abacus pane with light background ([30f48ab](30f48ab897))
2025-10-18 23:05:58 +00:00
Thomas Hallock
30f48ab897 fix: simplify abacus pane with light background
- Changed background from dark to white for better contrast
- Removed duplicate number display (abacus has built-in numerals)
- Removed custom styles (defaults work well on light background)
- Updated instruction text color to gray.700 for readability

Abacus component isn't designed for dark mode yet, so light pane is simpler.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 18:04:49 -05:00
semantic-release-bot
d2f6b8b46c chore(release): 4.13.5 [skip ci]
## [4.13.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.4...v4.13.5) (2025-10-18)

### Bug Fixes

* correct AbacusReact API usage and add structural styling ([247377f](247377fca3)), closes [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24) [#a78](https://github.com/antialias/soroban-abacus-flashcards/issues/a78)
2025-10-18 23:01:22 +00:00
Thomas Hallock
247377fca3 fix: correct AbacusReact API usage and add structural styling
Fixed critical issues based on storybook examples:
- Changed callback syntax: onValueChange prop directly (not in callbacks object)
- Added reckoningBar styling: golden stroke (#fbbf24, 4px width)
- Added columnPosts styling: purple stroke (#a78bfa, 3px width)
- Now structural elements are visible on dark background and interactive works

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 18:00:23 -05:00
semantic-release-bot
be39401716 chore(release): 4.13.4 [skip ci]
## [4.13.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.3...v4.13.4) (2025-10-18)

### Bug Fixes

* **card-sorting:** increase card tile sizes to contain abacuses ([d2a3b7a](d2a3b7ae2e))
2025-10-18 22:56:13 +00:00
Thomas Hallock
d2a3b7ae2e fix(card-sorting): increase card tile sizes to contain abacuses
Make card tiles larger to properly contain AbacusReact SVGs without overflow.

**Issue:**
Card tiles were too small (90px × 90px and 90px × 110px) to contain the
AbacusReact SVGs, causing abacuses to overflow their containers.

**Fix:**
- **Available cards**: Increased from 90px × 90px to 140px × 140px
- **Position slots**: Increased from 90px × 110px to 140px × 160px

**Result:**
Abacus SVGs now fit comfortably within their card containers without
overflowing, while maintaining the overflow: hidden constraint as a
safety measure.

src/arcade-games/card-sorting/components/PlayingPhase.tsx:283-284,419-420

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:55:15 -05:00
semantic-release-bot
39ab605279 chore(release): 4.13.3 [skip ci]
## [4.13.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.2...v4.13.3) (2025-10-18)

### Bug Fixes

* **card-sorting:** increase SVG size to fill card containers ([cf9d893](cf9d893f3f))
2025-10-18 22:53:10 +00:00
Thomas Hallock
cf9d893f3f fix(card-sorting): increase SVG size to fill card containers
Make AbacusReact SVGs larger by allowing them to fill their containers.

**Issue:**
SVG containers were set to fixed small sizes (74px × 74px and 70px × 70px),
making the abacuses appear too small within their cards.

**Fix:**
- **Available cards**: Changed SVG container from 74px × 74px to 100% × 100%
  to fill the entire 90px card (accounting for 8px padding)
- **Position slots**: Changed SVG container from 70px × 70px to flex: 1,
  width: 100% to fill available space while leaving room for label

**Result:**
Abacuses now appear larger and fill their card containers better while
still being constrained by overflow: hidden to prevent breaking out.

src/arcade-games/card-sorting/components/PlayingPhase.tsx:314-329,456-473

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:51:57 -05:00
semantic-release-bot
e6d0bd4953 chore(release): 4.13.2 [skip ci]
## [4.13.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.1...v4.13.2) (2025-10-18)

### Bug Fixes

* show initial value and improve numeral contrast ([1b57f6d](1b57f6ddec)), closes [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24)
2025-10-18 22:51:19 +00:00
Thomas Hallock
1b57f6ddec fix: show initial value and improve numeral contrast
- Changed back to controlled value (with state update) to display initial 1234567
- Added customStyles for numerals: golden color (#fbbf24), bold, larger font
- Numerals now have excellent contrast against dark background

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:50:12 -05:00
semantic-release-bot
d38ea312a7 chore(release): 4.13.1 [skip ci]
## [4.13.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.0...v4.13.1) (2025-10-18)

### Bug Fixes

* use defaultValue for interactive abacus control ([06aca98](06aca986ac))
2025-10-18 22:47:40 +00:00
Thomas Hallock
06aca986ac fix: use defaultValue for interactive abacus control
Changed from controlled (value) to uncontrolled (defaultValue) pattern
to allow AbacusReact to fully manage its own state and be interactive.
Still tracks value changes via onValueChange callback for display.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:46:32 -05:00
semantic-release-bot
a126466037 chore(release): 4.13.0 [skip ci]
## [4.13.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.12.0...v4.13.0) (2025-10-18)

### Features

* make home page abacus interactive with audio ([9a53d7e](9a53d7e5db))
2025-10-18 22:45:39 +00:00
Thomas Hallock
9a53d7e5db feat: make home page abacus interactive with audio
Enhanced the hero abacus to be a fully interactive showcase:
- Increased size from default to 2.2x scale factor
- Enabled interactive mode (click beads to change value)
- Enabled smooth animations for bead movements
- Enabled audio feedback with volume at 0.4
- Added live value display below abacus in large golden text
- Added instructional text "Click the beads to interact!"
- Shows place value numbers on each column

The abacus now serves as a "try it now" demo right on the landing page.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:44:40 -05:00
semantic-release-bot
d2d8f7740f chore(release): 4.12.0 [skip ci]
## [4.12.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.11.1...v4.12.0) (2025-10-18)

### Features

* redesign home page with component showcase ([29af265](29af265958))
2025-10-18 22:44:06 +00:00
Thomas Hallock
29af265958 feat: redesign home page with component showcase
Replaced outdated landing page with component-based "storybook" design:
- Large featured AbacusReact component (1,234,567 with place-value colors)
- Color scheme showcase with 4 actual AbacusReact instances
- Dark, fancy theme with purple gradients and golden accents
- All game links now point to /games instead of /arcade
- Hover animations and visual polish throughout
- Dot pattern background texture

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:42:55 -05:00
semantic-release-bot
291bcc581d chore(release): 4.11.1 [skip ci]
## [4.11.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.11.0...v4.11.1) (2025-10-18)

### Bug Fixes

* **card-sorting:** center AbacusReact SVGs in card tiles ([26edec1](26edec1bbf))
2025-10-18 22:41:08 +00:00
Thomas Hallock
26edec1bbf fix(card-sorting): center AbacusReact SVGs in card tiles
Improve centering of abacus SVGs within both available cards and position slots.

**Issue:**
AbacusReact SVGs were not properly centered within their card containers,
appearing off-center or misaligned.

**Fix:**
- **Available cards**: Added maxHeight: '100%' constraint and display: 'block'
  with margin: '0 auto' to SVG styling
- **Position slots**: Changed container to use flex: 1 and proper flex centering,
  constrained SVG to maxWidth: '70px' with centering styles

**Changes:**
- Available card SVG container: Added display flex with center alignment
- Available card SVG: maxHeight: '100%', display: 'block', margin: '0 auto'
- Position slot SVG container: width: '100%', flex: 1, flex centering
- Position slot SVG: maxWidth: '70px', maxHeight: '100%', display: 'block', margin: '0 auto'

Now AbacusReact SVGs render centered within their card tiles regardless of
the actual SVG dimensions.

src/arcade-games/card-sorting/components/PlayingPhase.tsx:314-330,457-475

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:40:03 -05:00
semantic-release-bot
da4fdc90e0 chore(release): 4.11.0 [skip ci]
## [4.11.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.6...v4.11.0) (2025-10-18)

### Features

* **home:** redesign home page to showcase complete platform ([ee6c4f2](ee6c4f2f4f))
2025-10-18 22:34:07 +00:00
Thomas Hallock
ee6c4f2f4f feat(home): redesign home page to showcase complete platform
Replace outdated "flashcard generator" landing page with comprehensive
platform showcase highlighting all three pillars: arcade games,
interactive learning, and flashcard creation.

**New Home Page Structure:**
- Compact hero with 3 CTAs: Play Games, Learn, Create
- 4 arcade game cards with player counts and mode tags
- Two-column feature sections for Learning & Flashcards
- Multiplayer features grid (4 cards)
- Stats banner: 4 games, 8 max players, 3 learning modes, 4+ formats

**Visual Design:**
- Smaller, denser components to fit more content
- Information-rich showcase vs marketing fluff
- Purple gradient hero matching guide branding
- Responsive grid layouts for all screen sizes

**Result:**
Home page now accurately represents the full platform:
multiplayer arcade games + interactive tutorials + flashcard tools.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:33:01 -05:00
semantic-release-bot
9b9f0cdbcb chore(release): 4.10.6 [skip ci]
## [4.10.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.5...v4.10.6) (2025-10-18)

### Bug Fixes

* **card-sorting:** position slots flow horizontally with wrap ([e14ffe4](e14ffe44d6))
2025-10-18 22:24:03 +00:00
Thomas Hallock
e14ffe44d6 fix(card-sorting): position slots flow horizontally with wrap
Fix position slots to match Python original - they should flow horizontally
and wrap, not stack vertically.

**Before:**
- Container: display: flex, flexDirection: 'column' (vertical stack)
- Each slot + insert button pair wrapped in a div
- One slot per line with insert button below it

**After (matching Python lines 3101-3111):**
- Container: display: flex, flexWrap: 'wrap' (horizontal flow)
- Slots and insert buttons flow together as siblings
- Wraps naturally based on container width
- Dashed border container with semi-transparent background

**Changes:**
- Position slots container: flex-wrap instead of flex-direction: column
- Removed wrapper div around slot+button pairs
- Added React keys to slots and buttons
- Added container styling (padding, background, border)

Now matches the Python original where all slots and + buttons flow
together horizontally and wrap as needed.

src/arcade-games/card-sorting/components/PlayingPhase.tsx:362-524

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:22:56 -05:00
semantic-release-bot
d5bc0bb27c chore(release): 4.10.5 [skip ci]
## [4.10.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.4...v4.10.5) (2025-10-18)

### Code Refactoring

* **arcade:** merge /arcade/room into /arcade route ([0790074](0790074ffc))
2025-10-18 22:11:17 +00:00
Thomas Hallock
0790074ffc refactor(arcade): merge /arcade/room into /arcade route
Simplify arcade routing by moving room page to main arcade route.
The /arcade route now handles both game selection and gameplay,
eliminating the need for a separate /arcade/room route.

**Changes:**
- Move /arcade/room/page.tsx → /arcade/page.tsx
- Update import paths for styled-system (room/ → arcade/)
- Remove legacy GAMES_CONFIG handling (now registry-only)
- Delete obsolete EnhancedChampionArena component

**Result:**
- Users navigate directly to /arcade for all arcade features
- Game selection UI shows when no game selected in room
- Selected game renders when room has gameName set
- Simpler, more intuitive URL structure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:10:10 -05:00
semantic-release-bot
1a44daf2ce chore(release): 4.10.4 [skip ci]
## [4.10.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.3...v4.10.4) (2025-10-18)

### Bug Fixes

* **card-sorting:** match Python card layout with flex wrap ([9679d68](9679d68154))
2025-10-18 21:35:53 +00:00
Thomas Hallock
9679d68154 fix(card-sorting): match Python card layout with flex wrap
Fix available cards layout to match the Python original implementation.

**Before:**
- Grid layout with responsive columns (1/2/3 cols)
- Cards stretched to fill grid cells
- Each card on its own line on mobile

**After (matching Python web_generator.py lines 3077-3087):**
- Flex container with wrap
- Fixed 90px x 90px card size
- Cards flow naturally and wrap based on container width
- Dashed border container with semi-transparent background
- Revealed numbers positioned absolutely in top-right corner
- Matching hover/selection animations from original

**Visual Changes:**
- Container: flex wrap, centered, dashed border, rgba background
- Cards: 90px x 90px fixed size
- Revealed numbers: absolute positioned badge (top-right)
- Selection state: scale(1.1), blue border, blue background
- Hover: translateY(-5px) lift effect

src/arcade-games/card-sorting/components/PlayingPhase.tsx:265-348

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 16:34:50 -05:00
semantic-release-bot
80ba94203d chore(release): 4.10.3 [skip ci]
## [4.10.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.2...v4.10.3) (2025-10-18)

### Bug Fixes

* **arcade:** remove broken query param from game URLs ([87631af](87631af678))
2025-10-18 21:15:27 +00:00
Thomas Hallock
87631af678 fix(arcade): remove broken query param from game URLs
Remove ?game= query parameter from GameSelector URLs. The /arcade/room
page doesn't use query params - it uses the room's gameName field from
the database and handles game selection through its own UI.

This was causing the URL to have a stale ?game= param that didn't match
the actual selected game, breaking testing and navigation.

**Changed:**
- GameSelector.tsx: url changed from `/arcade/room?game=${name}` to `/arcade/room`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 16:14:14 -05:00
29 changed files with 3743 additions and 680 deletions

View File

@@ -1,3 +1,195 @@
## [4.14.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.15...v4.14.0) (2025-10-19)
### Features
* **card-sorting:** add spectator mode UX enhancements ([4ab093a](https://github.com/antialias/soroban-abacus-flashcards/commit/4ab093a9d8ba5b290da44aaa6aa71ad7d7149b32))
### Documentation
* **arcade:** add comprehensive spectator mode documentation ([1eb6cec](https://github.com/antialias/soroban-abacus-flashcards/commit/1eb6ceca19805be53dfe3194e07e68002b2d09a7))
* **arcade:** spec spectator mode UX enhancements for card sorting ([aafba77](https://github.com/antialias/soroban-abacus-flashcards/commit/aafba77d62b47403f21f5dd72859d6a6fbb1efac))
## [4.13.15](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.14...v4.13.15) (2025-10-19)
### Bug Fixes
* **docker:** bypass PEP 668 externally-managed-environment error ([bb59c61](https://github.com/antialias/soroban-abacus-flashcards/commit/bb59c61638e60b0678043e954e044d9390f88e7f))
## [4.13.14](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.13...v4.13.14) (2025-10-19)
### Bug Fixes
* **docker:** install py3-pip for Python dependency installation ([0f55909](https://github.com/antialias/soroban-abacus-flashcards/commit/0f55909533414bdc07f113b93bb8bfa21367959b))
### Documentation
* add Panda CSS styling framework documentation ([c92d7d9](https://github.com/antialias/soroban-abacus-flashcards/commit/c92d7d9d89a72e012c30fc5ac88fa96e7a526f83))
* **arcade:** fix incorrect Tailwind references - use Panda CSS ([34a377d](https://github.com/antialias/soroban-abacus-flashcards/commit/34a377d91b37ad47968b85aedd112f9fcf72ad63))
## [4.13.13](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.12...v4.13.13) (2025-10-19)
### Bug Fixes
* **docker:** install Python dependencies for flashcard generation ([c9b7e92](https://github.com/antialias/soroban-abacus-flashcards/commit/c9b7e92f39ee7aa7f13606c2836763144df102e7))
### Code Refactoring
* **arcade:** standardize game card themes with preset system ([0209975](https://github.com/antialias/soroban-abacus-flashcards/commit/0209975af642944cc5a434c0b44205a87e634e7e)), closes [#99f6e4](https://github.com/antialias/soroban-abacus-flashcards/issues/99f6e4) [#5eead4](https://github.com/antialias/soroban-abacus-flashcards/issues/5eead4)
## [4.13.12](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.11...v4.13.12) (2025-10-18)
### Bug Fixes
* **card-sorting:** use blue gradient matching other game cards ([bdb84f5](https://github.com/antialias/soroban-abacus-flashcards/commit/bdb84f5d909542060fa886a83a5af62c4a785a98))
## [4.13.11](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.10...v4.13.11) (2025-10-18)
### Bug Fixes
* **card-sorting:** match game selector background to other games ([db62519](https://github.com/antialias/soroban-abacus-flashcards/commit/db62519f9beb0b4bc6120e1fd5ec251cfde5c3c1)), closes [#ccfbf1](https://github.com/antialias/soroban-abacus-flashcards/issues/ccfbf1) [#99f6e4](https://github.com/antialias/soroban-abacus-flashcards/issues/99f6e4)
* **docker:** copy core package with Python scripts to production image ([33e9ad2](https://github.com/antialias/soroban-abacus-flashcards/commit/33e9ad2f79b591f1c5ee57a6691e1bcf48420859))
## [4.13.10](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.9...v4.13.10) (2025-10-18)
### Bug Fixes
* add Typst to Docker image for flashcard generation ([d9a7694](https://github.com/antialias/soroban-abacus-flashcards/commit/d9a769403187bf70fb069be7ffe77417a62271a5))
### Code Refactoring
* remove 'Complete Soroban Learning Platform' section ([42dcbff](https://github.com/antialias/soroban-abacus-flashcards/commit/42dcbff85708ad378550634cbf7a3345eccb578e))
## [4.13.9](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.8...v4.13.9) (2025-10-18)
### Bug Fixes
* set color on abacus container div for numeral visibility ([cd47960](https://github.com/antialias/soroban-abacus-flashcards/commit/cd4796024e41f731ae5d83c82f6582e19d6eaf99)), closes [#1f2937](https://github.com/antialias/soroban-abacus-flashcards/issues/1f2937)
## [4.13.8](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.7...v4.13.8) (2025-10-18)
### Bug Fixes
* use color instead of fill for numeral styling ([ea10c16](https://github.com/antialias/soroban-abacus-flashcards/commit/ea10c16811eb969b9963417079c330ae9ff295ba))
## [4.13.7](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.6...v4.13.7) (2025-10-18)
### Bug Fixes
* add dark color for abacus numerals ([73ff32c](https://github.com/antialias/soroban-abacus-flashcards/commit/73ff32c2432beb62710e57aa8b3b4793eca43fda)), closes [#1f2937](https://github.com/antialias/soroban-abacus-flashcards/issues/1f2937)
* use app-wide abacus config and remove instruction text ([0a50c73](https://github.com/antialias/soroban-abacus-flashcards/commit/0a50c733b089c7c341f0fdef47da78d1c61a3cb5))
## [4.13.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.5...v4.13.6) (2025-10-18)
### Bug Fixes
* simplify abacus pane with light background ([30f48ab](https://github.com/antialias/soroban-abacus-flashcards/commit/30f48ab8976976688e089b07ece7fdae6d7ada79))
## [4.13.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.4...v4.13.5) (2025-10-18)
### Bug Fixes
* correct AbacusReact API usage and add structural styling ([247377f](https://github.com/antialias/soroban-abacus-flashcards/commit/247377fca35ee3433e02ad594ecc1c4f391f0143)), closes [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24) [#a78](https://github.com/antialias/soroban-abacus-flashcards/issues/a78)
## [4.13.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.3...v4.13.4) (2025-10-18)
### Bug Fixes
* **card-sorting:** increase card tile sizes to contain abacuses ([d2a3b7a](https://github.com/antialias/soroban-abacus-flashcards/commit/d2a3b7ae2e3f6819b8d9ace32be22f04f748d1bc))
## [4.13.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.2...v4.13.3) (2025-10-18)
### Bug Fixes
* **card-sorting:** increase SVG size to fill card containers ([cf9d893](https://github.com/antialias/soroban-abacus-flashcards/commit/cf9d893f3fdbef6e91cd0ba283d602b9215569f1))
## [4.13.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.1...v4.13.2) (2025-10-18)
### Bug Fixes
* show initial value and improve numeral contrast ([1b57f6d](https://github.com/antialias/soroban-abacus-flashcards/commit/1b57f6ddecf3a118f2e4fadd1a91be1256f5a034)), closes [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24)
## [4.13.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.0...v4.13.1) (2025-10-18)
### Bug Fixes
* use defaultValue for interactive abacus control ([06aca98](https://github.com/antialias/soroban-abacus-flashcards/commit/06aca986ace4d76b70f2fd2f5e57f66758185b38))
## [4.13.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.12.0...v4.13.0) (2025-10-18)
### Features
* make home page abacus interactive with audio ([9a53d7e](https://github.com/antialias/soroban-abacus-flashcards/commit/9a53d7e5db18853aca4e2e0c7abc799217feaecf))
## [4.12.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.11.1...v4.12.0) (2025-10-18)
### Features
* redesign home page with component showcase ([29af265](https://github.com/antialias/soroban-abacus-flashcards/commit/29af265958f9fdab0253b92e153c01575840454d))
## [4.11.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.11.0...v4.11.1) (2025-10-18)
### Bug Fixes
* **card-sorting:** center AbacusReact SVGs in card tiles ([26edec1](https://github.com/antialias/soroban-abacus-flashcards/commit/26edec1bbf038264405ec9d161edcd18f67a6fc6))
## [4.11.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.6...v4.11.0) (2025-10-18)
### Features
* **home:** redesign home page to showcase complete platform ([ee6c4f2](https://github.com/antialias/soroban-abacus-flashcards/commit/ee6c4f2f4f39e3b30f59c54866c3857c218fb80f))
## [4.10.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.5...v4.10.6) (2025-10-18)
### Bug Fixes
* **card-sorting:** position slots flow horizontally with wrap ([e14ffe4](https://github.com/antialias/soroban-abacus-flashcards/commit/e14ffe44d66d0c97bc0cc4e0c255698e88ce723a))
## [4.10.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.4...v4.10.5) (2025-10-18)
### Code Refactoring
* **arcade:** merge /arcade/room into /arcade route ([0790074](https://github.com/antialias/soroban-abacus-flashcards/commit/0790074ffc5008bce9a162fe0ddbd1d5c214c4f7))
## [4.10.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.3...v4.10.4) (2025-10-18)
### Bug Fixes
* **card-sorting:** match Python card layout with flex wrap ([9679d68](https://github.com/antialias/soroban-abacus-flashcards/commit/9679d68154ac8b6a2f905ec7d17a34b39bc00237))
## [4.10.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.2...v4.10.3) (2025-10-18)
### Bug Fixes
* **arcade:** remove broken query param from game URLs ([87631af](https://github.com/antialias/soroban-abacus-flashcards/commit/87631af6788bd7b42e671374e55ec0ad8435900c))
## [4.10.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.1...v4.10.2) (2025-10-18)

View File

@@ -33,8 +33,8 @@ RUN turbo build --filter=@soroban/web
FROM node:18-alpine AS runner
WORKDIR /app
# Install Python and build tools for better-sqlite3 (needed at runtime)
RUN apk add --no-cache python3 py3-setuptools make g++
# Install Python, pip, build tools for better-sqlite3, and Typst (needed at runtime)
RUN apk add --no-cache python3 py3-pip py3-setuptools make g++ typst
# Create non-root user
RUN addgroup --system --gid 1001 nodejs
@@ -55,6 +55,12 @@ COPY --from=builder --chown=nextjs:nodejs /app/apps/web/drizzle ./apps/web/drizz
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/node_modules ./apps/web/node_modules
# Copy core package (needed for Python flashcard generation scripts)
COPY --from=builder --chown=nextjs:nodejs /app/packages/core ./packages/core
# Install Python dependencies for flashcard generation
RUN pip3 install --no-cache-dir --break-system-packages -r packages/core/requirements.txt
# Copy package.json files for module resolution
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/package.json ./apps/web/

View File

@@ -16,6 +16,7 @@ Following `docs/terminology-user-player-room.md`:
- **PLAYER ROSTER** - All PLAYERS belonging to a USER (can have many)
- **ACTIVE PLAYERS** - PLAYERS where `isActive = true` - these are the ones that actually participate in games
- **ROOM MEMBER** - A USER's participation in a multiplayer room (tracked in `room_members` table)
- **SPECTATOR** - A room member who watches another player's game without participating (see Spectator Mode section)
**Important:** A USER can have many PLAYERS in their roster, but only the ACTIVE PLAYERS (where `isActive = true`) participate in games. This enables "hot-potato" style local multiplayer where multiple people share the same device. This is LOCAL play (not networked), even though multiple PLAYERS participate.
@@ -27,25 +28,46 @@ In arcade sessions:
## Critical Architectural Requirements
### 1. Mode Isolation (MUST ENFORCE)
### 1. Game Synchronization Modes
**Local Play** (`/arcade/[game-name]`)
The arcade system supports three synchronization patterns:
#### Local Play (No Network Sync)
**Route**: Custom route or dedicated local page
**Use Case**: Practice, offline play, or games that should never be visible to others
- MUST NOT sync game state across the network
- MUST NOT use room data, even if the USER is currently a member of an active room
- MUST create isolated, per-USER game sessions
- MUST pass `roomId: undefined` to `useArcadeSession`
- Game state lives only in the current browser tab/session
- CAN have multiple ACTIVE PLAYERS from the same USER (local multiplayer / hot-potato)
- State is NOT shared across the network, only within the browser session
**Room-Based Play** (`/arcade/room`)
#### Room-Based with Spectator Mode (RECOMMENDED PATTERN)
**Route**: `/arcade/room` (or use room context anywhere)
**Use Case**: Most arcade games - enables spectating even for single-player games
- MUST sync game state across all room members via network
- MUST use the USER's current active room
- MUST coordinate moves via server WebSocket
- SYNCS game state across all room members via network
- Uses the USER's current active room (`roomId: roomData?.id`)
- Coordinates moves via server WebSocket
- Game state is shared across all ACTIVE PLAYERS from all USERS in the room
- When a PLAYER makes a move, all room members see it in real-time
- CAN ALSO have multiple ACTIVE PLAYERS per USER (networked + local multiplayer combined)
- **Non-playing room members become SPECTATORS** (see Spectator Mode section)
- When a PLAYER makes a move, all room members see it in real-time (players + spectators)
- CAN have multiple ACTIVE PLAYERS per USER (networked + local multiplayer combined)
**✅ This is the PREFERRED pattern** - even for single-player games like Card Sorting, because:
- Enables spectator mode automatically
- Creates social experience ("watch me solve this!")
- No extra code needed
- Works seamlessly with multiplayer games too
#### Pure Multiplayer (Room-Only)
**Route**: `/arcade/room` with validation
**Use Case**: Games that REQUIRE multiple players (e.g., competitive battles)
- Same as Room-Based with Spectator Mode
- Plus: Validates minimum player count before starting
- Plus: May prevent game start if `activePlayers.length < minPlayers`
### 2. Room ID Usage Rules
@@ -258,6 +280,327 @@ sendMove({
- Mode: Room-based play (roomId: "room_xyz")
- 5 total active PLAYERS across 2 devices, all synced over network
5. **Single-player game with spectators (Card Sorting):**
- USER A: "guest_abc"
- Active PLAYERS: ["player_001"]
- Playing Card Sorting Challenge
- USER B: "guest_def"
- Active PLAYERS: [] (none selected)
- Spectating USER A's game
- USER C: "guest_ghi"
- Active PLAYERS: ["player_005"]
- Spectating USER A's game (could play after USER A finishes)
- Mode: Room-based play (roomId: "room_xyz")
- All room members see USER A's card placements in real-time
- Spectators cannot interact with the game state
## Spectator Mode
### Overview
Spectator mode is automatically enabled when using room-based sync (`roomId: roomData?.id`). Any room member who is not actively playing becomes a spectator and can watch the game in real-time.
**Key Benefits**:
- Creates social/collaborative experience even for single-player games
- "Watch me solve this!" engagement
- Learning by observation
- Cheering/coaching opportunity
- No extra implementation needed
### How It Works
1. **Automatic Role Assignment**:
- Room members with active PLAYERs in the game → **Players**
- Room members without active PLAYERs in the game → **Spectators**
2. **State Synchronization**:
- All game state updates broadcast to entire room via `game:${roomId}` socket room
- Spectators receive same state updates as players
- Spectators see game in real-time as it happens
3. **Interaction Control**:
- Players can make moves (send move actions)
- Spectators can only observe (no move actions permitted)
- Server validates PLAYER ownership before accepting moves
### Implementation Pattern
**Provider** (Room-Based with Spectator Support):
```typescript
export function CardSortingProvider({ children }: { children: ReactNode }) {
const { data: viewerId } = useViewerId()
const { roomData } = useRoomData() // ✅ Fetch room data
const { activePlayers, players } = useGameMode()
// Find local player (if any)
const localPlayerId = useMemo(() => {
return Array.from(activePlayers).find((id) => {
const player = players.get(id)
return player?.isLocal !== false
})
}, [activePlayers, players])
const { state, sendMove, exitSession } = useArcadeSession<CardSortingState>({
userId: viewerId || '',
roomId: roomData?.id, // ✅ Enable spectator mode
initialState,
applyMove: applyMoveOptimistically,
})
// Actions check if local player exists before allowing moves
const startGame = useCallback(() => {
if (!localPlayerId) {
console.warn('[CardSorting] No local player - spectating only')
return // ✅ Spectators cannot start game
}
sendMove({ type: 'START_GAME', playerId: localPlayerId, ... })
}, [localPlayerId, sendMove])
// ... rest of provider
}
```
**Key Implementation Points**:
- Always check `if (!localPlayerId)` before allowing moves
- Return early or show "Spectating..." message
- Don't throw errors - spectating is a valid state
- UI should indicate spectator vs player role
### UI/UX Considerations
#### 1. Spectator Indicators
Show visual feedback when user is spectating:
```typescript
{!localPlayerId && state.gamePhase === 'playing' && (
<div className={spectatorBannerStyles}>
👀 Spectating {state.playerMetadata.name}'s game
</div>
)}
```
#### 2. Disabled Controls
Disable interactive elements for spectators:
```typescript
<button
onClick={placeCard}
disabled={!localPlayerId} // Spectators can't interact
className={css({
opacity: !localPlayerId ? 0.5 : 1,
cursor: !localPlayerId ? 'not-allowed' : 'pointer',
})}
>
Place Card
</button>
```
#### 3. Join Prompt
For games that support multiple players, show "Join Game" option:
```typescript
{!localPlayerId && state.gamePhase === 'setup' && (
<button onClick={selectPlayerAndJoin}>
Join as Player
</button>
)}
```
#### 4. Real-Time Updates
Ensure spectators see smooth updates:
- Use optimistic UI updates (same as players)
- Show animations for state changes
- Display current player's moves as they happen
### When to Use Spectator Mode
**✅ Use Spectator Mode (room-based sync) For**:
- Single-player puzzle games (Card Sorting, Sudoku, etc.)
- Turn-based competitive games (Matching Pairs Battle)
- Cooperative games (Memory Lightning)
- Any game where watching is educational/entertaining
- Social/family game nights
- Classroom settings (teacher demonstrates, students watch)
**❌ Avoid Spectator Mode (use local-only) For**:
- Private practice sessions
- Timed competitive games where watching gives unfair advantage
- Games with personal/sensitive content
- Offline/no-network scenarios
- Performance-critical games (reduce network overhead)
### Example Scenarios
#### Scenario 1: Family Game Night - Card Sorting
```
Room: "Smith Family Game Night"
USER A (Dad): Playing Card Sorting
- Active PLAYER: "Dad 👨"
- State: Placing cards, 6/8 complete
- Can interact with game
USER B (Mom): Spectating
- Active PLAYERS: [] (none selected)
- State: Sees Dad's card placements in real-time
- Cannot place cards
- Can cheer and help
USER C (Kid): Spectating
- Active PLAYER: "Emma 👧" (selected but not in this game)
- State: Watching to learn strategy
- Will play next round
Flow:
1. Dad starts Card Sorting
2. Mom and Kid see setup phase
3. Dad places cards one by one
4. Mom/Kid see each placement instantly
5. Dad checks solution
6. Everyone sees the score together
7. Kid says "My turn!" and starts their own game
8. Dad and Mom become spectators
```
#### Scenario 2: Classroom - Memory Lightning
```
Room: "Ms. Johnson's 3rd Grade"
USER A (Teacher): Playing cooperatively with 2 students
- Active PLAYERS: ["Teacher 👩‍🏫", "Student 1 👦"]
- State: Memorizing cards
- Both can participate
USER B-F (5 other students): Spectating
- Watching demonstration
- Learning the rules
- Will join next round
Flow:
1. Teacher demonstrates with 2 students
2. Other students watch and learn
3. Round ends
4. Teacher sets up new round
5. New students join as players
6. Previous players become spectators
```
### Server-Side Handling
The server must handle spectators correctly:
```typescript
// Validate move ownership
socket.on('game-move', ({ move, roomId }) => {
const session = getSession(roomId)
// Check if PLAYER making move is in the active players list
if (!session.activePlayers.includes(move.playerId)) {
return {
error: 'PLAYER not in game - spectators cannot make moves'
}
}
// Check if USER owns this PLAYER
const playerOwner = getPlayerOwner(move.playerId)
if (playerOwner !== socket.userId) {
return {
error: 'USER does not own this PLAYER'
}
}
// Valid move - apply and broadcast
const newState = validator.validateMove(session.state, move)
io.to(`game:${roomId}`).emit('state-update', newState) // ALL room members get update
})
```
**Key Server Logic**:
- Validate PLAYER is in `session.activePlayers`
- Validate USER owns PLAYER
- Broadcast to entire room (players + spectators)
- Spectators receive updates but cannot send moves
### Testing Spectator Mode
```typescript
describe('Spectator Mode', () => {
it('should allow room members to spectate single-player games', () => {
// Setup: USER A and USER B in same room
// Action: USER A starts Card Sorting (single-player)
// Assert: USER B receives game state updates
// Assert: USER B cannot make moves
// Assert: USER B sees USER A's card placements in real-time
})
it('should prevent spectators from making moves', () => {
// Setup: USER A playing, USER B spectating
// Action: USER B attempts to place a card
// Assert: Server rejects move (PLAYER not in activePlayers)
// Assert: Client UI disables controls for USER B
})
it('should show spectator indicator in UI', () => {
// Setup: USER B spectating USER A's game
// Assert: UI shows "Spectating [Player Name]" banner
// Assert: Interactive controls are disabled
// Assert: Game state is visible
})
it('should allow spectator to join next round', () => {
// Setup: USER B spectating USER A's Card Sorting game
// Action: USER A finishes game, returns to setup
// Action: USER B starts new game
// Assert: USER A becomes spectator
// Assert: USER B becomes active player
})
})
```
### Migration Path
**For existing games**:
If your game currently uses `roomId: roomData?.id`, it already supports spectator mode! You just need to:
1. ✅ Check for `!localPlayerId` before allowing moves
2. ✅ Add spectator UI indicators
3. ✅ Disable controls when spectating
4. ✅ Test spectator experience
**Example Fix**:
```typescript
// Before (will crash for spectators)
const placeCard = useCallback((cardId, position) => {
sendMove({
type: 'PLACE_CARD',
playerId: localPlayerId, // ❌ Will be undefined for spectators!
...
})
}, [localPlayerId, sendMove])
// After (spectator-safe)
const placeCard = useCallback((cardId, position) => {
if (!localPlayerId) {
console.warn('Spectators cannot place cards')
return // ✅ Spectators blocked from moving
}
sendMove({
type: 'PLACE_CARD',
playerId: localPlayerId,
...
})
}, [localPlayerId, sendMove])
```
## Common Mistakes to Avoid
### Mistake 1: Conditional Room Usage

View File

@@ -0,0 +1,440 @@
# Arcade Routing Architecture - Complete Overview
## 1. Current /arcade Page
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/app/arcade/page.tsx` (lines 1-129)
**Purpose:** The main arcade landing page - displays the "Champion Arena"
**Key Components:**
- `ArcadeContent()` - Renders the main arcade interface
- Uses `EnhancedChampionArena` component which contains `GameSelector`
- The `GameSelector` displays all available games as cards
- `GameSelector` includes both legacy games and registry games
**Current Flow:**
1. User navigates to `/arcade`
2. Page renders `FullscreenProvider` wrapper
3. Displays `PageWithNav` with title "🏟️ Champion Arena"
4. Content area shows `EnhancedChampionArena``GameSelector`
5. `GameSelector` renders `GameCard` components for each game
6. When user clicks a game card, `GameCard` calls `router.push(config.url)`
7. For registry games, `config.url` is `/arcade/room?game={gameName}`
8. For legacy games, URL would be direct to their page
**State Management:**
- `GameModeContext` provides player selection (emoji, name, color)
- `PageWithNav` wraps content and provides mini-nav with:
- Active player list
- Add player button
- Game mode indicator (single/battle/tournament)
- Exit session handler
## 2. Current /arcade/room Page
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/app/arcade/room/page.tsx` (lines 1-359)
**Purpose:** "Magical place" that shows either a game OR the game chooser, driven by room state
**Three States:**
### State 1: Loading
- Shows "Loading room..." message
- Waits for `useRoomData()` hook to resolve
### State 2: Game Selection UI (when `!roomData.gameName`)
- Shows large game selection buttons
- User clicks to select a game
- Calls `setRoomGame()` mutation to save selection to room
- Invokes `handleGameSelect()` which:
1. Checks if game exists in registry via `hasGame(gameType)`
2. If registry game: calls `setRoomGame({roomId, gameName: gameType})`
3. If legacy game: maps to internal name via `GAME_TYPE_TO_NAME`, then calls `setRoomGame()`
4. Game selection is persisted to the room database
### State 3: Game Display (when `roomData.gameName` is set)
- Checks game registry first via `hasGame(roomData.gameName)`
- If registry game:
- Gets game definition via `getGame(roomData.gameName)`
- Renders: `<Provider><GameComponent /></Provider>`
- Provider and GameComponent come from game registry definition
- If legacy game:
- Switch statement with TODO for individual games
- Currently only shows "Game not yet supported"
**Key Hook:**
- `useRoomData()` - Fetches current room from API and subscribes to socket updates
- Returns `roomData` with fields: `id`, `name`, `code`, `gameName`, `gameConfig`, `members`, `memberPlayers`
- Also returns `isLoading` boolean
**Navigation Flow:**
1. User navigates to `/arcade`
2. `GameCard` onClick calls `router.push('/arcade/room?game={gameName}')`
3. User arrives at `/arcade/room`
4. If NOT in a room yet: Shows error with link back to `/arcade`
5. If in room but no game selected: Shows game selection UI
6. If game selected: Loads and displays game
## 3. The "Mini App Nav" - GameContextNav Component
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/components/nav/GameContextNav.tsx` (lines 1-372)
**What It Is:**
The "mini app nav" is actually a sophisticated component within the `PageWithNav` wrapper that intelligently shows different UI based on context:
**Components & Props:**
- `navTitle` - Current page title (e.g., "Champion Arena", "Choose Game", "Speed Complement Race")
- `navEmoji` - Icon emoji for current page
- `gameMode` - Computed from active player count: 'none' | 'single' | 'battle' | 'tournament'
- `activePlayers` - Array of selected players
- `inactivePlayers` - Array of available but unselected players
- `shouldEmphasize` - Boolean to emphasize player selection
- `showFullscreenSelection` - Boolean to show fullscreen mode for player selection
- `roomInfo` - Optional arcade room data (roomId, roomName, gameName, playerCount, joinCode)
- `networkPlayers` - Remote players from room members
**Three Display Modes:**
### Mode 1: Fullscreen Player Selection
- When `showFullscreenSelection === true`
- Displays:
- Large title with emoji
- Game mode indicator
- Fullscreen player selection UI
- Shows all inactive players for selection
### Mode 2: Solo Mode (NOT in room)
- When `roomInfo` is undefined
- Shows:
- **Game Title Section** (left side):
- `GameTitleMenu` with game title and emoji
- Menu options: Setup, New Game, Quit
- `GameModeIndicator`
- **Player Section** (right side):
- `ActivePlayersList` - shows selected players
- `AddPlayerButton` - add more players
### Mode 3: Room Mode (IN a room)
- When `roomInfo` is defined
- Shows:
- **Hidden:** Game title section (display: none)
- **Room Info Pane** (left side):
- `RoomInfo` component with room details
- Game mode indicator with color/emoji
- Room name, player count, join code
- `NetworkPlayerIndicator` components for remote players
- **Player Section** (may be hidden):
- Shows local active players
- Add player button (for local players only)
**Key Sub-Components:**
- `GameTitleMenu` - Menu for game options (setup, new game, quit)
- `GameModeIndicator` - Shows 🎯 Solo, ⚔️ Battle, 🏆 Tournament, 👥 Select
- `RoomInfo` - Displays room metadata
- `NetworkPlayerIndicator` - Shows remote players with scores/streaks
- `ActivePlayersList` - List of selected players
- `AddPlayerButton` - Button to add more players with popover
- `FullscreenPlayerSelection` - Large player picker for fullscreen mode
- `PendingInvitations` - Banner for room invitations
**State Management:**
- Lifted from `PageWithNav` to preserve state across remounts:
- `showPopover` / `setShowPopover` - AddPlayerButton popover state
- `activeTab` / `setActiveTab` - 'add' or 'invite' tab selection
## 4. Navigation Flow
### Flow 1: Solo Player → Game Selection → Room Creation → Game Start
```
/arcade (Champion Arena)
↓ [Select players - updates GameModeContext]
↓ [Click game card - GameCard.onClick → router.push]
/arcade/room (if not in room, shows game selector)
↓ [Select game - calls setRoomGame mutation]
↓ [Room created, gameName saved to roomData]
↓ [useRoomData refetch updates roomData.gameName]
/arcade/room (now displays the game)
↓ [Game Provider and Component render]
```
### Flow 2: Multiplayer - Room Invitation
```
User A: Creates room via Champion Arena
User B: Receives invitation
User B: Joins room via /arcade/room
User B: Sees same game selection (if set) or game selector (if not set)
```
### Flow 3: Exit Game
```
/arcade/room (in-game)
↓ [Click "Quit" or "Exit Session" in GameContextNav]
↓ [onExitSession callback → router.push('/arcade')]
/arcade (back to champion arena)
↓ Player selection reset by GameModeContext
```
## 5. Game Chooser / Game Selection System
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/components/GameSelector.tsx` (lines 1-112)
**How It Works:**
1. `GameSelector` component gets all games from both sources:
- Legacy `GAMES_CONFIG` (currently empty)
- Registry games via `getAllGames()`
2. For each game, creates `GameCard` component with configuration including `url` field
3. Game Cards rendered in 2-column grid (responsive)
4. When card clicked:
- `GameCard` checks `activePlayerCount` against game's `maxPlayers`
- If valid: calls `router.push(config.url)` - client-side navigation via Next.js
- If invalid: blocks navigation with warning
**Two Game Systems:**
### Registry Games (NEW - Modular)
- Location: `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/arcade-games/`
- File: `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/lib/arcade/game-registry.ts`
- Examples: `complement-race`, `memory-quiz`, `matching`
- Each game has: `manifest` (metadata), `Provider` (context), `GameComponent` (UI)
- Games registered globally via `registerGame()` function
### Legacy Games (OLD)
- Location: Directly in `/app/arcade/` directory
- Examples: `/app/arcade/complement-race/page.tsx`
- Currently, only complement-race is partially migrated
- Direct URL structure: `/arcade/{gameName}/page.tsx`
**Game Config Structure (for display):**
```javascript
{
name: string, // Display name
fullName?: string, // Longer name for detailed view
description: string, // Short description
longDescription?: string, // Detailed description
icon: emoji, // Game icon emoji
gradient: css gradient, // Background gradient
borderColor: css color, // Border color for availability
maxPlayers: number, // Player limit for validation
chips?: string[], // Feature labels
color?: 'green'|'purple'|'blue', // Color theme
difficulty?: string, // Difficulty level
available: boolean, // Is game available
}
```
## 6. Key Components Summary
### PageWithNav - Main Layout Wrapper
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/components/PageWithNav.tsx` (lines 1-192)
**Responsibilities:**
- Wraps all game/arcade pages
- Manages GameContextNav state (mini-nav)
- Handles player configuration dialog
- Shows moderation notifications
- Renders top navigation bar via `AppNavBar`
**Key Props:**
- `navTitle` - Passed to GameContextNav
- `navEmoji` - Passed to GameContextNav
- `gameName` - Internal game name for API
- `emphasizePlayerSelection` - Highlight player controls
- `onExitSession` - Callback when user exits
- `onSetup`, `onNewGame` - Game-specific callbacks
- `children` - Page content
### AppNavBar - Top Navigation Bar
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/components/AppNavBar.tsx` (lines 1-625)
**Variants:**
- `full` - Standard navigation (default for non-game pages)
- `minimal` - Game navigation (auto-selected for `/arcade` and `/games`)
**Minimal Nav Features:**
- Hamburger menu (left) with:
- Site navigation (Home, Create, Guide, Games)
- Controls (Fullscreen, Exit Arcade)
- Abacus style dropdown
- Centered game context (navSlot)
- Fullscreen indicator badge
### EnhancedChampionArena - Main Arcade Display
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/components/EnhancedChampionArena.tsx` (lines 1-40)
**Responsibilities:**
- Container for game selector
- Full-height flex layout
- Passes configuration to `GameSelector`
### GameSelector - Game Grid
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/components/GameSelector.tsx` (lines 1-112)
**Responsibilities:**
- Fetches all games from registry
- Arranges in responsive grid
- Shows header "🎮 Available Games"
- Renders GameCard for each game
### GameCard - Individual Game Button
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/components/GameCard.tsx` (lines 1-241)
**Responsibilities:**
- Displays game with icon, name, description
- Shows feature chips and player count indicator
- Validates player count against game requirements
- Handles click to navigate to game
- Two variants: compact and detailed
## 7. State Management
### GameModeContext
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/contexts/GameModeContext.tsx` (lines 1-325)
**Manages:**
- Local players (Map<string, Player>)
- Active players (Set<string>)
- Game mode (computed from active player count)
- Player CRUD operations (add, update, remove)
**Key Features:**
- Fetches players from user's local DB via `useUserPlayers()`
- Creates 4 default players if none exist
- When in room: merges room members' players (marked as isLocal: false)
- Syncs to room members via `notifyRoomOfPlayerUpdate()`
**Computed Values:**
- `activePlayerCount` - Size of activePlayers set
- `gameMode`:
- 1 player → 'single'
- 2 players → 'battle'
- 3+ players → 'tournament'
### useRoomData Hook
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/hooks/useRoomData.ts` (lines 1-450+)
**Manages:**
- Current room fetching via TanStack Query
- Socket.io real-time updates
- Room state (members, players, game name)
- Moderation events (kicked, banned, invitations)
**Key Operations:**
- `fetchCurrentRoom()` - GET `/api/arcade/rooms/current`
- `createRoomApi()` - POST `/api/arcade/rooms`
- `joinRoomApi()` - POST `/api/arcade/rooms/{id}/join`
- `leaveRoomApi()` - POST `/api/arcade/rooms/{id}/leave`
- `setRoomGame()` - Updates room's gameName and gameConfig
**Socket Events:**
- `join-user-channel` - Personal notifications
- `join-room` - Subscribe to room updates
- `room-joined` - Refresh when entering room
- `member-joined` - When player joins
- `member-left` - When player leaves
- `room-players-updated` - When players change
- Moderation events (kicked, banned, etc.)
## 8. Routing Summary
**Current URL Structure:**
```
/ → Home page (Soroban Generator)
/create → Create flashcards
/guide → Tutorial guide
/games → Games library (external game pages)
/arcade → Champion Arena (main landing with game selector)
/arcade/room → Active game display or game selection UI
/arcade/room?game={name} → Query param for game selection (optional)
/arcade/complement-race → OLD: Direct complement-race page (legacy)
/arcade/complement-race/practice → Complement-race practice mode
/arcade/complement-race/sprint → Complement-race sprint mode
/arcade/complement-race/survival → Complement-race survival mode
/arcade/memory-quiz → Memory quiz game page (legacy structure)
```
**Query Parameters:**
- `/arcade/room?game={gameName}` - Optional game selection (parsed by GameCard)
## 9. Key Differences: /arcade vs /arcade/room
| Aspect | /arcade | /arcade/room |
|--------|---------|--------------|
| **Purpose** | Game selection hub | Active game display or selection within room |
| **Displays** | GameSelector with all games | Selected game OR game selector if no game in room |
| **Room Context** | Optional (can start solo) | Usually in a room (fetches via useRoomData) |
| **Navigation** | Click game → /arcade/room | Click game → Saves to room → Displays game |
| **GameContextNav** | Shows player selector | Shows room info when joined |
| **Player State** | Local only | Local + remote (room members) |
| **Exit Button** | Usually hidden | Shows "Exit Session" to return to /arcade |
| **Socket Connection** | Optional | Always connected (in room) |
| **Page Transition** | User controls | Driven by room state updates |
## 10. Planning the Merge (/arcade/room → /arcade)
**Challenges to Consider:**
1. **URL Consolidation:**
- `/arcade/room` would become a sub-path or handled by `/arcade` with state
- Query param `?game={name}` could drive game selection
- Current: `/arcade/room?game=complement-race`
- Could become: `/arcade?game=complement-race&mode=play`
2. **Route Disambiguation:**
- `/arcade` needs to handle: game selection display, game display, game loading
- Same page different modes based on state
- Or: Sub-routes like `/arcade/select`, `/arcade/play`
3. **State Layering:**
- Local game mode (solo player, GameModeContext)
- Room state (multiplayer, useRoomData)
- Both need to coexist
4. **Navigation Preservation:**
- Currently: `GameCard``router.push('/arcade/room?game=X')`
- After merge: Would need new logic
- Fullscreen state must persist (uses Next.js router, not reload)
5. **PageWithNav Behavior:**
- Mini-nav shows game selection UI vs room info
- Currently determined by `roomInfo` presence
- After merge: Need same logic but one route
6. **Game Display:**
- Currently: `/arcade/room` fetches game from registry
- New: `/arcade` would need same game registry lookup
- Game Provider/Component rendering must work identically
**Merge Strategy Options:**
### Option A: Single Route with Modes
```
/arcade
├── Mode: browse (default, show GameSelector)
├── Mode: select (game selected, show GameSelector for confirmation)
└── Mode: play (in-game, show game display)
```
### Option B: Sub-routes
```
/arcade
├── /arcade (selector)
├── /arcade/play (game display)
└── /arcade/configure (player config)
```
### Option C: Query-Parameter Driven
```
/arcade
├── /arcade (default - selector)
├── /arcade?game=X (game loading)
└── /arcade?game=X&playing=true (in-game)
```
**Recommendation:** Option C (Query-driven) is closest to current architecture and requires minimal changes to existing logic.

View File

@@ -0,0 +1,404 @@
# Card Sorting Challenge - Arcade Architecture Audit
**Date**: 2025-10-18
**Auditor**: Claude Code
**Reference**: `.claude/ARCADE_ARCHITECTURE.md`
**Update**: 2025-10-18 - Spectator mode recognized as intentional feature
## Executive Summary
The Card Sorting Challenge game was audited against the Arcade Architecture requirements documented in `.claude/ARCADE_ARCHITECTURE.md`. The initial audit identified the room-based sync pattern as a potential issue, but this was later recognized as an **intentional spectator mode feature**.
**Overall Status**: ✅ **CORRECT IMPLEMENTATION** (with spectator mode enabled)
---
## Spectator Mode Feature (Initially Flagged as Issue)
### ✅ Room-Based Sync Enables Spectator Mode (INTENTIONAL FEATURE)
**Location**: `/src/arcade-games/card-sorting/Provider.tsx` lines 286, 312
**Initial Assessment**: The provider **ALWAYS** calls `useRoomData()` and **ALWAYS** passes `roomId: roomData?.id` to `useArcadeSession`. This was initially flagged as a mode isolation violation.
```typescript
const { roomData } = useRoomData() // Line 286
...
const { state, sendMove, exitSession } = useArcadeSession<CardSortingState>({
userId: viewerId || '',
roomId: roomData?.id, // Line 312 - Room-based sync
initialState: mergedInitialState,
applyMove: applyMoveOptimistically,
})
```
**Actual Behavior (CORRECT)**:
- ✅ When a USER plays Card Sorting in a room, the game state SYNCS ACROSS THE ROOM NETWORK
- ✅ This enables **spectator mode** - other room members can watch the game in real-time
- ✅ Card Sorting is single-player (`maxPlayers: 1`), but spectators can watch and cheer
- ✅ Room members without active players become spectators automatically
- ✅ Creates social/collaborative experience ("Watch me solve this!")
**Supported By Architecture** (ARCADE_ARCHITECTURE.md, Spectator Mode section):
> Spectator mode is automatically enabled when using room-based sync (`roomId: roomData?.id`).
> Any room member who is not actively playing becomes a spectator and can watch the game in real-time.
>
> **✅ This is the PREFERRED pattern** - even for single-player games like Card Sorting, because:
> - Enables spectator mode automatically
> - Creates social experience ("watch me solve this!")
> - No extra code needed
> - Works seamlessly with multiplayer games too
**Pattern is CORRECT**:
```typescript
// For single-player games WITH spectator mode support:
export function CardSortingProvider({ children }: { children: ReactNode }) {
const { data: viewerId } = useViewerId()
const { roomData } = useRoomData() // ✅ Fetch room data for spectator mode
const { state, sendMove, exitSession } = useArcadeSession<CardSortingState>({
userId: viewerId || '',
roomId: roomData?.id, // ✅ Enable spectator mode - room members can watch
initialState: mergedInitialState,
applyMove: applyMoveOptimistically,
})
// Actions check for localPlayerId - spectators won't have one
const startGame = useCallback(() => {
if (!localPlayerId) {
console.warn('[CardSorting] No local player - spectating only')
return // ✅ Spectators blocked from starting game
}
// ... send move
}, [localPlayerId, sendMove])
}
```
**Why This Pattern is Used**:
This enables spectator mode as a first-class user experience. Room members can:
- Watch other players solve puzzles
- Learn strategies by observation
- Cheer and coach
- Take turns (finish watching, then play yourself)
**Status**: ✅ CORRECT IMPLEMENTATION
**Priority**: N/A - No changes needed
---
## Scope of Spectator Mode
This same room-based sync pattern exists in **ALL** arcade games currently:
```bash
$ grep -A 2 "useRoomData" /path/to/arcade-games/*/Provider.tsx
card-sorting/Provider.tsx: const { roomData } = useRoomData()
complement-race/Provider.tsx: const { roomData } = useRoomData()
matching/Provider.tsx: const { roomData } = useRoomData()
memory-quiz/Provider.tsx: const { roomData } = useRoomData()
```
All providers pass `roomId: roomData?.id` to `useArcadeSession`. This means:
-**All games** support spectator mode automatically
-**Single-player games** (card-sorting) enable "watch me play" experience
-**Multiplayer games** (matching, memory-quiz, complement-race) support both players and spectators
**Status**: This is the recommended pattern for social/family gaming experiences.
---
## ✅ Correct Implementations
### 1. Active Players Handling (CORRECT)
**Location**: `/src/arcade-games/card-sorting/Provider.tsx` lines 287, 294-299
The provider correctly uses `useGameMode()` to access active players:
```typescript
const { activePlayers, players } = useGameMode()
const localPlayerId = useMemo(() => {
return Array.from(activePlayers).find((id) => {
const player = players.get(id)
return player?.isLocal !== false
})
}, [activePlayers, players])
```
✅ Only includes players with `isActive = true`
✅ Finds the first local player for this single-player game
✅ Follows architecture pattern correctly
---
### 2. Player ID vs User ID (CORRECT)
**Location**: Provider.tsx lines 383-491 (all move creators)
All moves correctly use:
- `playerId: localPlayerId` (PLAYER makes the move)
- `userId: viewerId || ''` (USER owns the session)
```typescript
// Example from startGame (lines 383-391)
sendMove({
type: 'START_GAME',
playerId: localPlayerId, // ✅ PLAYER ID
userId: viewerId || '', // ✅ USER ID
data: { playerMetadata, selectedCards },
})
```
✅ Follows USER/PLAYER distinction correctly
✅ Server can validate PLAYER ownership
✅ Matches architecture requirements
---
### 3. Validator Implementation (CORRECT)
**Location**: `/src/arcade-games/card-sorting/Validator.ts`
The validator correctly implements all required methods:
```typescript
export class CardSortingValidator implements GameValidator<CardSortingState, CardSortingMove> {
validateMove(state, move, context): ValidationResult { ... }
isGameComplete(state): boolean { ... }
getInitialState(config: CardSortingConfig): CardSortingState { ... }
}
```
✅ All move types have validation
`getInitialState()` accepts full config
✅ Proper error messages
✅ Server-side score calculation
✅ State transitions validated
---
### 4. Game Registration (CORRECT)
**Location**: `/src/arcade-games/card-sorting/index.ts`
Uses the modular game system correctly:
```typescript
export const cardSortingGame = defineGame<CardSortingConfig, CardSortingState, CardSortingMove>({
manifest,
Provider: CardSortingProvider,
GameComponent,
validator: cardSortingValidator,
defaultConfig,
validateConfig: validateCardSortingConfig,
})
```
✅ Proper TypeScript generics
✅ Manifest includes all required fields
✅ Config validation function provided
✅ Uses `getGameTheme()` for consistent styling
---
### 5. Type Definitions (CORRECT)
**Location**: `/src/arcade-games/card-sorting/types.ts`
State and move types properly extend base types:
```typescript
export interface CardSortingState extends GameState { ... }
export interface CardSortingConfig extends GameConfig { ... }
export type CardSortingMove =
| { type: 'START_GAME', playerId: string, userId: string, ... }
| { type: 'PLACE_CARD', playerId: string, userId: string, ... }
...
```
✅ All moves include `playerId` and `userId`
✅ Extends SDK base types
✅ Proper TypeScript structure
---
## Recommendations
### 1. Add Spectator UI Indicators (Enhancement)
The current implementation correctly enables spectator mode, but could be enhanced with better UI/UX:
**Action**: Add spectator indicators to `GameComponent.tsx`:
```typescript
export function GameComponent() {
const { state, localPlayerId } = useCardSorting()
return (
<>
{!localPlayerId && state.gamePhase === 'playing' && (
<div className={spectatorBannerStyles}>
👀 Spectating {state.playerMetadata?.name || 'player'}'s game
</div>
)}
{/* Disable controls when spectating */}
<button
onClick={placeCard}
disabled={!localPlayerId}
className={css({
opacity: !localPlayerId ? 0.5 : 1,
cursor: !localPlayerId ? 'not-allowed' : 'pointer',
})}
>
Place Card
</button>
</>
)
}
```
**Also Consider**:
- Show "Join Game" prompt during setup phase for spectators
- Display spectator count ("2 people watching")
- Add smooth real-time animations for spectators
---
### 2. Document Other Games
All arcade games currently support spectator mode. Consider documenting this in each game's README:
**Games with Spectator Mode**:
-`card-sorting` - Single-player puzzle with spectators
-`matching` - Multiplayer battle with spectators
-`memory-quiz` - Cooperative with spectators
-`complement-race` - Competitive with spectators
**Documentation to Add**:
- How spectator mode works in each game
- Example scenarios (family game night, classroom)
- Best practices for spectator experience
---
### 3. Add Spectator Mode Tests
Following ARCADE_ARCHITECTURE.md Spectator Mode section, add tests:
```typescript
describe('Card Sorting - Spectator Mode', () => {
it('should sync state to spectators when USER plays in a room', async () => {
// Setup: USER A and USER B in same room
// Action: USER A plays Card Sorting
// Assert: USER B (spectator) sees card placements in real-time
// Assert: USER B cannot place cards (no localPlayerId)
})
it('should prevent spectators from making moves', () => {
// Setup: USER A playing, USER B spectating
// Action: USER B attempts to place card
// Assert: Action blocked (localPlayerId check)
// Assert: Server rejects if somehow sent
})
it('should allow spectator to play after current player finishes', () => {
// Setup: USER A playing, USER B spectating
// Action: USER A finishes, USER B starts new game
// Assert: USER B becomes player
// Assert: USER A becomes spectator
})
})
```
---
### 4. Architecture Documentation
**✅ COMPLETED**: ARCADE_ARCHITECTURE.md has been updated with comprehensive spectator mode documentation:
- Added "SPECTATOR" to core terminology
- Documented three synchronization modes (Local, Room-Based with Spectator, Pure Multiplayer)
- Complete "Spectator Mode" section with:
- Implementation patterns
- UI/UX considerations
- Example scenarios (Family Game Night, Classroom)
- Server-side validation
- Testing requirements
- Migration path
**No further documentation needed** - Card Sorting follows the recommended pattern
---
## Compliance Checklist
Based on ARCADE_ARCHITECTURE.md Spectator Mode Pattern:
- [x]**Provider uses room-based sync to enable spectator mode**
- Calls `useRoomData()` and passes `roomId: roomData?.id`
- [x] ✅ Provider uses `useGameMode()` to get active players
- [x] ✅ Provider finds `localPlayerId` to distinguish player vs spectator
- [x] ✅ Game components correctly use PLAYER IDs (not USER IDs) for moves
- [x] ✅ Move actions check `localPlayerId` before sending
- Spectators without `localPlayerId` cannot make moves
- [x] ✅ Game supports multiple active PLAYERS from same USER
- Implementation allows it (finds first local player)
- [x] ✅ Inactive PLAYERS are never included in game sessions
- Uses `activePlayers` which filters to `isActive = true`
- [ ] ⚠️ **UI shows spectator indicator**
- Could be enhanced (see Recommendations #1)
- [ ] ⚠️ **UI disables controls for spectators**
- Could be enhanced (see Recommendations #1)
- [ ] ⚠️ Tests verify spectator mode
- No tests found (see Recommendations #3)
- [ ] ⚠️ Tests verify PLAYER ownership validation
- No tests found
- [x] ✅ Validator implements all required methods
- [x] ✅ Game registered with modular system
**Overall Compliance**: 9/13 ✅ (Core features complete, enhancements recommended)
---
## Summary
The Card Sorting Challenge game is **correctly implemented** with:
- ✅ Active players (only `isActive = true` players participate)
- ✅ Player ID vs User ID distinction
- ✅ Validator pattern
- ✅ Game registration
- ✅ Type safety
-**Spectator mode enabled** (room-based sync pattern)
**Architecture Pattern**: Room-Based with Spectator Mode (RECOMMENDED)
**CORRECT**: Room sync enables spectator mode as a first-class feature
The `roomId: roomData?.id` pattern is **intentional and correct**:
1. ✅ Enables spectator mode automatically
2. ✅ Room members can watch games in real-time
3. ✅ Creates social/collaborative experience
4. ✅ Spectators blocked from making moves (via `localPlayerId` check)
5. ✅ Follows ARCADE_ARCHITECTURE.md recommended pattern
**Recommended Enhancements** (not critical):
1. Add spectator UI indicators ("👀 Spectating...")
2. Disable controls visually for spectators
3. Add spectator mode tests
**Priority**: LOW (enhancements only - core implementation is correct)
---
## Next Steps (Optional Enhancements)
1.**Architecture documentation** - COMPLETED (ARCADE_ARCHITECTURE.md updated with spectator mode)
2. Add spectator UI indicators to GameComponent (banner, disabled controls)
3. Add spectator mode tests
4. Document spectator mode in other arcade games
5. Consider adding spectator count display ("2 watching")
**Note**: Card Sorting is production-ready as-is. Enhancements are for improved UX only.

File diff suppressed because it is too large Load Diff

View File

@@ -90,6 +90,37 @@ npm run check # Biome check (format + lint + organize imports)
**Remember: Always run `npm run pre-commit` before creating commits.**
## Styling Framework
**CRITICAL: This project uses Panda CSS, NOT Tailwind CSS.**
- All styling is done with Panda CSS (`@pandacss/dev`)
- Configuration: `/panda.config.ts`
- Generated system: `/styled-system/`
- Import styles using: `import { css } from '../../styled-system/css'`
- Token syntax: `color: 'blue.200'`, `borderColor: 'gray.300'`, etc.
**Common Mistakes to Avoid:**
- ❌ Don't reference "Tailwind" in code, comments, or documentation
- ❌ Don't use Tailwind utility classes (e.g., `className="bg-blue-500"`)
- ✅ Use Panda CSS `css()` function for all styling
- ✅ Use Panda's token system (defined in `panda.config.ts`)
**Color Tokens:**
```typescript
// Correct (Panda CSS)
css({
bg: 'blue.200',
borderColor: 'gray.300',
color: 'brand.600'
})
// Incorrect (Tailwind)
className="bg-blue-200 border-gray-300 text-brand-600"
```
See `.claude/GAME_THEMES.md` for standardized color theme usage in arcade games.
## Known Issues
### @soroban/abacus-react TypeScript Module Resolution

View File

@@ -0,0 +1,154 @@
# Game Theme Standardization
## Problem
Previously, each game manually specified `color`, `gradient`, and `borderColor` in their manifest. This led to:
- Inconsistent appearance across game cards
- No guidance on what colors/gradients to use
- Easy to choose saturated colors that don't match the pastel style
- Duplication and maintenance burden
## Solution
**Standard theme presets** in `/src/lib/arcade/game-themes.ts`
All games now use predefined color themes that ensure consistent, professional appearance.
## Usage
### 1. Import from the Game SDK
```typescript
import { defineGame, getGameTheme } from '@/lib/arcade/game-sdk'
import type { GameManifest } from '@/lib/arcade/game-sdk'
```
### 2. Use the Theme Spread Operator
```typescript
const manifest: GameManifest = {
name: 'my-game',
displayName: 'My Awesome Game',
icon: '🎮',
description: 'A fun game',
longDescription: 'More details...',
maxPlayers: 4,
difficulty: 'Intermediate',
chips: ['🎯 Feature 1', '⚡ Feature 2'],
...getGameTheme('blue'), // ← Just add this!
available: true,
}
```
That's it! The theme automatically provides:
- `color: 'blue'`
- `gradient: 'linear-gradient(135deg, #dbeafe, #bfdbfe)'`
- `borderColor: 'blue.200'`
## Available Themes
All themes use Panda CSS's 100-200 color range for soft pastel appearance:
| Theme | Color Range | Use Case |
|-------|-------------|----------|
| `blue` | blue-100 to blue-200 | Memory, puzzle games |
| `purple` | purple-100 to purple-200 | Strategic, battle games |
| `green` | green-100 to green-200 | Growth, achievement games |
| `teal` | teal-100 to teal-200 | Creative, sorting games |
| `indigo` | indigo-100 to indigo-200 | Deep thinking games |
| `pink` | pink-100 to pink-200 | Fun, casual games |
| `orange` | orange-100 to orange-200 | Speed, energy games |
| `yellow` | yellow-100 to yellow-200 | Bright, happy games |
| `red` | red-100 to red-200 | Competition, challenge |
| `gray` | gray-100 to gray-200 | Neutral games |
## Examples
### Current Games
```typescript
// Memory Lightning - blue theme
...getGameTheme('blue')
// Matching Pairs Battle - purple theme
...getGameTheme('purple')
// Card Sorting Challenge - teal theme
...getGameTheme('teal')
// Speed Complement Race - blue theme
...getGameTheme('blue')
```
## Benefits
**Consistency** - All games have the same professional pastel look
**Simple** - One line instead of three properties
**Maintainable** - Update all games by changing the theme definition
**Discoverable** - TypeScript autocomplete shows available themes
**No mistakes** - Can't accidentally use wrong color values
## Advanced Usage
If you need to inspect or customize a theme:
```typescript
import { GAME_THEMES } from '@/lib/arcade/game-sdk'
import type { GameTheme } from '@/lib/arcade/game-sdk'
// Access a specific theme
const blueTheme: GameTheme = GAME_THEMES.blue
// Use it
const manifest: GameManifest = {
// ... other fields
...blueTheme,
// Or customize:
color: blueTheme.color,
gradient: 'linear-gradient(135deg, #custom, #values)', // override
borderColor: blueTheme.borderColor,
}
```
## Adding New Themes
To add a new theme, edit `/src/lib/arcade/game-themes.ts`:
```typescript
export const GAME_THEMES = {
// ... existing themes
mycolor: {
color: 'mycolor',
gradient: 'linear-gradient(135deg, #lighter, #darker)', // Use Panda CSS 100-200 range
borderColor: 'mycolor.200',
},
} as const satisfies Record<string, GameTheme>
```
Then update the TypeScript type:
```typescript
export type GameThemeName = keyof typeof GAME_THEMES
```
## Migration Checklist
When creating a new game:
- [x] Import `getGameTheme` from `@/lib/arcade/game-sdk`
- [x] Use `...getGameTheme('theme-name')` in manifest
- [x] Remove manual `color`, `gradient`, `borderColor` properties
- [x] Choose a theme that matches your game's vibe
## Summary
**Old way** (error-prone, inconsistent):
```typescript
color: 'teal',
gradient: 'linear-gradient(135deg, #99f6e4, #5eead4)', // Too saturated!
borderColor: 'teal.200',
```
**New way** (simple, consistent):
```typescript
...getGameTheme('teal')
```

View File

@@ -97,7 +97,11 @@
"Bash(pnpm exec turbo build --filter=@soroban/web)",
"Bash(do gh run list --limit 1 --json conclusion,status,name,databaseId --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\")\"\"')",
"Bash(do gh run list --limit 1 --json conclusion,status,name --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\") - \\(.name)\"\"')",
"Bash(do gh run list --limit 1 --workflow=\"Build and Deploy\" --json conclusion,status --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\")\"\"')"
"Bash(do gh run list --limit 1 --workflow=\"Build and Deploy\" --json conclusion,status --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\")\"\"')",
"WebFetch(domain:abaci.one)",
"Bash(do gh run list --limit 1 --workflow=\"Build and Deploy\" --json conclusion,status,databaseId --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\") - Run ID: \\(.databaseId)\"\"')",
"Bash(node -e:*)",
"Bash(do gh run list --limit 1 --workflow=\"Build and Deploy\" --json conclusion,status,databaseId --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\") - Run \\(.databaseId)\"\"')"
],
"deny": [],
"ask": []

View File

@@ -158,7 +158,7 @@ export default function RoomDetailPage() {
const startGame = () => {
if (!room) return
// Navigate to the room game page
router.push('/arcade/room')
router.push('/arcade')
}
const joinRoom = async () => {

View File

@@ -1,128 +1,315 @@
'use client'
import { useEffect, useRef } from 'react'
import { useRouter } from 'next/navigation'
import { useRoomData, useSetRoomGame } from '@/hooks/useRoomData'
import { GAMES_CONFIG } from '@/components/GameSelector'
import type { GameType } from '@/components/GameSelector'
import { PageWithNav } from '@/components/PageWithNav'
import { css } from '../../../styled-system/css'
import { EnhancedChampionArena } from '../../components/EnhancedChampionArena'
import { FullscreenProvider, useFullscreen } from '../../contexts/FullscreenContext'
import { getAllGames, getGame, hasGame } from '@/lib/arcade/game-registry'
function ArcadeContent() {
const { setFullscreenElement } = useFullscreen()
const arcadeRef = useRef<HTMLDivElement>(null)
/**
* /arcade - Renders the game for the user's current room
* Since users can only be in one room at a time, this is a simple singular route
*
* Shows game selection when no game is set, then shows the game itself once selected.
* URL never changes - it's always /arcade regardless of selection, setup, or gameplay.
*
* Note: We show a friendly message with a link if no room exists to avoid navigation loops.
*
* Note: ModerationNotifications is handled by PageWithNav inside each game component,
* so we don't need to render it here.
*/
export default function RoomPage() {
const router = useRouter()
const { roomData, isLoading } = useRoomData()
const { mutate: setRoomGame } = useSetRoomGame()
useEffect(() => {
// Register this component's main div as the fullscreen element
if (arcadeRef.current) {
setFullscreenElement(arcadeRef.current)
}
}, [setFullscreenElement])
return (
<div
ref={arcadeRef}
className={css({
minHeight: 'calc(100vh - 80px)', // Account for mini nav height
background: 'linear-gradient(135deg, #0f0f23 0%, #1a1a3a 50%, #2d1b69 100%)',
position: 'relative',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
py: { base: '4', md: '6' },
})}
>
{/* Animated background elements */}
// Show loading state
if (isLoading) {
return (
<div
className={css({
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundImage: `
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.2) 0%, transparent 50%)
`,
animation: 'arcadeFloat 20s ease-in-out infinite',
})}
/>
{/* Main Champion Arena - takes remaining space */}
<div
className={css({
flex: 1,
style={{
display: 'flex',
px: { base: '2', md: '4' },
position: 'relative',
zIndex: 1,
minHeight: 0, // Important for flex children
})}
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
fontSize: '18px',
color: '#666',
}}
>
<EnhancedChampionArena
onConfigurePlayer={() => {}}
Loading room...
</div>
)
}
// Show error if no room (instead of redirecting)
if (!roomData) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
fontSize: '18px',
color: '#666',
gap: '1rem',
}}
>
<div>No active room found</div>
<a
href="/arcade"
style={{
color: '#3b82f6',
textDecoration: 'underline',
}}
>
Go to Champion Arena
</a>
</div>
)
}
// Show game selection if no game is set
if (!roomData.gameName) {
const handleGameSelect = (gameType: GameType) => {
console.log('[RoomPage] handleGameSelect called with gameType:', gameType)
// All games are now in the registry
if (hasGame(gameType)) {
const gameDef = getGame(gameType)
if (!gameDef?.manifest.available) {
console.log('[RoomPage] Registry game not available, blocking selection')
return
}
console.log('[RoomPage] Selecting registry game:', gameType)
setRoomGame({
roomId: roomData.id,
gameName: gameType,
})
return
}
console.log('[RoomPage] Unknown game type:', gameType)
}
return (
<PageWithNav
navTitle="Choose Game"
navEmoji="🎮"
emphasizePlayerSelection={true}
onExitSession={() => router.push('/arcade')}
>
<div
className={css({
width: '100%',
height: '100%',
background: 'rgba(255, 255, 255, 0.05)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255, 255, 255, 0.1)',
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.3)',
minHeight: '100vh',
background: 'linear-gradient(135deg, #0f0f23 0%, #1a1a3a 50%, #2d1b69 100%)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '4',
})}
/>
</div>
</div>
)
}
>
<h1
className={css({
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '8',
textAlign: 'center',
})}
>
Choose a Game
</h1>
function ArcadePageWithRedirect() {
return (
<PageWithNav navTitle="Champion Arena" navEmoji="🏟️" emphasizePlayerSelection={true}>
<ArcadeContent />
</PageWithNav>
)
}
<div
className={css({
display: 'grid',
gridTemplateColumns: { base: '1fr', md: 'repeat(2, 1fr)' },
gap: '4',
maxWidth: '800px',
width: '100%',
})}
>
{/* Legacy games */}
{Object.entries(GAMES_CONFIG).map(([gameType, config]: [string, any]) => {
const isAvailable = !('available' in config) || config.available !== false
return (
<button
key={gameType}
onClick={() => handleGameSelect(gameType as GameType)}
disabled={!isAvailable}
className={css({
background: config.gradient,
border: '2px solid',
borderColor: config.borderColor || 'blue.200',
borderRadius: '2xl',
padding: '6',
cursor: !isAvailable ? 'not-allowed' : 'pointer',
opacity: !isAvailable ? 0.5 : 1,
transition: 'all 0.3s ease',
_hover: !isAvailable
? {}
: {
transform: 'translateY(-4px) scale(1.02)',
boxShadow: '0 20px 40px rgba(59, 130, 246, 0.2)',
},
})}
>
<div
className={css({
fontSize: '4xl',
mb: '2',
})}
>
{config.icon}
</div>
<h3
className={css({
fontSize: 'xl',
fontWeight: 'bold',
color: 'gray.900',
mb: '2',
})}
>
{config.name}
</h3>
<p
className={css({
fontSize: 'sm',
color: 'gray.600',
})}
>
{config.description}
</p>
</button>
)
})}
export default function ArcadePage() {
return (
<FullscreenProvider>
<ArcadePageWithRedirect />
</FullscreenProvider>
)
}
// Arcade-specific animations
const arcadeAnimations = `
@keyframes arcadeFloat {
0%, 100% {
transform: translateY(0px) rotate(0deg);
opacity: 0.7;
{/* Registry games */}
{getAllGames().map((gameDef) => {
const isAvailable = gameDef.manifest.available
return (
<button
key={gameDef.manifest.name}
onClick={() => handleGameSelect(gameDef.manifest.name)}
disabled={!isAvailable}
className={css({
background: gameDef.manifest.gradient,
border: '2px solid',
borderColor: gameDef.manifest.borderColor,
borderRadius: '2xl',
padding: '6',
cursor: !isAvailable ? 'not-allowed' : 'pointer',
opacity: !isAvailable ? 0.5 : 1,
transition: 'all 0.3s ease',
_hover: !isAvailable
? {}
: {
transform: 'translateY(-4px) scale(1.02)',
boxShadow: '0 20px 40px rgba(59, 130, 246, 0.2)',
},
})}
>
<div
className={css({
fontSize: '4xl',
mb: '2',
})}
>
{gameDef.manifest.icon}
</div>
<h3
className={css({
fontSize: 'xl',
fontWeight: 'bold',
color: 'gray.900',
mb: '2',
})}
>
{gameDef.manifest.displayName}
</h3>
<p
className={css({
fontSize: 'sm',
color: 'gray.600',
})}
>
{gameDef.manifest.description}
</p>
</button>
)
})}
</div>
</div>
</PageWithNav>
)
}
33% {
transform: translateY(-20px) rotate(1deg);
opacity: 1;
// Check if this is a registry game first
if (hasGame(roomData.gameName)) {
const gameDef = getGame(roomData.gameName)
if (!gameDef) {
return (
<PageWithNav
navTitle="Game Not Found"
navEmoji="⚠️"
emphasizePlayerSelection={true}
onExitSession={() => router.push('/arcade')}
>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
fontSize: '18px',
color: '#666',
}}
>
Game "{roomData.gameName}" not found in registry
</div>
</PageWithNav>
)
}
// Render registry game dynamically
const { Provider, GameComponent } = gameDef
return (
<Provider>
<GameComponent />
</Provider>
)
}
66% {
transform: translateY(-10px) rotate(-1deg);
opacity: 0.8;
// Render legacy games based on room's gameName
switch (roomData.gameName) {
// TODO: Add other legacy games (complement-race, etc.) once migrated
default:
return (
<PageWithNav
navTitle="Game Not Available"
navEmoji="⚠️"
emphasizePlayerSelection={true}
onExitSession={() => router.push('/arcade')}
>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
fontSize: '18px',
color: '#666',
}}
>
Game "{roomData.gameName}" not yet supported
</div>
</PageWithNav>
)
}
}
@keyframes arcadePulse {
0%, 100% {
box-shadow: 0 0 20px rgba(96, 165, 250, 0.3);
}
50% {
box-shadow: 0 0 40px rgba(96, 165, 250, 0.6);
}
}
`
// Inject arcade animations
if (typeof document !== 'undefined' && !document.getElementById('arcade-animations')) {
const style = document.createElement('style')
style.id = 'arcade-animations'
style.textContent = arcadeAnimations
document.head.appendChild(style)
}

View File

@@ -1,358 +0,0 @@
'use client'
import { useRouter } from 'next/navigation'
import { useRoomData, useSetRoomGame } from '@/hooks/useRoomData'
import { GAMES_CONFIG } from '@/components/GameSelector'
import type { GameType } from '@/components/GameSelector'
import { PageWithNav } from '@/components/PageWithNav'
import { css } from '../../../../styled-system/css'
import { getAllGames, getGame, hasGame } from '@/lib/arcade/game-registry'
// Map GameType keys to internal game names
// Note: "battle-arena" removed - now handled by game registry as "matching"
const GAME_TYPE_TO_NAME: Record<GameType, string> = {
'complement-race': 'complement-race',
'master-organizer': 'master-organizer',
}
/**
* /arcade/room - Renders the game for the user's current room
* Since users can only be in one room at a time, this is a simple singular route
*
* Shows game selection when no game is set, then shows the game itself once selected.
* URL never changes - it's always /arcade/room regardless of selection, setup, or gameplay.
*
* Note: We don't redirect to /arcade if no room exists to avoid navigation loops.
* Instead, we show a friendly message with a link back to the Champion Arena.
*
* Note: ModerationNotifications is handled by PageWithNav inside each game component,
* so we don't need to render it here.
*/
export default function RoomPage() {
const router = useRouter()
const { roomData, isLoading } = useRoomData()
const { mutate: setRoomGame } = useSetRoomGame()
// Show loading state
if (isLoading) {
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
fontSize: '18px',
color: '#666',
}}
>
Loading room...
</div>
)
}
// Show error if no room (instead of redirecting)
if (!roomData) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
fontSize: '18px',
color: '#666',
gap: '1rem',
}}
>
<div>No active room found</div>
<a
href="/arcade"
style={{
color: '#3b82f6',
textDecoration: 'underline',
}}
>
Go to Champion Arena
</a>
</div>
)
}
// Show game selection if no game is set
if (!roomData.gameName) {
const handleGameSelect = (gameType: GameType) => {
console.log('[RoomPage] handleGameSelect called with gameType:', gameType)
// Check if it's a registry game first
if (hasGame(gameType)) {
const gameDef = getGame(gameType)
if (!gameDef?.manifest.available) {
console.log('[RoomPage] Registry game not available, blocking selection')
return
}
console.log('[RoomPage] Selecting registry game:', gameType)
setRoomGame({
roomId: roomData.id,
gameName: gameType, // Use the game name directly for registry games
})
return
}
// Legacy game handling
const gameConfig = GAMES_CONFIG[gameType as keyof typeof GAMES_CONFIG]
if (!gameConfig) {
console.log('[RoomPage] Unknown game type:', gameType)
return
}
console.log('[RoomPage] Game config:', {
name: gameConfig.name,
available: 'available' in gameConfig ? gameConfig.available : true,
})
if ('available' in gameConfig && gameConfig.available === false) {
console.log('[RoomPage] Game not available, blocking selection')
return // Don't allow selecting unavailable games
}
// Map GameType to internal game name
const internalGameName = GAME_TYPE_TO_NAME[gameType]
console.log('[RoomPage] Mapping:', {
gameType,
internalGameName,
mappingExists: !!internalGameName,
})
console.log('[RoomPage] Calling setRoomGame with:', {
roomId: roomData.id,
gameName: internalGameName,
preservingGameConfig: true,
})
// Don't pass gameConfig - we want to preserve existing settings for all games
setRoomGame({
roomId: roomData.id,
gameName: internalGameName,
})
}
return (
<PageWithNav
navTitle="Choose Game"
navEmoji="🎮"
emphasizePlayerSelection={true}
onExitSession={() => router.push('/arcade')}
>
<div
className={css({
minHeight: '100vh',
background: 'linear-gradient(135deg, #0f0f23 0%, #1a1a3a 50%, #2d1b69 100%)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '4',
})}
>
<h1
className={css({
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '8',
textAlign: 'center',
})}
>
Choose a Game
</h1>
<div
className={css({
display: 'grid',
gridTemplateColumns: { base: '1fr', md: 'repeat(2, 1fr)' },
gap: '4',
maxWidth: '800px',
width: '100%',
})}
>
{/* Legacy games */}
{Object.entries(GAMES_CONFIG).map(([gameType, config]) => {
const isAvailable = !('available' in config) || config.available !== false
return (
<button
key={gameType}
onClick={() => handleGameSelect(gameType as GameType)}
disabled={!isAvailable}
className={css({
background: config.gradient,
border: '2px solid',
borderColor: config.borderColor || 'blue.200',
borderRadius: '2xl',
padding: '6',
cursor: !isAvailable ? 'not-allowed' : 'pointer',
opacity: !isAvailable ? 0.5 : 1,
transition: 'all 0.3s ease',
_hover: !isAvailable
? {}
: {
transform: 'translateY(-4px) scale(1.02)',
boxShadow: '0 20px 40px rgba(59, 130, 246, 0.2)',
},
})}
>
<div
className={css({
fontSize: '4xl',
mb: '2',
})}
>
{config.icon}
</div>
<h3
className={css({
fontSize: 'xl',
fontWeight: 'bold',
color: 'gray.900',
mb: '2',
})}
>
{config.name}
</h3>
<p
className={css({
fontSize: 'sm',
color: 'gray.600',
})}
>
{config.description}
</p>
</button>
)
})}
{/* Registry games */}
{getAllGames().map((gameDef) => {
const isAvailable = gameDef.manifest.available
return (
<button
key={gameDef.manifest.name}
onClick={() => handleGameSelect(gameDef.manifest.name)}
disabled={!isAvailable}
className={css({
background: gameDef.manifest.gradient,
border: '2px solid',
borderColor: gameDef.manifest.borderColor,
borderRadius: '2xl',
padding: '6',
cursor: !isAvailable ? 'not-allowed' : 'pointer',
opacity: !isAvailable ? 0.5 : 1,
transition: 'all 0.3s ease',
_hover: !isAvailable
? {}
: {
transform: 'translateY(-4px) scale(1.02)',
boxShadow: '0 20px 40px rgba(59, 130, 246, 0.2)',
},
})}
>
<div
className={css({
fontSize: '4xl',
mb: '2',
})}
>
{gameDef.manifest.icon}
</div>
<h3
className={css({
fontSize: 'xl',
fontWeight: 'bold',
color: 'gray.900',
mb: '2',
})}
>
{gameDef.manifest.displayName}
</h3>
<p
className={css({
fontSize: 'sm',
color: 'gray.600',
})}
>
{gameDef.manifest.description}
</p>
</button>
)
})}
</div>
</div>
</PageWithNav>
)
}
// Check if this is a registry game first
if (hasGame(roomData.gameName)) {
const gameDef = getGame(roomData.gameName)
if (!gameDef) {
return (
<PageWithNav
navTitle="Game Not Found"
navEmoji="⚠️"
emphasizePlayerSelection={true}
onExitSession={() => router.push('/arcade')}
>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
fontSize: '18px',
color: '#666',
}}
>
Game "{roomData.gameName}" not found in registry
</div>
</PageWithNav>
)
}
// Render registry game dynamically
const { Provider, GameComponent } = gameDef
return (
<Provider>
<GameComponent />
</Provider>
)
}
// Render legacy games based on room's gameName
switch (roomData.gameName) {
// TODO: Add other legacy games (complement-race, etc.) once migrated
default:
return (
<PageWithNav
navTitle="Game Not Available"
navEmoji="⚠️"
emphasizePlayerSelection={true}
onExitSession={() => router.push('/arcade')}
>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
fontSize: '18px',
color: '#666',
}}
>
Game "{roomData.gameName}" not yet supported
</div>
</PageWithNav>
)
}
}

View File

@@ -231,7 +231,7 @@ export default function JoinRoomPage({ params }: { params: { code: string } }) {
password: roomPassword,
})
// Navigate to the game
router.push('/arcade/room')
router.push('/arcade')
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to join room')
setIsJoining(false)
@@ -261,7 +261,7 @@ export default function JoinRoomPage({ params }: { params: { code: string } }) {
// If user is already in this exact room, just navigate to game
if (roomData && roomData.id === room.id) {
router.push('/arcade/room')
router.push('/arcade')
return
}
@@ -313,7 +313,7 @@ export default function JoinRoomPage({ params }: { params: { code: string } }) {
}
const handleCancel = () => {
router.push('/arcade/room') // Stay in current room
router.push('/arcade') // Stay in current room
}
const handlePasswordSubmit = () => {

View File

@@ -1,185 +1,465 @@
'use client'
import { useState } from 'react'
import Link from 'next/link'
import { PageWithNav } from '@/components/PageWithNav'
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
import { css } from '../../styled-system/css'
import { container, hstack, stack } from '../../styled-system/patterns'
import { container, grid, hstack, stack } from '../../styled-system/patterns'
export default function HomePage() {
const [abacusValue, setAbacusValue] = useState(1234567)
const appConfig = useAbacusConfig()
return (
<PageWithNav navTitle="Soroban Flashcards" navEmoji="🧮">
<div
className={css({
minHeight: '100vh',
bg: 'gradient-to-br from-brand.50 to-brand.100',
})}
>
{/* Hero Section */}
<main className={container({ maxW: '6xl', px: '4' })}>
<PageWithNav navTitle="Soroban Mastery Platform" navEmoji="🧮">
<div className={css({ bg: 'gray.900', minHeight: '100vh' })}>
{/* Hero with Large Abacus */}
<div
className={css({
background:
'linear-gradient(135deg, rgba(17, 24, 39, 1) 0%, rgba(88, 28, 135, 0.3) 50%, rgba(17, 24, 39, 1) 100%)',
color: 'white',
py: { base: '12', md: '20' },
position: 'relative',
overflow: 'hidden',
})}
>
<div
className={stack({
gap: '12',
py: '16',
align: 'center',
textAlign: 'center',
className={css({
position: 'absolute',
inset: 0,
opacity: 0.1,
backgroundImage:
'radial-gradient(circle at 2px 2px, rgba(255, 255, 255, 0.15) 1px, transparent 0)',
backgroundSize: '40px 40px',
})}
>
{/* Hero Content */}
<div className={stack({ gap: '6', maxW: '4xl' })}>
/>
<div className={container({ maxW: '6xl', px: '4', position: 'relative' })}>
<div className={css({ textAlign: 'center', maxW: '5xl', mx: 'auto' })}>
<h1
className={css({
fontSize: { base: '4xl', md: '6xl' },
fontSize: { base: '3xl', md: '5xl', lg: '6xl' },
fontWeight: 'bold',
color: 'gray.900',
mb: '6',
lineHeight: 'tight',
background: 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 50%, #fbbf24 100%)',
backgroundClip: 'text',
color: 'transparent',
})}
>
Beautiful Soroban <span className={css({ color: 'brand.600' })}>Flashcards</span>
Master the Soroban
</h1>
{/* Large Featured Abacus */}
<div
className={css({
display: 'flex',
justifyContent: 'center',
my: '10',
p: '8',
bg: 'white',
borderRadius: '2xl',
border: '2px solid',
borderColor: 'purple.500/30',
boxShadow: '0 25px 50px -12px rgba(139, 92, 246, 0.25)',
color: '#1f2937',
})}
>
<AbacusReact
value={abacusValue}
columns={7}
beadShape={appConfig.beadShape}
colorScheme={appConfig.colorScheme}
hideInactiveBeads={appConfig.hideInactiveBeads}
interactive={true}
animated={true}
soundEnabled={true}
soundVolume={0.4}
scaleFactor={2.2}
showNumbers={true}
onValueChange={(newValue: number) => setAbacusValue(newValue)}
/>
</div>
<p
className={css({
fontSize: { base: 'lg', md: 'xl' },
color: 'gray.600',
maxW: '2xl',
color: 'gray.300',
mb: '8',
maxW: '3xl',
mx: 'auto',
})}
>
Create stunning, educational flashcards with authentic Japanese abacus
representations. Perfect for teachers, students, and mental math enthusiasts.
Interactive tutorials, multiplayer games, and beautiful flashcardsyour complete
soroban learning ecosystem
</p>
<div className={hstack({ gap: '4', justify: 'center', mt: '8' })}>
<div className={hstack({ gap: '4', justify: 'center', flexWrap: 'wrap' })}>
<Link
href="/create"
href="/games"
className={css({
px: '8',
py: '4',
bg: 'brand.600',
color: 'white',
bg: 'linear-gradient(135deg, #fbbf24, #f59e0b)',
color: 'gray.900',
fontWeight: 'bold',
fontSize: 'lg',
fontWeight: 'semibold',
rounded: 'xl',
shadow: 'card',
transition: 'all',
shadow: '0 10px 40px rgba(251, 191, 36, 0.3)',
_hover: {
bg: 'brand.700',
transform: 'translateY(-2px)',
shadow: 'modal',
shadow: '0 20px 50px rgba(251, 191, 36, 0.4)',
},
transition: 'all 0.3s ease',
})}
>
Start Creating
🎮 Play Games
</Link>
<Link
href="/guide"
className={css({
px: '8',
py: '4',
bg: 'white',
color: 'brand.700',
bg: 'rgba(139, 92, 246, 0.2)',
color: 'white',
fontWeight: 'bold',
fontSize: 'lg',
fontWeight: 'semibold',
rounded: 'xl',
shadow: 'card',
border: '2px solid',
borderColor: 'brand.200',
transition: 'all',
borderColor: 'purple.500',
_hover: {
borderColor: 'brand.400',
bg: 'rgba(139, 92, 246, 0.3)',
transform: 'translateY(-2px)',
},
transition: 'all 0.3s ease',
})}
>
📚 Learn Soroban
📚 Learn
</Link>
<Link
href="/create"
className={css({
px: '8',
py: '4',
bg: 'rgba(139, 92, 246, 0.2)',
color: 'white',
fontWeight: 'bold',
fontSize: 'lg',
rounded: 'xl',
border: '2px solid',
borderColor: 'purple.500',
_hover: {
bg: 'rgba(139, 92, 246, 0.3)',
transform: 'translateY(-2px)',
},
transition: 'all 0.3s ease',
})}
>
🎨 Create
</Link>
</div>
</div>
</div>
</div>
{/* Features Grid */}
<div
className={css({
display: 'grid',
gridTemplateColumns: { base: '1', md: '3' },
gap: '8',
mt: '16',
w: 'full',
})}
>
<FeatureCard
icon="🎨"
title="Beautiful Design"
description="Vector graphics, color schemes, authentic bead positioning"
{/* Main content container */}
<div className={container({ maxW: '7xl', px: '4', py: '12' })}>
{/* Arcade Games Section */}
<section className={stack({ gap: '6', mt: '16' })}>
<div className={hstack({ justify: 'space-between', alignItems: 'center' })}>
<div>
<h2
className={css({
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '2',
})}
>
🕹 Multiplayer Arcade
</h2>
<p className={css({ color: 'gray.400', fontSize: 'md' })}>
Compete with friends in real-time soroban games
</p>
</div>
<Link
href="/games"
className={css({
fontSize: 'md',
color: 'yellow.400',
fontWeight: 'semibold',
_hover: { color: 'yellow.300' },
display: { base: 'none', md: 'block' },
})}
>
View All
</Link>
</div>
<div className={grid({ columns: { base: 1, sm: 2, lg: 4 }, gap: '5' })}>
<GameCard
icon="🧠"
title="Memory Lightning"
description="Memorize soroban numbers"
players="1-8 players"
tags={['Co-op', 'Competitive']}
gradient="linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
href="/games"
/>
<FeatureCard
icon=""
title="Instant Generation"
description="Create PDFs, interactive HTML, PNGs, and SVGs in seconds"
<GameCard
icon="⚔️"
title="Matching Pairs"
description="Turn-based card battles"
players="1-4 players"
tags={['Pattern Recognition']}
gradient="linear-gradient(135deg, #f093fb 0%, #f5576c 100%)"
href="/games"
/>
<FeatureCard
icon="🎯"
title="Educational Focus"
description="Perfect for teachers, students, and soroban enthusiasts"
<GameCard
icon="🏁"
title="Speed Race"
description="Race AI with complements"
players="1-4 players + AI"
tags={['Practice', 'Sprint', 'Survival']}
gradient="linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)"
href="/games"
/>
<GameCard
icon="🔢"
title="Card Sorting"
description="Arrange cards visually"
players="Solo challenge"
tags={['Visual Literacy']}
gradient="linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)"
href="/games"
/>
</div>
</section>
{/* Interactive Learning & Flashcard Creator */}
<div className={grid({ columns: { base: 1, lg: 2 }, gap: '8', mt: '16' })}>
<FeaturePanel
icon="📚"
title="Interactive Learning"
description="Master soroban through hands-on guided tutorials"
features={[
'Visual tutorials on reading bead positions',
'Step-by-step arithmetic operations',
'Interactive exercises with instant feedback',
]}
ctaText="Start Learning →"
ctaHref="/guide"
accentColor="purple"
/>
<FeaturePanel
icon="🎨"
title="Flashcard Creator"
description="Design beautiful soroban flashcards for any purpose"
features={[
'Multiple export formats: PDF, PNG, SVG, HTML',
'Custom bead shapes, colors, and layouts',
'All paper sizes: A3, A4, A5, US Letter',
]}
ctaText="Create Flashcards →"
ctaHref="/create"
accentColor="blue"
/>
</div>
</main>
</div>
</div>
</PageWithNav>
)
}
function FeatureCard({
icon,
function ColorSchemeCard({
title,
description,
colorScheme,
value,
beadShape,
}: {
icon: string
title: string
description: string
colorScheme: 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating'
value: number
beadShape: 'diamond' | 'circle' | 'square'
}) {
return (
<div
className={css({
p: '8',
bg: 'white',
rounded: '2xl',
shadow: 'card',
textAlign: 'center',
transition: 'all',
bg: 'rgba(0, 0, 0, 0.4)',
rounded: 'xl',
p: '6',
border: '2px solid',
borderColor: 'gray.700',
transition: 'all 0.3s ease',
_hover: {
borderColor: 'purple.500',
transform: 'translateY(-4px)',
shadow: 'modal',
boxShadow: '0 20px 40px rgba(139, 92, 246, 0.2)',
},
})}
>
<div
className={css({
fontSize: '4xl',
mb: '4',
})}
>
{icon}
</div>
<h3
className={css({
fontSize: 'xl',
fontSize: 'lg',
fontWeight: 'bold',
color: 'gray.900',
mb: '3',
color: 'white',
mb: '2',
})}
>
{title}
</h3>
<p
<p className={css({ fontSize: 'sm', color: 'gray.400', mb: '4' })}>{description}</p>
<div
className={css({
color: 'gray.600',
lineHeight: 'relaxed',
display: 'flex',
justifyContent: 'center',
p: '4',
bg: 'rgba(255, 255, 255, 0.05)',
rounded: 'lg',
})}
>
{description}
</p>
<AbacusReact
value={value}
columns={3}
beadShape={beadShape}
colorScheme={colorScheme}
hideInactiveBeads={false}
/>
</div>
</div>
)
}
function GameCard({
icon,
title,
description,
players,
tags,
gradient,
href,
}: {
icon: string
title: string
description: string
players: string
tags: string[]
gradient: string
href: string
}) {
return (
<Link href={href}>
<div
className={css({
background: gradient,
rounded: 'xl',
p: '6',
shadow: 'lg',
transition: 'all 0.3s ease',
cursor: 'pointer',
_hover: {
transform: 'translateY(-6px) scale(1.02)',
shadow: '0 25px 50px rgba(0, 0, 0, 0.3)',
},
})}
>
<div className={css({ fontSize: '3xl', mb: '3' })}>{icon}</div>
<h3 className={css({ fontSize: 'lg', fontWeight: 'bold', color: 'white', mb: '2' })}>
{title}
</h3>
<p className={css({ fontSize: 'sm', color: 'rgba(255, 255, 255, 0.9)', mb: '2' })}>
{description}
</p>
<p className={css({ fontSize: 'xs', color: 'rgba(255, 255, 255, 0.7)', mb: '3' })}>
{players}
</p>
<div className={hstack({ gap: '2', flexWrap: 'wrap' })}>
{tags.map((tag) => (
<span
key={tag}
className={css({
fontSize: 'xs',
px: '2',
py: '1',
bg: 'rgba(255, 255, 255, 0.2)',
color: 'white',
rounded: 'full',
fontWeight: 'semibold',
})}
>
{tag}
</span>
))}
</div>
</div>
</Link>
)
}
function FeaturePanel({
icon,
title,
description,
features,
ctaText,
ctaHref,
accentColor,
}: {
icon: string
title: string
description: string
features: string[]
ctaText: string
ctaHref: string
accentColor: 'purple' | 'blue'
}) {
const borderColor = accentColor === 'purple' ? 'purple.500/30' : 'blue.500/30'
const bgColor = accentColor === 'purple' ? 'purple.500/10' : 'blue.500/10'
const hoverBg = accentColor === 'purple' ? 'purple.500/20' : 'blue.500/20'
return (
<div
className={css({
bg: 'rgba(0, 0, 0, 0.4)',
rounded: 'xl',
p: '8',
border: '2px solid',
borderColor,
})}
>
<div className={hstack({ gap: '3', mb: '4' })}>
<span className={css({ fontSize: '3xl' })}>{icon}</span>
<h2 className={css({ fontSize: '2xl', fontWeight: 'bold', color: 'white' })}>{title}</h2>
</div>
<p className={css({ fontSize: 'md', color: 'gray.300', mb: '6' })}>{description}</p>
<div className={stack({ gap: '3', mb: '6' })}>
{features.map((feature, i) => (
<div key={i} className={hstack({ gap: '3' })}>
<span className={css({ color: 'yellow.400', fontSize: 'lg' })}></span>
<span className={css({ color: 'gray.300', fontSize: 'sm' })}>{feature}</span>
</div>
))}
</div>
<Link
href={ctaHref}
className={css({
display: 'block',
textAlign: 'center',
py: '3',
px: '6',
bg: bgColor,
color: 'white',
fontWeight: 'bold',
rounded: 'lg',
border: '2px solid',
borderColor,
_hover: { bg: hoverBg },
transition: 'all 0.2s ease',
})}
>
{ctaText}
</Link>
</div>
)
}

View File

@@ -1,13 +1,6 @@
'use client'
import {
type ReactNode,
useCallback,
useMemo,
createContext,
useContext,
useState,
} from 'react'
import { type ReactNode, useCallback, useMemo, createContext, useContext, useState } from 'react'
import { useArcadeSession } from '@/hooks/useArcadeSession'
import { useRoomData, useUpdateGameConfig } from '@/hooks/useRoomData'
import { useViewerId } from '@/hooks/useViewerId'
@@ -40,6 +33,9 @@ interface CardSortingContextValue {
// UI state
selectedCardId: string | null
selectCard: (cardId: string | null) => void
// Spectator mode
localPlayerId: string | undefined
isSpectating: boolean
}
// Create context
@@ -553,6 +549,9 @@ export function CardSortingProvider({ children }: { children: ReactNode }) {
// UI state
selectedCardId,
selectCard: setSelectedCardId,
// Spectator mode
localPlayerId,
isSpectating: !localPlayerId,
}
return <CardSortingContext.Provider value={contextValue}>{children}</CardSortingContext.Provider>

View File

@@ -13,7 +13,7 @@ import { ResultsPhase } from './ResultsPhase'
export function GameComponent() {
const router = useRouter()
const { state, exitSession, startGame, goToSetup } = useCardSorting()
const { state, exitSession, startGame, goToSetup, isSpectating } = useCardSorting()
const { setFullscreenElement } = useFullscreen()
const gameRef = useRef<HTMLDivElement>(null)
@@ -57,6 +57,34 @@ export function GameComponent() {
overflow: 'auto',
})}
>
{/* Spectator Mode Banner */}
{isSpectating && state.gamePhase !== 'setup' && (
<div
className={css({
width: '100%',
maxWidth: '1200px',
background: 'linear-gradient(135deg, #fef3c7, #fde68a)',
borderRadius: { base: '8px', md: '12px' },
padding: { base: '12px', md: '16px' },
marginBottom: { base: '12px', md: '16px' },
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px',
fontSize: { base: '14px', sm: '16px', md: '18px' },
fontWeight: '600',
color: '#92400e',
textAlign: 'center',
})}
>
<span role="img" aria-label="watching">
👀
</span>
<span>Spectating {state.playerMetadata?.name || 'player'}'s game</span>
</div>
)}
<main
className={css({
width: '100%',

View File

@@ -18,6 +18,7 @@ export function PlayingPhase() {
canCheckSolution,
placedCount,
elapsedTime,
isSpectating,
} = useCardSorting()
// Status message (mimics Python updateSortingStatus)
@@ -64,6 +65,7 @@ export function PlayingPhase() {
}
const handleCardClick = (cardId: string) => {
if (isSpectating) return // Spectators cannot interact
if (selectedCardId === cardId) {
selectCard(null) // Deselect
} else {
@@ -72,6 +74,7 @@ export function PlayingPhase() {
}
const handleSlotClick = (position: number) => {
if (isSpectating) return // Spectators cannot interact
if (!selectedCardId) {
// No card selected - if slot has a card, move it back and auto-select
if (state.placedCards[position]) {
@@ -89,6 +92,7 @@ export function PlayingPhase() {
}
const handleInsertClick = (insertPosition: number) => {
if (isSpectating) return // Spectators cannot interact
if (!selectedCardId) {
setStatusMessage('Please select a card first, then click where to insert it.')
return
@@ -181,17 +185,19 @@ export function PlayingPhase() {
<button
type="button"
onClick={revealNumbers}
disabled={isSpectating}
className={css({
padding: '0.5rem 1rem',
borderRadius: '0.375rem',
background: 'orange.500',
background: isSpectating ? 'gray.300' : 'orange.500',
color: 'white',
fontSize: 'sm',
fontWeight: '600',
border: 'none',
cursor: 'pointer',
cursor: isSpectating ? 'not-allowed' : 'pointer',
opacity: isSpectating ? 0.5 : 1,
_hover: {
background: 'orange.600',
background: isSpectating ? 'gray.300' : 'orange.600',
},
})}
>
@@ -201,19 +207,19 @@ export function PlayingPhase() {
<button
type="button"
onClick={checkSolution}
disabled={!canCheckSolution}
disabled={!canCheckSolution || isSpectating}
className={css({
padding: '0.5rem 1rem',
borderRadius: '0.375rem',
background: canCheckSolution ? 'teal.600' : 'gray.300',
background: canCheckSolution && !isSpectating ? 'teal.600' : 'gray.300',
color: 'white',
fontSize: 'sm',
fontWeight: '600',
border: 'none',
cursor: canCheckSolution ? 'pointer' : 'not-allowed',
opacity: canCheckSolution ? 1 : 0.6,
cursor: canCheckSolution && !isSpectating ? 'pointer' : 'not-allowed',
opacity: canCheckSolution && !isSpectating ? 1 : 0.5,
_hover: {
background: canCheckSolution ? 'teal.700' : 'gray.300',
background: canCheckSolution && !isSpectating ? 'teal.700' : 'gray.300',
},
})}
>
@@ -222,17 +228,19 @@ export function PlayingPhase() {
<button
type="button"
onClick={goToSetup}
disabled={isSpectating}
className={css({
padding: '0.5rem 1rem',
borderRadius: '0.375rem',
background: 'gray.600',
background: isSpectating ? 'gray.400' : 'gray.600',
color: 'white',
fontSize: 'sm',
fontWeight: '600',
border: 'none',
cursor: 'pointer',
cursor: isSpectating ? 'not-allowed' : 'pointer',
opacity: isSpectating ? 0.5 : 1,
_hover: {
background: 'gray.700',
background: isSpectating ? 'gray.400' : 'gray.700',
},
})}
>
@@ -264,13 +272,15 @@ export function PlayingPhase() {
</h3>
<div
className={css({
display: 'grid',
gridTemplateColumns: {
base: '1',
sm: '2',
md: '3',
},
gap: '0.75rem',
display: 'flex',
flexWrap: 'wrap',
gap: '10px',
justifyContent: 'center',
padding: '15px',
background: 'rgba(255,255,255,0.5)',
borderRadius: '8px',
minHeight: '120px',
border: '2px dashed #2c5f76',
})}
>
{state.availableCards.map((card) => (
@@ -278,39 +288,68 @@ export function PlayingPhase() {
key={card.id}
onClick={() => handleCardClick(card.id)}
className={css({
padding: '0.5rem',
width: '140px',
height: '140px',
padding: '8px',
border: '2px solid',
borderColor: selectedCardId === card.id ? 'blue.500' : 'gray.300',
borderRadius: '0.5rem',
background: selectedCardId === card.id ? 'blue.50' : 'white',
cursor: 'pointer',
transition: 'all 0.2s',
transform: selectedCardId === card.id ? 'scale(1.05)' : 'scale(1)',
_hover: {
transform: 'scale(1.05)',
borderColor: 'blue.500',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
},
borderColor: selectedCardId === card.id ? '#1976d2' : 'transparent',
borderRadius: '8px',
background: selectedCardId === card.id ? '#e3f2fd' : 'white',
cursor: isSpectating ? 'not-allowed' : 'pointer',
opacity: isSpectating ? 0.5 : 1,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
position: 'relative',
userSelect: 'none',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 2px 6px rgba(0,0,0,0.1)',
_hover: isSpectating
? {}
: {
transform: 'translateY(-5px)',
boxShadow: '0 8px 16px rgba(0,0,0,0.2)',
borderColor: '#2c5f76',
},
})}
style={
selectedCardId === card.id
? {
transform: 'scale(1.1)',
boxShadow: '0 6px 20px rgba(25, 118, 210, 0.3)',
}
: undefined
}
>
<div
dangerouslySetInnerHTML={{ __html: card.svgContent }}
className={css({
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
'& svg': {
width: '100%',
height: 'auto',
height: '100%',
display: 'block',
},
})}
/>
{state.numbersRevealed && (
<div
className={css({
textAlign: 'center',
marginTop: '0.5rem',
fontSize: 'lg',
position: 'absolute',
top: '5px',
right: '5px',
background: '#ffc107',
color: '#333',
borderRadius: '4px',
padding: '2px 8px',
fontSize: '14px',
fontWeight: 'bold',
color: 'gray.700',
})}
>
{card.number}
@@ -336,35 +375,43 @@ export function PlayingPhase() {
<div
className={css({
display: 'flex',
flexDirection: 'column',
gap: '0.25rem',
flexWrap: 'wrap',
gap: '8px',
justifyContent: 'center',
alignItems: 'center',
padding: '15px',
background: 'rgba(255,255,255,0.7)',
borderRadius: '8px',
border: '2px dashed #2c5f76',
})}
>
{/* Insert button before first position */}
<button
type="button"
onClick={() => handleInsertClick(0)}
disabled={!selectedCardId}
disabled={!selectedCardId || isSpectating}
className={css({
width: '32px',
height: '50px',
background: selectedCardId ? '#1976d2' : '#2c5f76',
background: selectedCardId && !isSpectating ? '#1976d2' : '#2c5f76',
color: 'white',
border: 'none',
borderRadius: '20px',
fontSize: '24px',
fontWeight: 'bold',
cursor: selectedCardId ? 'pointer' : 'default',
opacity: selectedCardId ? 1 : 0.3,
cursor: selectedCardId && !isSpectating ? 'pointer' : 'not-allowed',
opacity: selectedCardId && !isSpectating ? 1 : 0.3,
transition: 'all 0.2s',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
_hover: {
opacity: 1,
background: '#1976d2',
transform: selectedCardId ? 'scale(1.1)' : 'none',
},
_hover: isSpectating
? {}
: {
opacity: 1,
background: '#1976d2',
transform: selectedCardId ? 'scale(1.1)' : 'none',
},
})}
>
+
@@ -376,17 +423,19 @@ export function PlayingPhase() {
const isEmpty = card === null
return (
<div key={index}>
<>
{/* Position slot */}
<div
key={`slot-${index}`}
onClick={() => handleSlotClick(index)}
className={css({
width: '90px',
height: '110px',
width: '140px',
height: '160px',
padding: '0.5rem',
borderRadius: '8px',
border: '2px solid',
cursor: 'pointer',
cursor: isSpectating ? 'not-allowed' : 'pointer',
opacity: isSpectating ? 0.5 : 1,
transition: 'all 0.2s',
display: 'flex',
flexDirection: 'column',
@@ -394,20 +443,23 @@ export function PlayingPhase() {
justifyContent: 'center',
gap: '0.25rem',
position: 'relative',
_hover: {
transform: selectedCardId && isEmpty ? 'scale(1.05)' : 'none',
boxShadow:
selectedCardId && isEmpty ? '0 4px 12px rgba(0,0,0,0.15)' : 'none',
},
_hover: isSpectating
? {}
: {
transform: selectedCardId && isEmpty ? 'scale(1.05)' : 'none',
boxShadow:
selectedCardId && isEmpty ? '0 4px 12px rgba(0,0,0,0.15)' : 'none',
},
})}
style={
isEmpty
? {
...gradientStyle,
// Active state: add slight glow when card is selected
boxShadow: selectedCardId
? '0 0 0 2px #1976d2, 0 2px 8px rgba(25, 118, 210, 0.3)'
: 'none',
boxShadow:
selectedCardId && !isSpectating
? '0 0 0 2px #1976d2, 0 2px 8px rgba(25, 118, 210, 0.3)'
: 'none',
}
: {
background: '#fff',
@@ -423,10 +475,16 @@ export function PlayingPhase() {
__html: card.svgContent,
}}
className={css({
width: '70px',
flex: 1,
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
'& svg': {
width: '100%',
height: 'auto',
height: '100%',
display: 'block',
},
})}
/>
@@ -457,35 +515,37 @@ export function PlayingPhase() {
{/* Insert button after this position */}
<button
key={`insert-${index + 1}`}
type="button"
onClick={() => handleInsertClick(index + 1)}
disabled={!selectedCardId}
disabled={!selectedCardId || isSpectating}
className={css({
width: '32px',
height: '50px',
background: selectedCardId ? '#1976d2' : '#2c5f76',
background: selectedCardId && !isSpectating ? '#1976d2' : '#2c5f76',
color: 'white',
border: 'none',
borderRadius: '20px',
fontSize: '24px',
fontWeight: 'bold',
cursor: selectedCardId ? 'pointer' : 'default',
opacity: selectedCardId ? 1 : 0.3,
cursor: selectedCardId && !isSpectating ? 'pointer' : 'not-allowed',
opacity: selectedCardId && !isSpectating ? 1 : 0.3,
transition: 'all 0.2s',
marginTop: '0.25rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
_hover: {
opacity: 1,
background: '#1976d2',
transform: selectedCardId ? 'scale(1.1)' : 'none',
},
_hover: isSpectating
? {}
: {
opacity: 1,
background: '#1976d2',
transform: selectedCardId ? 'scale(1.1)' : 'none',
},
})}
>
+
</button>
</div>
</>
)
})}
</div>

View File

@@ -5,7 +5,7 @@
* in ascending order using only visual patterns (no numbers shown).
*/
import { defineGame } from '@/lib/arcade/game-sdk'
import { defineGame, getGameTheme } from '@/lib/arcade/game-sdk'
import type { GameManifest } from '@/lib/arcade/game-sdk'
import { GameComponent } from './components/GameComponent'
import { CardSortingProvider } from './Provider'
@@ -24,9 +24,7 @@ const manifest: GameManifest = {
maxPlayers: 1, // Single player only
difficulty: 'Intermediate',
chips: ['🧠 Pattern Recognition', '🎯 Solo Challenge', '📊 Smart Scoring'],
color: 'teal',
gradient: 'linear-gradient(135deg, #99f6e4, #5eead4)',
borderColor: 'teal.200',
...getGameTheme('teal'),
available: true,
}

View File

@@ -3,7 +3,7 @@
* Complete integration into the arcade system with multiplayer support
*/
import { defineGame } from '@/lib/arcade/game-sdk'
import { defineGame, getGameTheme } from '@/lib/arcade/game-sdk'
import type { GameManifest } from '@/lib/arcade/game-sdk'
import { complementRaceValidator } from './Validator'
import { ComplementRaceProvider } from './Provider'
@@ -20,9 +20,7 @@ const manifest: GameManifest = {
maxPlayers: 4,
icon: '🏁',
chips: ['👥 1-4 Players', '🚂 Sprint Mode', '🤖 AI Opponents', '🔥 Speed Challenge'],
color: 'blue',
gradient: 'linear-gradient(135deg, #dbeafe, #bfdbfe)',
borderColor: 'blue.200',
...getGameTheme('blue'),
difficulty: 'Intermediate',
available: true,
}

View File

@@ -5,7 +5,7 @@
* Supports both abacus-numeral matching and complement pairs modes.
*/
import { defineGame } from '@/lib/arcade/game-sdk'
import { defineGame, getGameTheme } from '@/lib/arcade/game-sdk'
import type { GameManifest } from '@/lib/arcade/game-sdk'
import { MemoryPairsGame } from './components/MemoryPairsGame'
import { MatchingProvider } from './Provider'
@@ -23,9 +23,7 @@ const manifest: GameManifest = {
maxPlayers: 4,
difficulty: 'Intermediate',
chips: ['👥 Multiplayer', '🎯 Strategic', '🏆 Competitive'],
color: 'purple',
gradient: 'linear-gradient(135deg, #e9d5ff, #ddd6fe)',
borderColor: 'purple.200',
...getGameTheme('purple'),
available: true,
}

View File

@@ -5,7 +5,7 @@
* Supports both cooperative and competitive multiplayer modes.
*/
import { defineGame } from '@/lib/arcade/game-sdk'
import { defineGame, getGameTheme } from '@/lib/arcade/game-sdk'
import type { GameManifest } from '@/lib/arcade/game-sdk'
import { MemoryQuizGame } from './components/MemoryQuizGame'
import { MemoryQuizProvider } from './Provider'
@@ -23,9 +23,7 @@ const manifest: GameManifest = {
maxPlayers: 8,
difficulty: 'Intermediate',
chips: ['👥 Multiplayer', '🧠 Memory', '🧮 Soroban'],
color: 'blue',
gradient: 'linear-gradient(135deg, #dbeafe, #bfdbfe)',
borderColor: 'blue.200',
...getGameTheme('blue'),
available: true,
}

View File

@@ -31,7 +31,7 @@ function getAllGameConfigs() {
maxPlayers: gameDef.manifest.maxPlayers,
description: gameDef.manifest.description,
longDescription: gameDef.manifest.longDescription,
url: `/arcade/room?game=${gameDef.manifest.name}`, // Registry games load in room
url: '/arcade', // Arcade page handles game selection through UI
icon: gameDef.manifest.icon,
chips: gameDef.manifest.chips,
color: gameDef.manifest.color,

View File

@@ -80,7 +80,7 @@ export function AddPlayerButton({
})
// Close popover and navigate to room to choose game
setShowPopover(false)
router.push('/arcade/room')
router.push('/arcade')
},
onError: (error) => {
console.error('Failed to create room:', error)
@@ -111,7 +111,7 @@ export function AddPlayerButton({
}
// Close popover and navigate to room
setShowPopover(false)
router.push('/arcade/room')
router.push('/arcade')
},
}
)

View File

@@ -833,7 +833,7 @@ export function ModerationNotifications({
// Close the modal
onClose()
// Navigate to the room
router.push('/arcade/room')
router.push('/arcade')
} catch (error) {
console.error('Failed to join room:', error)
showError(

View File

@@ -68,7 +68,7 @@ export function PendingInvitations({ onInvitationChange, currentRoomId }: Pendin
// Join the room
await joinRoom({ roomId: invitation.roomId })
// Navigate to the room
router.push('/arcade/room')
router.push('/arcade')
// Refresh invitations
await fetchInvitations()
onInvitationChange?.()

View File

@@ -82,6 +82,13 @@ export { loadManifest } from './load-manifest'
*/
export { defineGame } from './define-game'
/**
* Standard color themes for game cards
* Use these to ensure consistent appearance across all games
*/
export { getGameTheme, GAME_THEMES } from '../game-themes'
export type { GameTheme, GameThemeName } from '../game-themes'
// ============================================================================
// Re-exports for convenience
// ============================================================================

View File

@@ -0,0 +1,88 @@
/**
* Standard color themes for arcade game cards
*
* Use these presets to ensure consistent, professional appearance
* across all game cards on the /arcade game chooser.
*
* All gradients use Panda CSS's 100-200 color range for soft pastel appearance.
*/
export interface GameTheme {
color: string
gradient: string
borderColor: string
}
/**
* Standard theme presets
* These use Panda CSS's color system and provide consistent styling
*/
export const GAME_THEMES = {
blue: {
color: 'blue',
gradient: 'linear-gradient(135deg, #dbeafe, #bfdbfe)', // blue.100 to blue.200
borderColor: 'blue.200',
},
purple: {
color: 'purple',
gradient: 'linear-gradient(135deg, #e9d5ff, #ddd6fe)', // purple.100 to purple.200
borderColor: 'purple.200',
},
green: {
color: 'green',
gradient: 'linear-gradient(135deg, #d1fae5, #a7f3d0)', // green.100 to green.200
borderColor: 'green.200',
},
teal: {
color: 'teal',
gradient: 'linear-gradient(135deg, #ccfbf1, #99f6e4)', // teal.100 to teal.200
borderColor: 'teal.200',
},
indigo: {
color: 'indigo',
gradient: 'linear-gradient(135deg, #e0e7ff, #c7d2fe)', // indigo.100 to indigo.200
borderColor: 'indigo.200',
},
pink: {
color: 'pink',
gradient: 'linear-gradient(135deg, #fce7f3, #fbcfe8)', // pink.100 to pink.200
borderColor: 'pink.200',
},
orange: {
color: 'orange',
gradient: 'linear-gradient(135deg, #ffedd5, #fed7aa)', // orange.100 to orange.200
borderColor: 'orange.200',
},
yellow: {
color: 'yellow',
gradient: 'linear-gradient(135deg, #fef3c7, #fde68a)', // yellow.100 to yellow.200
borderColor: 'yellow.200',
},
red: {
color: 'red',
gradient: 'linear-gradient(135deg, #fee2e2, #fecaca)', // red.100 to red.200
borderColor: 'red.200',
},
gray: {
color: 'gray',
gradient: 'linear-gradient(135deg, #f3f4f6, #e5e7eb)', // gray.100 to gray.200
borderColor: 'gray.200',
},
} as const satisfies Record<string, GameTheme>
export type GameThemeName = keyof typeof GAME_THEMES
/**
* Get a standard theme by name
* Use this in your game manifest instead of hardcoding gradients
*
* @example
* const manifest: GameManifest = {
* name: 'my-game',
* // ... other fields
* ...getGameTheme('blue')
* }
*/
export function getGameTheme(themeName: GameThemeName): GameTheme {
return GAME_THEMES[themeName]
}

View File

@@ -81,7 +81,7 @@ export function initializeSocketServer(httpServer: HTTPServer) {
session = await createArcadeSession({
userId,
gameName: room.gameName as GameName,
gameUrl: '/arcade/room',
gameUrl: '/arcade',
initialState,
activePlayers: roomPlayerIds, // Include all room members' active players
roomId: room.id,
@@ -173,7 +173,7 @@ export function initializeSocketServer(httpServer: HTTPServer) {
await createArcadeSession({
userId: data.userId,
gameName: 'matching',
gameUrl: '/arcade/room', // Room-based sessions use /arcade/room
gameUrl: '/arcade', // Room-based sessions use /arcade
initialState,
activePlayers,
roomId: room.id,

View File

@@ -1,6 +1,6 @@
{
"name": "soroban-monorepo",
"version": "4.10.2",
"version": "4.14.0",
"private": true,
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
"workspaces": [