Merge branch 'main' into claude/migrate-quartz-nip58-XQ8bw
This commit is contained in:
+4
-1
@@ -92,7 +92,10 @@ class LabeledBookmarkListEvent(
|
||||
const val ALT = "A labeled list of bookmarks"
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
fun createBookmarkAddress(pubKey: HexKey) = Address(KIND, pubKey, Uuid.random().toString())
|
||||
fun createAddress(
|
||||
pubKey: HexKey,
|
||||
dTag: String,
|
||||
) = Address(KIND, pubKey, dTag)
|
||||
|
||||
suspend fun create(
|
||||
title: String = "",
|
||||
|
||||
+25
-1
@@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip64Chess
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip64Chess.jester.JesterGameEvents
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
|
||||
/**
|
||||
* Deterministic chess state reconstruction from Jester protocol events.
|
||||
@@ -59,16 +60,23 @@ object ChessStateReconstructor {
|
||||
events: JesterGameEvents,
|
||||
viewerPubkey: String,
|
||||
): ReconstructionResult {
|
||||
Log.d("chessdebug", "[Reconstructor] reconstruct() called: viewerPubkey=${viewerPubkey.take(8)}, startEvent=${events.startEvent?.id?.take(8)}, moveCount=${events.moves.size}")
|
||||
|
||||
// Step 1: Get start event (required for player info)
|
||||
val startEvent =
|
||||
events.startEvent
|
||||
?: return ReconstructionResult.Error("No start event found")
|
||||
?: run {
|
||||
Log.d("chessdebug", "[Reconstructor] ERROR: No start event found")
|
||||
return ReconstructionResult.Error("No start event found")
|
||||
}
|
||||
|
||||
val startEventId = startEvent.id
|
||||
val challengerPubkey = startEvent.pubKey
|
||||
val challengerColor = startEvent.playerColor() ?: Color.WHITE
|
||||
val challengedPubkey = startEvent.opponentPubkey()
|
||||
|
||||
Log.d("chessdebug", "[Reconstructor] startEventId=${startEventId.take(8)}, challenger=${challengerPubkey.take(8)}, challengerColor=$challengerColor, challengedPubkey=${challengedPubkey?.take(8)}")
|
||||
|
||||
// In Jester, we determine opponent from the p-tag or from move events
|
||||
// For open challenges, we need to find who made the first move
|
||||
val opponentFromMoves =
|
||||
@@ -77,6 +85,7 @@ object ChessStateReconstructor {
|
||||
?.pubKey
|
||||
|
||||
val actualOpponent = challengedPubkey ?: opponentFromMoves
|
||||
Log.d("chessdebug", "[Reconstructor] opponentFromMoves=${opponentFromMoves?.take(8)}, actualOpponent=${actualOpponent?.take(8)}")
|
||||
|
||||
// Determine players based on challenger's color choice
|
||||
val (whitePubkey, blackPubkey) =
|
||||
@@ -108,6 +117,8 @@ object ChessStateReconstructor {
|
||||
ViewerRole.SPECTATOR -> blackPubkey ?: "" // For spectators, "opponent" is black
|
||||
}
|
||||
|
||||
Log.d("chessdebug", "[Reconstructor] white=${whitePubkey?.take(8)}, black=${blackPubkey?.take(8)}, viewerRole=$viewerRole, playerColor=$playerColor")
|
||||
|
||||
// Check if game is pending (no moves yet AND viewer is the challenger)
|
||||
// If viewer accepted someone else's challenge, the game is NOT pending
|
||||
val isChallenger = viewerPubkey == startEvent.pubKey
|
||||
@@ -118,6 +129,11 @@ object ChessStateReconstructor {
|
||||
val latestMove = events.latestMove()
|
||||
val history = latestMove?.history() ?: emptyList()
|
||||
|
||||
Log.d("chessdebug", "[Reconstructor] latestMove=${latestMove?.id?.take(8)}, historySize=${history.size}, isPendingChallenge=$isPendingChallenge")
|
||||
if (history.isNotEmpty()) {
|
||||
Log.d("chessdebug", "[Reconstructor] history: ${history.joinToString(" ")}")
|
||||
}
|
||||
|
||||
// Track applied moves
|
||||
val appliedMoveNumbers = mutableSetOf<Int>()
|
||||
var isDesynced = false
|
||||
@@ -131,8 +147,10 @@ object ChessStateReconstructor {
|
||||
} else {
|
||||
// Move failed - game might be desynced
|
||||
isDesynced = true
|
||||
Log.d("chessdebug", "[Reconstructor] DESYNC: move #$moveNumber '$san' failed: ${result.error}")
|
||||
// Try to recover by loading the FEN from latest move if available
|
||||
latestMove?.fen()?.let { fen ->
|
||||
Log.d("chessdebug", "[Reconstructor] Recovering with FEN: $fen")
|
||||
engine.loadFen(fen)
|
||||
}
|
||||
break
|
||||
@@ -144,6 +162,7 @@ object ChessStateReconstructor {
|
||||
val currentFen = engine.getFen()
|
||||
if (!fenPositionsMatch(currentFen, expectedFen)) {
|
||||
isDesynced = true
|
||||
Log.d("chessdebug", "[Reconstructor] FEN MISMATCH: current=$currentFen, expected=$expectedFen")
|
||||
engine.loadFen(expectedFen)
|
||||
}
|
||||
}
|
||||
@@ -154,17 +173,20 @@ object ChessStateReconstructor {
|
||||
when {
|
||||
gameResult != null -> {
|
||||
val result = parseGameResult(gameResult)
|
||||
Log.d("chessdebug", "[Reconstructor] Game finished from result tag: $gameResult -> $result")
|
||||
GameStatus.Finished(result)
|
||||
}
|
||||
|
||||
engine.isCheckmate() -> {
|
||||
val winner = engine.getSideToMove().opposite()
|
||||
Log.d("chessdebug", "[Reconstructor] Checkmate detected, winner=$winner")
|
||||
GameStatus.Finished(
|
||||
if (winner == Color.WHITE) GameResult.WHITE_WINS else GameResult.BLACK_WINS,
|
||||
)
|
||||
}
|
||||
|
||||
engine.isStalemate() -> {
|
||||
Log.d("chessdebug", "[Reconstructor] Stalemate detected")
|
||||
GameStatus.Finished(GameResult.DRAW)
|
||||
}
|
||||
|
||||
@@ -197,6 +219,8 @@ object ChessStateReconstructor {
|
||||
timeControl = null, // Not supported in Jester
|
||||
)
|
||||
|
||||
Log.d("chessdebug", "[Reconstructor] Result: status=$gameStatus, appliedMoves=${appliedMoveNumbers.size}, isDesynced=$isDesynced, headEvent=${headEventId.take(8)}")
|
||||
|
||||
return ReconstructionResult.Success(state, engine)
|
||||
}
|
||||
|
||||
|
||||
+13
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip64Chess
|
||||
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -125,12 +126,15 @@ class LiveChessGameState(
|
||||
to: String,
|
||||
promotion: PieceType? = null,
|
||||
): ChessMoveEvent? {
|
||||
Log.d("chessdebug", "[LiveGame] makeMove: game=${startEventId.take(8)}, from=$from, to=$to, promotion=$promotion, isPlayerTurn=${isPlayerTurn()}, status=${_gameStatus.value}")
|
||||
if (!isPlayerTurn()) {
|
||||
Log.d("chessdebug", "[LiveGame] makeMove REJECTED: not player's turn (sideToMove=${engine.getSideToMove()}, playerColor=$playerColor)")
|
||||
_lastError.value = "Not your turn"
|
||||
return null
|
||||
}
|
||||
|
||||
if (_gameStatus.value != GameStatus.InProgress) {
|
||||
Log.d("chessdebug", "[LiveGame] makeMove REJECTED: game not in progress (${_gameStatus.value})")
|
||||
_lastError.value = "Game is not in progress"
|
||||
return null
|
||||
}
|
||||
@@ -171,6 +175,7 @@ class LiveChessGameState(
|
||||
* @param newHeadEventId The ID of the newly published move event
|
||||
*/
|
||||
fun updateHeadEventId(newHeadEventId: String) {
|
||||
Log.d("chessdebug", "[LiveGame] updateHeadEventId: game=${startEventId.take(8)}, old=${_headEventId.value.take(8)}, new=${newHeadEventId.take(8)}")
|
||||
_headEventId.value = newHeadEventId
|
||||
}
|
||||
|
||||
@@ -204,8 +209,10 @@ class LiveChessGameState(
|
||||
fen: String,
|
||||
moveNumber: Int? = null,
|
||||
): Boolean {
|
||||
Log.d("chessdebug", "[LiveGame] applyOpponentMove: game=${startEventId.take(8)}, san=$san, moveNumber=$moveNumber, expectedMove=${_moveHistory.value.size + 1}")
|
||||
// Check for duplicate move
|
||||
if (moveNumber != null && receivedMoveNumbers.contains(moveNumber)) {
|
||||
Log.d("chessdebug", "[LiveGame] applyOpponentMove: DUPLICATE move #$moveNumber, ignoring")
|
||||
// Already processed this move, ignore
|
||||
return true
|
||||
}
|
||||
@@ -213,12 +220,14 @@ class LiveChessGameState(
|
||||
// Check for out-of-order move
|
||||
val expectedMoveNumber = _moveHistory.value.size + 1
|
||||
if (moveNumber != null && moveNumber > expectedMoveNumber) {
|
||||
Log.d("chessdebug", "[LiveGame] applyOpponentMove: OUT-OF-ORDER move #$moveNumber (expected #$expectedMoveNumber), queuing")
|
||||
// Store for later processing
|
||||
pendingMoves[moveNumber] = san to fen
|
||||
return true
|
||||
}
|
||||
|
||||
if (isPlayerTurn()) {
|
||||
Log.d("chessdebug", "[LiveGame] applyOpponentMove: REJECTED - it's player's turn, not opponent's")
|
||||
_lastError.value = "Received move but it's not opponent's turn"
|
||||
return false
|
||||
}
|
||||
@@ -237,11 +246,13 @@ class LiveChessGameState(
|
||||
if (currentFen != fen) {
|
||||
// Positions don't match - desync detected
|
||||
_isDesynced.value = true
|
||||
Log.d("chessdebug", "[LiveGame] applyOpponentMove: FEN MISMATCH after $san - current=$currentFen, expected=$fen")
|
||||
_lastError.value = "Position mismatch - syncing to opponent's position"
|
||||
// Load the opponent's FEN to stay in sync
|
||||
engine.loadFen(fen)
|
||||
} else {
|
||||
_isDesynced.value = false
|
||||
Log.d("chessdebug", "[LiveGame] applyOpponentMove: SUCCESS - $san applied, totalMoves=${_moveHistory.value.size + 1}")
|
||||
}
|
||||
|
||||
_currentPosition.value = engine.getPosition()
|
||||
@@ -260,6 +271,7 @@ class LiveChessGameState(
|
||||
|
||||
return true
|
||||
} else {
|
||||
Log.d("chessdebug", "[LiveGame] applyOpponentMove: INVALID move $san - ${result.error}")
|
||||
_lastError.value = "Invalid opponent move: $san"
|
||||
return false
|
||||
}
|
||||
@@ -512,6 +524,7 @@ class LiveChessGameState(
|
||||
* Used when loading a game from cache that already has an end event.
|
||||
*/
|
||||
fun markAsFinished(result: GameResult) {
|
||||
Log.d("chessdebug", "[LiveGame] markAsFinished: game=${startEventId.take(8)}, result=$result")
|
||||
_gameStatus.value = GameStatus.Finished(result)
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -24,6 +24,7 @@ import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class BleChunkAssemblerTest {
|
||||
@Test
|
||||
@@ -43,7 +44,7 @@ class BleChunkAssemblerTest {
|
||||
val assembler = BleChunkAssembler()
|
||||
val message = """["EVENT",{"content":"hello world from nostr ble mesh networking"}]"""
|
||||
val chunks = BleMessageChunker.splitIntoChunks(message, chunkSize = 10)
|
||||
assert(chunks.size > 1)
|
||||
assertTrue(chunks.size > 1)
|
||||
|
||||
for (i in 0 until chunks.size - 1) {
|
||||
val result = assembler.addChunk(chunks[i])
|
||||
|
||||
Reference in New Issue
Block a user