full chess implementation
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
# Live Chess Implementation Status
|
||||
|
||||
## ✅ Completed Implementation
|
||||
|
||||
### 1. Chess Engine & Move Validation
|
||||
- **kchesslib dependency** added to `quartz/build.gradle.kts:150`
|
||||
- **ChessEngine** interface with expect/actual pattern:
|
||||
- `ChessEngine.kt` (commonMain) - Platform-agnostic API
|
||||
- `ChessEngine.jvmAndroid.kt` (jvmAndroid) - kchesslib implementation
|
||||
- **Full move validation**: Legal moves, check/checkmate, castling, en passant, pawn promotion
|
||||
- **FEN import/export**: Board state serialization
|
||||
- **Move history tracking**: SAN notation
|
||||
|
||||
### 2. Interactive UI Components (commons/)
|
||||
- **InteractiveChessBoard.kt**: Click-to-move with visual feedback
|
||||
- Selected piece highlighting (green)
|
||||
- Legal move indicators (circles/rings)
|
||||
- Automatic move validation
|
||||
- Callback system for move events
|
||||
|
||||
- **LiveChessGame.kt**: Complete game UI
|
||||
- NewChessGameDialog (challenge creation)
|
||||
- LiveChessGameScreen (full game interface)
|
||||
- GameInfoHeader (turn indicator, game status)
|
||||
- MoveHistoryDisplay (SAN move list)
|
||||
- GameControls (Resign, Offer Draw)
|
||||
|
||||
### 3. Nostr Event Protocol (quartz/nip64Chess/)
|
||||
**New Event Kinds:**
|
||||
| Kind | Event | Purpose |
|
||||
|------|-------|---------|
|
||||
| 30064 | LiveChessGameChallengeEvent | Create/accept challenges |
|
||||
| 30065 | LiveChessGameAcceptEvent | Accept challenge |
|
||||
| 30066 | LiveChessMoveEvent | Individual moves (SAN + FEN) |
|
||||
| 30067 | LiveChessGameEndEvent | Game result & termination |
|
||||
|
||||
**Event Factory Integration** (EventFactory.kt:190-193):
|
||||
- All live chess events registered for automatic parsing
|
||||
|
||||
### 4. Game State Management
|
||||
- **LiveChessGameState.kt**: Coordinates engine, Nostr, and UI
|
||||
- Turn validation
|
||||
- Move synchronization
|
||||
- Automatic end detection (checkmate/stalemate)
|
||||
- PGN generation
|
||||
- Position mismatch recovery
|
||||
|
||||
### 5. Feed Integration
|
||||
**Chess Feed Filter** (TopNavFilterState.kt:130-142):
|
||||
- Shows Kind 64 (completed games)
|
||||
- Shows Kind 30064 (challenges - open & directed)
|
||||
- Shows Kind 30067 (game endings)
|
||||
- Excludes Kind 30066 (individual moves - too noisy)
|
||||
|
||||
**Event Rendering** (Chess.kt, NoteCompose.kt):
|
||||
- `RenderChessGame()` - Completed PGN games (Kind 64)
|
||||
- `RenderLiveChessChallenge()` - Challenge cards with visual states:
|
||||
- 🔓 **Open challenges** (green border)
|
||||
- 💌 **Incoming challenges** (orange border)
|
||||
- ⏳ **Sent challenges** (gray border)
|
||||
- `RenderLiveChessGameEnd()` - Game results with PGN
|
||||
|
||||
## 🚧 Remaining Work
|
||||
|
||||
### Priority 1: Navigation & Game Flow
|
||||
**Files to create:**
|
||||
1. **ChessViewModel.kt** (amethyst/ui/screen/loggedIn/chess/)
|
||||
```kotlin
|
||||
class ChessViewModel(account: Account) {
|
||||
val activeGames: StateFlow<List<LiveChessGameState>>
|
||||
val challenges: StateFlow<List<LiveChessGameChallengeEvent>>
|
||||
val badgeCount: StateFlow<Int> // For in-app notifications
|
||||
|
||||
fun createChallenge(opponentPubkey: String?, color: Color)
|
||||
fun acceptChallenge(challengeEventId: String)
|
||||
fun publishMove(gameId: String, move: ChessMoveEvent)
|
||||
}
|
||||
```
|
||||
|
||||
2. **Navigation Routes** (ui/navigation/routes/)
|
||||
- Add `LiveChessGame(gameId: String)` route
|
||||
- Wire up from challenge card click
|
||||
- Pass LiveChessGameState to screen
|
||||
|
||||
3. **ChessGameScreen.kt** (amethyst/ui/screen/loggedIn/chess/)
|
||||
- Wrapper around `LiveChessGameScreen` composable
|
||||
- Connects ViewModel to UI
|
||||
- Handles move publishing callback
|
||||
|
||||
### Priority 2: Event Publishing & Subscriptions
|
||||
**Files to modify:**
|
||||
1. **ChessViewModel.kt** - Add relay operations:
|
||||
```kotlin
|
||||
// Subscribe to game events
|
||||
fun subscribeToGame(gameId: String) {
|
||||
// Listen for Kind 30066 (moves) with d tag = gameId
|
||||
// Listen for Kind 30067 (game end) with d tag = gameId
|
||||
}
|
||||
|
||||
// Publish events
|
||||
suspend fun publishChallenge(challenge: ChessGameChallenge) {
|
||||
val event = LiveChessGameChallengeEvent.build(...)
|
||||
account.sendEvent(event)
|
||||
}
|
||||
```
|
||||
|
||||
2. **Event Handlers** - Process incoming events:
|
||||
```kotlin
|
||||
fun handleOpponentMove(moveEvent: LiveChessMoveEvent) {
|
||||
val game = activeGames.find { it.gameId == moveEvent.gameId() }
|
||||
game?.applyOpponentMove(moveEvent.san(), moveEvent.fen())
|
||||
}
|
||||
```
|
||||
|
||||
### Priority 3: FAB & Challenge Creation
|
||||
**Files to modify:**
|
||||
1. **HomeScreen.kt** or chess-specific screen
|
||||
```kotlin
|
||||
var showNewGameDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
floatingActionButton = {
|
||||
if (isChessFeed) {
|
||||
FloatingActionButton(
|
||||
onClick = { showNewGameDialog = true }
|
||||
) {
|
||||
Icon(Icons.Default.Add, "New Game")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (showNewGameDialog) {
|
||||
NewChessGameDialog(
|
||||
onDismiss = { showNewGameDialog = false },
|
||||
onCreateGame = { opponentPubkey, color ->
|
||||
viewModel.createChallenge(opponentPubkey, color)
|
||||
showNewGameDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Priority 4: In-App Badges (Optional)
|
||||
**Files to create:**
|
||||
1. **BadgeProvider.kt** (commons/)
|
||||
```kotlin
|
||||
object ChessBadgeProvider {
|
||||
fun getBadgeCount(
|
||||
challenges: List<LiveChessGameChallengeEvent>,
|
||||
activeGames: List<LiveChessGameState>,
|
||||
userPubkey: String
|
||||
): Int {
|
||||
val incomingChallenges = challenges.count {
|
||||
it.opponentPubkey() == userPubkey
|
||||
}
|
||||
val yourTurnGames = activeGames.count { it.isPlayerTurn() }
|
||||
return incomingChallenges + yourTurnGames
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Badge Display** - Add to chess filter icon:
|
||||
```kotlin
|
||||
BadgedBox(badge = { Badge { Text("$count") } }) {
|
||||
Icon(/* chess icon */)
|
||||
}
|
||||
```
|
||||
|
||||
## 📋 Implementation Checklist
|
||||
|
||||
### Phase 1: Basic Playability
|
||||
- [ ] Create ChessViewModel
|
||||
- [ ] Add navigation route for LiveChessGameScreen
|
||||
- [ ] Create ChessGameScreen wrapper
|
||||
- [ ] Wire up NewGameDialog → ViewModel.createChallenge()
|
||||
- [ ] Implement event publishing (challenges, moves, game end)
|
||||
- [ ] Test: Create challenge, see it in feed
|
||||
|
||||
### Phase 2: Two-Player Functionality
|
||||
- [ ] Implement relay subscriptions for game events
|
||||
- [ ] Wire up move synchronization (publish/receive)
|
||||
- [ ] Handle challenge acceptance flow
|
||||
- [ ] Test: Play complete game between two accounts
|
||||
|
||||
### Phase 3: Polish
|
||||
- [ ] Add FAB to chess feed
|
||||
- [ ] Implement badge counts
|
||||
- [ ] Add "Accept Challenge" button to incoming challenge cards
|
||||
- [ ] Add "View Game" navigation from challenge/game-end cards
|
||||
- [ ] Handle edge cases (network errors, position desync, abandoned games)
|
||||
|
||||
### Phase 4: Persistence (Optional)
|
||||
- [ ] Create Room entities for active games
|
||||
- [ ] Store games locally for offline viewing
|
||||
- [ ] Sync on app start
|
||||
- [ ] Background service for "your turn" notifications
|
||||
|
||||
## 🎯 UX Decisions Implemented
|
||||
|
||||
Based on user choices:
|
||||
|
||||
1. ✅ **Entry Point**: FAB on Chess feed (pending UI implementation)
|
||||
2. ✅ **Display**: Visual cards in one feed with borders/icons
|
||||
3. ✅ **Discovery**: Open challenges shown in Chess feed (green border)
|
||||
4. ✅ **Tap Action**: Navigate directly to full screen (pending nav)
|
||||
5. ⏳ **Notifications**: In-app badge for counts (pending implementation)
|
||||
6. ✅ **Filter Scope**: Meaningful events only (completed games, challenges, endings)
|
||||
7. ⏳ **Storage**: Local cache + relay sync (pending Room/ViewModel)
|
||||
8. ✅ **Input**: Click-to-move (already implemented)
|
||||
|
||||
## 🔧 Quick Start for Development
|
||||
|
||||
### Test the Chess Engine:
|
||||
```kotlin
|
||||
val engine = ChessEngine()
|
||||
engine.reset()
|
||||
val result = engine.makeMove("e2", "e4")
|
||||
println("Move success: ${result.success}, SAN: ${result.san}")
|
||||
```
|
||||
|
||||
### Test the Interactive Board:
|
||||
```kotlin
|
||||
@Composable
|
||||
fun TestScreen() {
|
||||
val engine = remember { ChessEngine().apply { reset() } }
|
||||
InteractiveChessBoard(
|
||||
engine = engine,
|
||||
onMoveMade = { san -> println("Move made: $san") }
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### View Chess Feed:
|
||||
1. Open Amethyst
|
||||
2. Select "Chess" from home feed dropdown
|
||||
3. See completed games (Kind 64)
|
||||
4. See challenges with colored borders (Kind 30064)
|
||||
5. See game endings with results (Kind 30067)
|
||||
|
||||
## 📁 File Structure
|
||||
|
||||
```
|
||||
AmethystMultiplatform/
|
||||
├── quartz/ # Protocol layer (KMP)
|
||||
│ ├── src/commonMain/kotlin/.../nip64Chess/
|
||||
│ │ ├── ChessEngine.kt ✅ Engine interface
|
||||
│ │ ├── ChessPosition.kt ✅ Board state
|
||||
│ │ ├── ChessMove.kt ✅ Move representation
|
||||
│ │ ├── ChessGame.kt ✅ Game model
|
||||
│ │ ├── PGNParser.kt ✅ PGN parsing (31 tests)
|
||||
│ │ ├── ChessGameEvent.kt ✅ Kind 64
|
||||
│ │ ├── LiveChessEvents.kt ✅ Data classes
|
||||
│ │ ├── LiveChessGameEvents.kt ✅ Kind 30064-30067
|
||||
│ │ └── LiveChessGameState.kt ✅ Game coordinator
|
||||
│ └── src/jvmAndroid/kotlin/.../nip64Chess/
|
||||
│ └── ChessEngine.jvmAndroid.kt ✅ kchesslib impl
|
||||
│
|
||||
├── commons/ # Shared UI (Android/Desktop)
|
||||
│ └── src/main/java/.../commons/chess/
|
||||
│ ├── ChessBoard.kt ✅ Static board
|
||||
│ ├── InteractiveChessBoard.kt ✅ Click-to-move
|
||||
│ ├── ChessGameViewer.kt ✅ PGN playback
|
||||
│ ├── MoveNavigator.kt ✅ Move controls
|
||||
│ ├── PGNMetadata.kt ✅ Game info
|
||||
│ └── LiveChessGame.kt ✅ Dialog + screen
|
||||
│
|
||||
├── amethyst/ # Android app
|
||||
│ └── src/main/java/.../amethyst/
|
||||
│ ├── ui/note/types/
|
||||
│ │ └── Chess.kt ✅ Event renderers (3 types)
|
||||
│ ├── ui/note/
|
||||
│ │ └── NoteCompose.kt ✅ Event dispatching
|
||||
│ ├── ui/screen/
|
||||
│ │ ├── TopNavFilterState.kt ✅ Chess filter (3 kinds)
|
||||
│ │ └── loggedIn/chess/ ⏳ TODO: ViewModels, screens
|
||||
│ └── ui/navigation/routes/ ⏳ TODO: Navigation
|
||||
│
|
||||
└── docs/
|
||||
└── live-chess-implementation-status.md ✅ This file
|
||||
```
|
||||
|
||||
## 🚀 Next Steps
|
||||
|
||||
**To make chess fully playable**, implement in this order:
|
||||
|
||||
1. **ChessViewModel** - Game state management & event publishing
|
||||
2. **Navigation** - Route to LiveChessGameScreen
|
||||
3. **Event Subscriptions** - Listen for opponent moves
|
||||
4. **FAB** - Add "New Game" button to chess feed
|
||||
5. **Test** - Play a complete game between two test accounts
|
||||
|
||||
The foundation is complete and tested. The remaining work is "wiring" - connecting the chess engine and UI to Amethyst's existing relay/ViewModel infrastructure.
|
||||
|
||||
---
|
||||
|
||||
**Status**: Core chess functionality fully implemented. Integration layer partially complete. Estimated 4-6 hours of focused work to reach playable state.
|
||||
Reference in New Issue
Block a user