Files
Matewave/app/src/main/java/cc/skysparkle/matewave/clock/GameClock.kt
T
Claude 6ac5a582db Add app source for F-Droid release (v1.0.43)
Adds the full Android source tree with unit tests, fastlane store
metadata and the Gradle wrapper. The archive shipped without
gradle-wrapper.jar, so the official Gradle 9.7.1 wrapper jar is
included to make ./gradlew work from a fresh clone. .gitignore now
also excludes build output and keystores. README gains build
instructions and an updated structure tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015g6ECVSKcBG8CJPrqC3G5o
2026-10-06 18:23:20 +00:00

127 lines
4.2 KiB
Kotlin

package cc.skysparkle.matewave.clock
import cc.skysparkle.matewave.engine.PieceColor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
data class ClockState(
val whiteMillis: Long,
val blackMillis: Long,
val running: PieceColor?,
val flagged: PieceColor? = null
)
/** Chess clock with increment; state is exposed as a StateFlow for the UI. */
class GameClock(private val timeControl: TimeControl, private val scope: CoroutineScope) {
private val unlimited = timeControl.initialSeconds == Int.MAX_VALUE
private var whiteLeft = timeControl.initialSeconds * 1000L
private var blackLeft = timeControl.initialSeconds * 1000L
private var running: PieceColor? = null
private var flagged: PieceColor? = null
private var lastAccountedNs = System.nanoTime()
private val _state = MutableStateFlow(ClockState(whiteLeft, blackLeft, null))
val state: StateFlow<ClockState> = _state
private var tickJob: Job? = null
private fun account() {
val now = System.nanoTime()
val elapsedMs = (now - lastAccountedNs) / 1_000_000L
lastAccountedNs = now
val side = running ?: return
if (unlimited || flagged != null || elapsedMs <= 0) return
if (side == PieceColor.WHITE) whiteLeft = (whiteLeft - elapsedMs).coerceAtLeast(0)
else blackLeft = (blackLeft - elapsedMs).coerceAtLeast(0)
}
private fun publish() {
_state.value = ClockState(whiteLeft, blackLeft, running, flagged)
}
fun start(side: PieceColor) {
tickJob?.cancel()
account()
running = side
publish()
if (unlimited) return
tickJob = scope.launch {
while (isActive) {
val current = running ?: break
val left = if (current == PieceColor.WHITE) whiteLeft else blackLeft
val sinceAccount = (System.nanoTime() - lastAccountedNs) / 1_000_000L
delay(((left - sinceAccount) % 1000L + 1L).coerceIn(1L, 1000L))
account()
val nowLeft = if (current == PieceColor.WHITE) whiteLeft else blackLeft
if (nowLeft <= 0L) {
flagged = current
publish()
break
}
publish()
}
}
}
fun onMoveMade(movedSide: PieceColor) {
account()
val incMs = timeControl.incrementSeconds * 1000L
if (!unlimited) {
if (movedSide == PieceColor.WHITE) whiteLeft += incMs else blackLeft += incMs
}
start(if (movedSide == PieceColor.WHITE) PieceColor.BLACK else PieceColor.WHITE)
}
fun applyRemoteTime(side: PieceColor, remainingMillis: Long) {
if (remainingMillis < 0 || unlimited) return
account()
if (flagged == side && remainingMillis > 0) {
// The opponent's move arrived in time after all: unflag and keep the clock ticking.
flagged = null
if (side == PieceColor.WHITE) whiteLeft = remainingMillis else blackLeft = remainingMillis
lastAccountedNs = System.nanoTime()
val current = running
if (current != null) start(current) else publish()
return
}
val local = if (side == PieceColor.WHITE) whiteLeft else blackLeft
if (kotlin.math.abs(local - remainingMillis) < SYNC_THRESHOLD_MS) return
if (side == PieceColor.WHITE) whiteLeft = remainingMillis else blackLeft = remainingMillis
publish()
}
fun restore(whiteMillis: Long, blackMillis: Long) {
if (unlimited) return
account()
if (whiteMillis >= 0) whiteLeft = whiteMillis
if (blackMillis >= 0) blackLeft = blackMillis
publish()
}
fun pause() {
tickJob?.cancel()
account()
running = null
publish()
}
fun stop() {
tickJob?.cancel()
account()
running = null
publish()
}
private companion object {
const val SYNC_THRESHOLD_MS = 1_500L
}
}