1.5.0 release; console stability fix; tetris sample rewritten to be RC-free
This commit is contained in:
parent
222a653040
commit
d17ad9ef0d
@ -10,6 +10,14 @@
|
||||
* - Space: hard drop
|
||||
* - P or Escape: pause
|
||||
* - Q: quit
|
||||
|
||||
Tsted to score:
|
||||
sergeych@sergeych-XPS-17-9720:~$ ~/dev/lyng/examples/tetris_console.lyng
|
||||
Bye.
|
||||
Score: 435480
|
||||
Lines: 271
|
||||
Level: 28
|
||||
Ssergeych@sergeych-XPS-17-9720:~$
|
||||
*/
|
||||
|
||||
import lyng.io.console
|
||||
@ -28,6 +36,7 @@ val DROP_FRAMES_BASE = 15
|
||||
val DROP_FRAMES_MIN = 3
|
||||
val FRAME_DELAY_MS = 35
|
||||
val RESIZE_WAIT_MS = 250
|
||||
val MAX_PENDING_INPUTS = 64
|
||||
val ROTATION_KICKS = [0, -1, 1, -2, 2]
|
||||
val ANSI_ESC = "\u001b["
|
||||
val ANSI_RESET = ANSI_ESC + "0m"
|
||||
@ -78,7 +87,14 @@ fun clearAndHome() {
|
||||
|
||||
fun logError(message: String, err: Object?): Void {
|
||||
try {
|
||||
val details = if (err == null) "" else ": " + err
|
||||
var details = ""
|
||||
if (err != null) {
|
||||
try {
|
||||
details = ": " + err
|
||||
} catch (_: Object) {
|
||||
details = ": <error-format-failed>"
|
||||
}
|
||||
}
|
||||
Path(ERROR_LOG_PATH).appendUtf8(message + details + "\n")
|
||||
} catch (_: Object) {
|
||||
// Never let logging errors affect gameplay.
|
||||
@ -136,6 +152,7 @@ fun emptyCellText(useColor: Bool): String {
|
||||
}
|
||||
|
||||
fun canPlace(board: Board, boardW: Int, boardH: Int, pieceId: Int, rot: Int, px: Int, py: Int): Bool {
|
||||
try {
|
||||
if (pieceId < 1 || pieceId > 7) return false
|
||||
val piece: Piece = PIECES[pieceId - 1]
|
||||
if (rot < 0 || rot >= piece.rotations.size) return false
|
||||
@ -149,11 +166,16 @@ fun canPlace(board: Board, boardW: Int, boardH: Int, pieceId: Int, rot: Int, px:
|
||||
if (y >= boardH) return false
|
||||
|
||||
if (y >= 0) {
|
||||
if (y >= board.size) return false
|
||||
val row = board[y]
|
||||
if (row == null) return false
|
||||
if (row[x] != 0) return false
|
||||
}
|
||||
}
|
||||
true
|
||||
} catch (_: Object) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun lockPiece(board: Board, pieceId: Int, rot: Int, px: Int, py: Int): Void {
|
||||
@ -164,12 +186,14 @@ fun lockPiece(board: Board, pieceId: Int, rot: Int, px: Int, py: Int): Void {
|
||||
val x = px + cell[0]
|
||||
val y = py + cell[1]
|
||||
|
||||
if (y >= 0) {
|
||||
if (y >= 0 && y < board.size) {
|
||||
val row = board[y]
|
||||
if (row != null) {
|
||||
row[x] = pieceId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCompletedLines(board: Board, boardW: Int, boardH: Int): Int {
|
||||
val b = board
|
||||
@ -177,7 +201,17 @@ fun clearCompletedLines(board: Board, boardW: Int, boardH: Int): Int {
|
||||
var cleared = 0
|
||||
|
||||
while (y >= 0) {
|
||||
if (y >= b.size) {
|
||||
y--
|
||||
continue
|
||||
}
|
||||
val row = b[y]
|
||||
if (row == null) {
|
||||
b.removeAt(y)
|
||||
b.insertAt(0, emptyRow(boardW))
|
||||
cleared++
|
||||
continue
|
||||
}
|
||||
var full = true
|
||||
for (x in 0..<boardW) {
|
||||
if (row[x] == 0) {
|
||||
@ -298,8 +332,8 @@ fun render(
|
||||
|
||||
for (x in 0..<boardW) {
|
||||
val a = activeCellId(state.pieceId, state.rot, state.px, state.py, x, y)
|
||||
val row = board[y]
|
||||
val b = row[x]
|
||||
val row = if (y < board.size) board[y] else null
|
||||
val b = if (row == null) 0 else row[x]
|
||||
val id = if (a > 0) a else b
|
||||
line += if (id > 0) blockText(id, useColor) else emptyCellText(useColor)
|
||||
}
|
||||
@ -507,8 +541,9 @@ if (!Console.isSupported()) {
|
||||
)
|
||||
var prevFrameLines: List<String> = []
|
||||
|
||||
val gameMutex = Mutex()
|
||||
var hasResizeEvent = false
|
||||
val gameMutex: Mutex = Mutex()
|
||||
var forceRedraw = false
|
||||
val pendingInputs: List<String> = []
|
||||
|
||||
val rawModeEnabled = Console.setRawMode(true)
|
||||
if (!rawModeEnabled) {
|
||||
@ -538,12 +573,12 @@ if (!Console.isSupported()) {
|
||||
s.running = false
|
||||
} else {
|
||||
s.paused = false
|
||||
prevFrameLines = []
|
||||
forceRedraw = true
|
||||
}
|
||||
}
|
||||
else if (key == "p" || key == "P" || key == "Escape") {
|
||||
s.paused = true
|
||||
prevFrameLines = []
|
||||
forceRedraw = true
|
||||
}
|
||||
else if (key == "q" || key == "Q") {
|
||||
s.running = false
|
||||
@ -607,15 +642,14 @@ if (!Console.isSupported()) {
|
||||
logError("Dropped key event with empty/null key", null)
|
||||
continue
|
||||
}
|
||||
gameMutex.withLock {
|
||||
val mapped = if (ctrl && (key == "c" || key == "C")) "__CTRL_C__" else key
|
||||
applyKeyInput(state, mapped)
|
||||
val mm: Mutex = gameMutex
|
||||
mm.withLock {
|
||||
if (pendingInputs.size >= MAX_PENDING_INPUTS) {
|
||||
pendingInputs.removeAt(0)
|
||||
}
|
||||
pendingInputs.add(mapped)
|
||||
}
|
||||
} else if (ev is ConsoleResizeEvent) {
|
||||
gameMutex.withLock {
|
||||
hasResizeEvent = true
|
||||
prevFrameLines = []
|
||||
}
|
||||
}
|
||||
} catch (eventErr: Object) {
|
||||
@ -644,12 +678,6 @@ if (!Console.isSupported()) {
|
||||
return null
|
||||
}
|
||||
|
||||
var resized = false
|
||||
gameMutex.withLock {
|
||||
resized = hasResizeEvent
|
||||
hasResizeEvent = false
|
||||
}
|
||||
|
||||
val contentCols = boardW * 2 + 2 + 3 + PANEL_WIDTH
|
||||
val contentRows = boardH + 1
|
||||
val requiredCols = max(MIN_COLS, contentCols)
|
||||
@ -663,7 +691,7 @@ if (!Console.isSupported()) {
|
||||
|
||||
val originCol = max(1, ((c - contentCols) / 2) + 1)
|
||||
val originRow = max(1, ((r - contentRows) / 2) + 1)
|
||||
LoopFrame(resized, originRow, originCol)
|
||||
LoopFrame(false, originRow, originCol)
|
||||
}
|
||||
|
||||
fun advanceGravity(s: GameState, frame: Int): Int {
|
||||
@ -709,19 +737,37 @@ if (!Console.isSupported()) {
|
||||
var shouldStop = false
|
||||
var prevOriginRow = -1
|
||||
var prevOriginCol = -1
|
||||
var prevPaused = false
|
||||
while (!shouldStop) {
|
||||
val frameData = pollLoopFrame()
|
||||
if (frameData == null) {
|
||||
frame = 0
|
||||
prevPaused = false
|
||||
continue
|
||||
}
|
||||
|
||||
gameMutex.withLock {
|
||||
val mm: Mutex = gameMutex
|
||||
mm.withLock {
|
||||
if (pendingInputs.size > 0) {
|
||||
val toApply: List<String> = []
|
||||
while (pendingInputs.size > 0) {
|
||||
val k = pendingInputs[0]
|
||||
pendingInputs.removeAt(0)
|
||||
toApply.add(k)
|
||||
}
|
||||
for (k in toApply) {
|
||||
applyKeyInput(state, k)
|
||||
if (!state.running || state.gameOver) break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!state.running || state.gameOver) {
|
||||
shouldStop = true
|
||||
} else {
|
||||
val localForceRedraw = forceRedraw
|
||||
forceRedraw = false
|
||||
val movedOrigin = frameData.originRow != prevOriginRow || frameData.originCol != prevOriginCol
|
||||
if (frameData.resized || movedOrigin) {
|
||||
if (frameData.resized || movedOrigin || localForceRedraw) {
|
||||
clearAndHome()
|
||||
prevFrameLines = []
|
||||
}
|
||||
@ -740,10 +786,10 @@ if (!Console.isSupported()) {
|
||||
frameData.originCol,
|
||||
useColor
|
||||
)
|
||||
if (state.paused) {
|
||||
if (state.paused && (!prevPaused || frameData.resized || movedOrigin)) {
|
||||
renderPauseOverlay(frameData.originRow, frameData.originCol, boardW, boardH)
|
||||
}
|
||||
}
|
||||
prevPaused = state.paused
|
||||
}
|
||||
Console.flush()
|
||||
delay(FRAME_DELAY_MS)
|
||||
|
||||
@ -37,10 +37,7 @@ import net.sergeych.lyng.obj.toObj
|
||||
import net.sergeych.lyng.pacman.ImportManager
|
||||
import net.sergeych.lyng.raiseIllegalOperation
|
||||
import net.sergeych.lyng.requireScope
|
||||
import net.sergeych.lyngio.console.ConsoleEvent
|
||||
import net.sergeych.lyngio.console.ConsoleEventSource
|
||||
import net.sergeych.lyngio.console.LyngConsole
|
||||
import net.sergeych.lyngio.console.getSystemConsole
|
||||
import net.sergeych.lyngio.console.*
|
||||
import net.sergeych.lyngio.console.security.ConsoleAccessDeniedException
|
||||
import net.sergeych.lyngio.console.security.ConsoleAccessPolicy
|
||||
import net.sergeych.lyngio.console.security.LyngConsoleSecured
|
||||
@ -245,13 +242,27 @@ private class ObjConsoleEventIterator(
|
||||
private suspend fun ensureCached(): Boolean {
|
||||
if (closed) return false
|
||||
if (cached != null) return true
|
||||
val event = source.nextEvent()
|
||||
while (!closed && cached == null) {
|
||||
val event = try {
|
||||
source.nextEvent()
|
||||
} catch (e: Throwable) {
|
||||
// Consumer loops must survive source/read failures: report and keep polling.
|
||||
consoleFlowDebug("console-bridge: nextEvent failed; dropping failure and continuing", e)
|
||||
continue
|
||||
}
|
||||
if (event == null) {
|
||||
closeSource()
|
||||
return false
|
||||
}
|
||||
cached = event.toObjEvent()
|
||||
return true
|
||||
cached = try {
|
||||
event.toObjEvent()
|
||||
} catch (e: Throwable) {
|
||||
// Malformed/native event payload must not terminate consumer iteration.
|
||||
consoleFlowDebug("console-bridge: malformed event dropped: $event", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
return cached != null
|
||||
}
|
||||
|
||||
private suspend fun closeSource() {
|
||||
@ -292,8 +303,14 @@ private class ObjConsoleEventIterator(
|
||||
|
||||
private fun ConsoleEvent.toObjEvent(): Obj = when (this) {
|
||||
is ConsoleEvent.Resize -> ObjConsoleResizeEvent(columns, rows)
|
||||
is ConsoleEvent.KeyDown -> ObjConsoleKeyEvent(type = ConsoleEnums.KEY_DOWN, key = key, codeName = code, ctrl = ctrl, alt = alt, shift = shift, meta = meta)
|
||||
is ConsoleEvent.KeyUp -> ObjConsoleKeyEvent(type = ConsoleEnums.KEY_UP, key = key, codeName = code, ctrl = ctrl, alt = alt, shift = shift, meta = meta)
|
||||
is ConsoleEvent.KeyDown -> ObjConsoleKeyEvent(type = ConsoleEnums.KEY_DOWN, key = sanitizedKeyOrFallback(key), codeName = code, ctrl = ctrl, alt = alt, shift = shift, meta = meta)
|
||||
is ConsoleEvent.KeyUp -> ObjConsoleKeyEvent(type = ConsoleEnums.KEY_UP, key = sanitizedKeyOrFallback(key), codeName = code, ctrl = ctrl, alt = alt, shift = shift, meta = meta)
|
||||
}
|
||||
|
||||
private fun sanitizedKeyOrFallback(key: String): String {
|
||||
if (key.isNotEmpty()) return key
|
||||
consoleFlowDebug("console-bridge: empty key value received; using fallback key name")
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
private object ConsoleEnums {
|
||||
|
||||
@ -100,8 +100,9 @@ object MordantLyngConsole : LyngConsole {
|
||||
var running = true
|
||||
|
||||
globalLaunch {
|
||||
var lastWidth = t.updateSize().width
|
||||
var lastHeight = t.updateSize().height
|
||||
val initialSize = runCatching { t.updateSize() }.getOrNull()
|
||||
var lastWidth = initialSize?.width ?: 0
|
||||
var lastHeight = initialSize?.height ?: 0
|
||||
val startMark = TimeSource.Monotonic.markNow()
|
||||
var lastHeartbeatMark = startMark
|
||||
var loops = 0L
|
||||
@ -113,6 +114,18 @@ object MordantLyngConsole : LyngConsole {
|
||||
var lastKeyMark = startMark
|
||||
var lastRawRecoveryMark = startMark
|
||||
|
||||
fun tryEmitResize(width: Int, height: Int) {
|
||||
if (width < 1 || height < 1) {
|
||||
consoleFlowDebug("events: ignored invalid resize width=$width height=$height")
|
||||
return
|
||||
}
|
||||
if (width == lastWidth && height == lastHeight) return
|
||||
out.trySend(ConsoleEvent.Resize(width, height))
|
||||
lastWidth = width
|
||||
lastHeight = height
|
||||
resizeEvents += 1
|
||||
}
|
||||
|
||||
consoleFlowDebug("events: collector started")
|
||||
try {
|
||||
while (currentCoroutineContext().isActive && sourceState.withLock { running }) {
|
||||
@ -122,12 +135,7 @@ object MordantLyngConsole : LyngConsole {
|
||||
delay(150)
|
||||
continue
|
||||
}
|
||||
if (currentSize.width != lastWidth || currentSize.height != lastHeight) {
|
||||
out.trySend(ConsoleEvent.Resize(currentSize.width, currentSize.height))
|
||||
lastWidth = currentSize.width
|
||||
lastHeight = currentSize.height
|
||||
resizeEvents += 1
|
||||
}
|
||||
tryEmitResize(currentSize.width, currentSize.height)
|
||||
|
||||
val raw = stateMutex.withLock {
|
||||
if (!rawModeRequested) {
|
||||
@ -173,10 +181,8 @@ object MordantLyngConsole : LyngConsole {
|
||||
val ev = readResult.getOrNull()
|
||||
|
||||
val resized = runCatching { t.updateSize() }.getOrNull()
|
||||
if (resized != null && (resized.width != lastWidth || resized.height != lastHeight)) {
|
||||
out.trySend(ConsoleEvent.Resize(resized.width, resized.height))
|
||||
lastWidth = resized.width
|
||||
lastHeight = resized.height
|
||||
if (resized != null) {
|
||||
tryEmitResize(resized.width, resized.height)
|
||||
}
|
||||
|
||||
when (ev) {
|
||||
|
||||
@ -215,10 +215,29 @@ object JvmLyngConsole : LyngConsole {
|
||||
var reader = activeTerm.reader()
|
||||
var keyThread: Thread? = null
|
||||
var heartbeatThread: Thread? = null
|
||||
val resizeEmitMutex = Any()
|
||||
var lastResizeCols = Int.MIN_VALUE
|
||||
var lastResizeRows = Int.MIN_VALUE
|
||||
|
||||
fun emitResize() {
|
||||
val size = runCatching { activeTerm.size }.getOrNull() ?: return
|
||||
out.trySend(ConsoleEvent.Resize(size.columns, size.rows))
|
||||
val cols = size.columns
|
||||
val rows = size.rows
|
||||
if (cols < 1 || rows < 1) {
|
||||
consoleFlowDebug("jline-events: ignored invalid resize columns=$cols rows=$rows")
|
||||
return
|
||||
}
|
||||
val shouldEmit = synchronized(resizeEmitMutex) {
|
||||
if (cols == lastResizeCols && rows == lastResizeRows) {
|
||||
false
|
||||
} else {
|
||||
lastResizeCols = cols
|
||||
lastResizeRows = rows
|
||||
true
|
||||
}
|
||||
}
|
||||
if (!shouldEmit) return
|
||||
out.trySend(ConsoleEvent.Resize(cols, rows))
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
|
||||
@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
group = "net.sergeych"
|
||||
version = "1.5.0-RC2"
|
||||
version = "1.5.0"
|
||||
|
||||
// Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 124 KiB |
BIN
site/src/jsMain/resources/tetris2.png
Normal file
BIN
site/src/jsMain/resources/tetris2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
Loading…
x
Reference in New Issue
Block a user