Compare commits

...

41 Commits

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

### Bug Fixes

* **deployment:** pass git info to Docker build for deployment info modal ([4b04e43](4b04e43ff8))
2025-10-19 02:24:27 +00:00
Thomas Hallock
4b04e43ff8 fix(deployment): pass git info to Docker build for deployment info modal
This commit fixes two critical production issues:

1. **Flashcard generator dependencies** - Added all required dependencies to Dockerfile:
   - Typst for PDF generation
   - Python pip and setuptools
   - Python packages (pyyaml, Pillow, imagehash)
   - packages/core directory with generate.py script

2. **Deployment info modal** - Fixed git commit hash display on production:
   - Modified generate-build-info.js to accept env vars as fallback when .git is unavailable
   - Updated Dockerfile to accept GIT_* build arguments
   - Updated GitHub Actions workflow to pass git information during Docker build

The deployment info modal (Ctrl+Shift+I) will now show the correct commit hash,
branch, and build time on production, matching the behavior on dev.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 21:23:15 -05:00
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
21 changed files with 2548 additions and 221 deletions

View File

@@ -57,6 +57,10 @@ jobs:
type=ref,event=pr
type=raw,value=latest,enable={{is_default_branch}}
- name: Get short commit SHA
id: vars
run: echo "short_sha=$(echo ${{ github.sha }} | cut -c1-8)" >> $GITHUB_OUTPUT
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
@@ -64,3 +68,9 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_COMMIT=${{ github.sha }}
GIT_COMMIT_SHORT=${{ steps.vars.outputs.short_sha }}
GIT_BRANCH=${{ github.ref_name }}
GIT_TAG=${{ github.ref_type == 'tag' && github.ref_name || '' }}
GIT_DIRTY=false

View File

@@ -1,3 +1,139 @@
## [4.14.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.0...v4.14.1) (2025-10-19)
### Bug Fixes
* **deployment:** pass git info to Docker build for deployment info modal ([4b04e43](https://github.com/antialias/soroban-abacus-flashcards/commit/4b04e43ff8c9e9f239d7f5e306aab338b535296f))
## [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)

View File

@@ -21,6 +21,21 @@ RUN pnpm install --frozen-lockfile
# Builder stage
FROM base AS builder
# Accept git information as build arguments
ARG GIT_COMMIT
ARG GIT_COMMIT_SHORT
ARG GIT_BRANCH
ARG GIT_TAG
ARG GIT_DIRTY
# Set as environment variables for build scripts
ENV GIT_COMMIT=${GIT_COMMIT}
ENV GIT_COMMIT_SHORT=${GIT_COMMIT_SHORT}
ENV GIT_BRANCH=${GIT_BRANCH}
ENV GIT_TAG=${GIT_TAG}
ENV GIT_DIRTY=${GIT_DIRTY}
COPY . .
# Generate Panda CSS styled-system before building
@@ -33,8 +48,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 +70,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,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

@@ -98,7 +98,10 @@
"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\"\")\"\"')",
"WebFetch(domain:abaci.one)"
"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

@@ -18,11 +18,13 @@ function exec(command) {
}
function getBuildInfo() {
const gitCommit = exec('git rev-parse HEAD')
const gitCommitShort = exec('git rev-parse --short HEAD')
const gitBranch = exec('git rev-parse --abbrev-ref HEAD')
const gitTag = exec('git describe --tags --exact-match 2>/dev/null')
const gitDirty = exec('git diff --quiet || echo "dirty"') === 'dirty'
// Try to get git info from environment variables first (for Docker builds)
// Fall back to git commands (for local development)
const gitCommit = process.env.GIT_COMMIT || exec('git rev-parse HEAD')
const gitCommitShort = process.env.GIT_COMMIT_SHORT || exec('git rev-parse --short HEAD')
const gitBranch = process.env.GIT_BRANCH || exec('git rev-parse --abbrev-ref HEAD')
const gitTag = process.env.GIT_TAG || exec('git describe --tags --exact-match 2>/dev/null')
const gitDirty = process.env.GIT_DIRTY === 'true' || exec('git diff --quiet || echo "dirty"') === 'dirty'
const packageJson = require('../package.json')

View File

@@ -3,12 +3,13 @@
import { useState } from 'react'
import Link from 'next/link'
import { PageWithNav } from '@/components/PageWithNav'
import { AbacusReact } from '@soroban/abacus-react'
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
import { css } from '../../styled-system/css'
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 Mastery Platform" navEmoji="🧮">
<div className={css({ bg: 'gray.900', minHeight: '100vh' })}>
@@ -53,55 +54,31 @@ export default function HomePage() {
<div
className={css({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '4',
justifyContent: 'center',
my: '10',
p: '8',
bg: 'rgba(0, 0, 0, 0.4)',
bg: 'white',
borderRadius: '2xl',
border: '2px solid',
borderColor: 'purple.500/30',
boxShadow: '0 25px 50px -12px rgba(139, 92, 246, 0.25)',
color: '#1f2937',
})}
>
<div
className={css({
fontSize: 'lg',
fontWeight: 'semibold',
color: 'gray.300',
mb: '2',
})}
>
Click the beads to interact!
</div>
<AbacusReact
defaultValue={1234567}
value={abacusValue}
columns={7}
beadShape="diamond"
colorScheme="place-value"
hideInactiveBeads={false}
beadShape={appConfig.beadShape}
colorScheme={appConfig.colorScheme}
hideInactiveBeads={appConfig.hideInactiveBeads}
interactive={true}
animated={true}
soundEnabled={true}
soundVolume={0.4}
scaleFactor={2.2}
showNumbers={true}
callbacks={{
onValueChange: (newValue: number) => setAbacusValue(newValue),
}}
onValueChange={(newValue: number) => setAbacusValue(newValue)}
/>
<div
className={css({
fontSize: '4xl',
fontWeight: 'bold',
color: 'yellow.400',
fontFamily: 'mono',
letterSpacing: 'wide',
})}
>
{abacusValue.toLocaleString()}
</div>
</div>
<p
@@ -185,57 +162,8 @@ export default function HomePage() {
</div>
</div>
{/* Color Scheme Showcase */}
{/* Main content container */}
<div className={container({ maxW: '7xl', px: '4', py: '12' })}>
<section className={stack({ gap: '8' })}>
<div className={css({ textAlign: 'center' })}>
<h2
className={css({
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '3',
})}
>
Beautiful Color Schemes
</h2>
<p className={css({ color: 'gray.400', fontSize: 'lg' })}>
Choose from multiple visual styles to enhance learning
</p>
</div>
<div className={grid({ columns: { base: 1, sm: 2, lg: 4 }, gap: '6' })}>
<ColorSchemeCard
title="Monochrome"
description="Classic, minimalist design"
colorScheme="monochrome"
value={42}
beadShape="diamond"
/>
<ColorSchemeCard
title="Place Value"
description="Each column has its own color"
colorScheme="place-value"
value={789}
beadShape="circle"
/>
<ColorSchemeCard
title="Heaven & Earth"
description="Distinct heaven and earth beads"
colorScheme="heaven-earth"
value={156}
beadShape="square"
/>
<ColorSchemeCard
title="Alternating"
description="Alternating column colors"
colorScheme="alternating"
value={234}
beadShape="diamond"
/>
</div>
</section>
{/* Arcade Games Section */}
<section className={stack({ gap: '6', mt: '16' })}>
<div className={hstack({ justify: 'space-between', alignItems: 'center' })}>
@@ -336,36 +264,6 @@ export default function HomePage() {
accentColor="blue"
/>
</div>
{/* Stats Banner */}
<section
className={css({
bg: 'linear-gradient(135deg, rgba(139, 92, 246, 0.2) 0%, rgba(59, 130, 246, 0.2) 100%)',
rounded: '2xl',
p: '10',
mt: '16',
border: '2px solid',
borderColor: 'purple.500/20',
})}
>
<h2
className={css({
fontSize: { base: 'xl', md: '2xl' },
fontWeight: 'bold',
mb: '8',
textAlign: 'center',
color: 'white',
})}
>
Complete Soroban Learning Platform
</h2>
<div className={grid({ columns: { base: 2, md: 4 }, gap: '8', textAlign: 'center' })}>
<StatItem number="4" label="Arcade Games" />
<StatItem number="8" label="Max Players" />
<StatItem number="3" label="Learning Modes" />
<StatItem number="4+" label="Export Formats" />
</div>
</section>
</div>
</div>
</PageWithNav>
@@ -565,23 +463,3 @@ function FeaturePanel({
</div>
)
}
function StatItem({ number, label }: { number: string; label: string }) {
return (
<div>
<div
className={css({
fontSize: { base: '3xl', md: '4xl' },
fontWeight: 'bold',
mb: '2',
background: 'linear-gradient(135deg, #fbbf24, #f59e0b)',
backgroundClip: 'text',
color: 'transparent',
})}
>
{number}
</div>
<div className={css({ fontSize: 'sm', color: 'gray.300' })}>{label}</div>
</div>
)
}

View File

@@ -33,6 +33,9 @@ interface CardSortingContextValue {
// UI state
selectedCardId: string | null
selectCard: (cardId: string | null) => void
// Spectator mode
localPlayerId: string | undefined
isSpectating: boolean
}
// Create context
@@ -546,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',
},
})}
>
@@ -280,14 +288,15 @@ export function PlayingPhase() {
key={card.id}
onClick={() => handleCardClick(card.id)}
className={css({
width: '90px',
height: '90px',
width: '140px',
height: '140px',
padding: '8px',
border: '2px solid',
borderColor: selectedCardId === card.id ? '#1976d2' : 'transparent',
borderRadius: '8px',
background: selectedCardId === card.id ? '#e3f2fd' : 'white',
cursor: 'pointer',
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',
@@ -296,11 +305,13 @@ export function PlayingPhase() {
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 2px 6px rgba(0,0,0,0.1)',
_hover: {
transform: 'translateY(-5px)',
boxShadow: '0 8px 16px rgba(0,0,0,0.2)',
borderColor: '#2c5f76',
},
_hover: isSpectating
? {}
: {
transform: 'translateY(-5px)',
boxShadow: '0 8px 16px rgba(0,0,0,0.2)',
borderColor: '#2c5f76',
},
})}
style={
selectedCardId === card.id
@@ -314,8 +325,8 @@ export function PlayingPhase() {
<div
dangerouslySetInnerHTML={{ __html: card.svgContent }}
className={css({
width: '74px',
height: '74px',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
@@ -378,27 +389,29 @@ export function PlayingPhase() {
<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',
},
})}
>
+
@@ -416,12 +429,13 @@ export function PlayingPhase() {
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',
@@ -429,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',
@@ -458,13 +475,12 @@ export function PlayingPhase() {
__html: card.svgContent,
}}
className={css({
width: '70px',
height: '70px',
flex: 1,
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
margin: '0 auto',
'& svg': {
width: '100%',
height: '100%',
@@ -502,27 +518,29 @@ export function PlayingPhase() {
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',
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',
},
})}
>
+

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

@@ -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

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