package coffee.liz.rookery.timer import coffee.liz.rookery.model.Penalty /** * The spacebar/touch timer as a pure state machine. The UI feeds it press/release/tick with a * monotonic clock and renders [state]; nothing in here touches real time. */ class TimerMachine(var inspection: Boolean = true, var holdMs: Long = 300) { sealed interface State { data object Idle : State data class Inspecting(val since: Long) : State /** Finger down, waiting for the hold time. [inspectingSince] carries over inspection. */ data class Holding(val since: Long, val inspectingSince: Long?) : State data class Ready(val inspectingSince: Long?) : State data class Running(val since: Long, val penalty: Penalty) : State data class Stopped(val ms: Long, val penalty: Penalty, val awaitingRelease: Boolean) : State } var state: State = State.Idle private set /** Fired once per finished solve. */ var onFinish: (ms: Long, penalty: Penalty) -> Unit = { _, _ -> } fun press(now: Long) { state = when (val s = state) { State.Idle -> State.Holding(now, null) is State.Stopped -> if (s.awaitingRelease) s else State.Holding(now, null) is State.Inspecting -> State.Holding(now, s.since) is State.Running -> { val ms = now - s.since onFinish(ms, s.penalty) State.Stopped(ms, s.penalty, awaitingRelease = true) } is State.Holding, is State.Ready -> s } } fun release(now: Long) { state = when (val s = state) { is State.Holding -> when { s.inspectingSince != null -> State.Inspecting(s.inspectingSince) // let go early: keep inspecting inspection -> State.Inspecting(now) else -> State.Idle } is State.Ready -> State.Running(now, inspectionPenalty(s.inspectingSince, now)) is State.Stopped -> s.copy(awaitingRelease = false) else -> s } } fun tick(now: Long) { val s = state if (s is State.Holding && now - s.since >= holdMs && (s.inspectingSince != null || !inspection)) { state = State.Ready(s.inspectingSince) } if (s is State.Inspecting && now - s.since > INSPECTION_DNF_MS) { onFinish(0, Penalty.DNF) state = State.Stopped(0, Penalty.DNF, awaitingRelease = false) } } fun cancel() { state = State.Idle } /** Start inspection without a key press (a smart cube has just been scrambled). */ fun beginInspection(now: Long) { if (state is State.Idle || state is State.Stopped) state = State.Inspecting(now) } /** Start the clock without a key press (a smart cube made its first move, or blind memo began). */ fun beginRunning(now: Long) { state = when (val s = state) { is State.Inspecting -> State.Running(now, inspectionPenalty(s.since, now)) State.Idle, is State.Stopped -> State.Running(now, Penalty.NONE) else -> return } } /** Stop the clock without a key press (the smart cube is solved). */ fun finish(now: Long) { val s = state as? State.Running ?: return val ms = now - s.since state = State.Stopped(ms, s.penalty, awaitingRelease = false) onFinish(ms, s.penalty) } private fun inspectionPenalty(since: Long?, now: Long): Penalty { if (since == null) return Penalty.NONE val t = now - since return when { t > INSPECTION_DNF_MS -> Penalty.DNF t > INSPECTION_MS -> Penalty.PLUS2 else -> Penalty.NONE } } companion object { const val INSPECTION_MS = 15_000L const val INSPECTION_DNF_MS = 17_000L } }