From fd6d05d568a94f8159c0e590529e25704768c824 Mon Sep 17 00:00:00 2001 From: sergeych Date: Fri, 3 Apr 2026 11:55:25 +0300 Subject: [PATCH] anotther compiler inference bug andlyng.io.console improvements --- examples/tetris_console.lyng | 53 +++--- .../lyng/io/console/LyngConsoleModule.kt | 78 +++++--- .../sergeych/lyngio/console/JvmLyngConsole.kt | 175 +++++++++--------- .../lyng/io/console/LyngConsoleModuleTest.kt | 65 ++++++- .../console/JvmConsoleKeyDecoderTest.kt | 59 ++++++ .../kotlin/net/sergeych/lyng/Compiler.kt | 20 ++ .../InferredCtorFieldIntRegressionTest.kt | 69 +++++++ 7 files changed, 389 insertions(+), 130 deletions(-) create mode 100644 lyngio/src/jvmTest/kotlin/net/sergeych/lyngio/console/JvmConsoleKeyDecoderTest.kt create mode 100644 lynglib/src/commonTest/kotlin/InferredCtorFieldIntRegressionTest.kt diff --git a/examples/tetris_console.lyng b/examples/tetris_console.lyng index 17e1567..86f59a3 100755 --- a/examples/tetris_console.lyng +++ b/examples/tetris_console.lyng @@ -80,6 +80,30 @@ class GameState( var paused = false } class LoopFrame(val resized: Bool, val originRow: Int, val originCol: Int) {} +class InputBuffer { + private val mutex: Mutex = Mutex() + private val items: List = [] + + fun push(value: String): Void { + mutex.withLock { + if (items.size >= MAX_PENDING_INPUTS) { + items.removeAt(0) + } + items.add(value) + } + } + + fun drain(): List { + val out: List = [] + mutex.withLock { + while (items.size > 0) { + out.add(items[0]) + items.removeAt(0) + } + } + out + } +} fun clearAndHome() { Console.clear() @@ -540,9 +564,8 @@ if (!Console.isSupported()) { ) var prevFrameLines: List = [] - val gameMutex: Mutex = Mutex() var forceRedraw = false - val pendingInputs: List = [] + val inputBuffer: InputBuffer = InputBuffer() val rawModeEnabled = Console.setRawMode(true) if (!rawModeEnabled) { @@ -642,13 +665,7 @@ if (!Console.isSupported()) { continue } val mapped = if (ctrl && (key == "c" || key == "C")) "__CTRL_C__" else key - val mm: Mutex = gameMutex - mm.withLock { - if (pendingInputs.size >= MAX_PENDING_INPUTS) { - pendingInputs.removeAt(0) - } - pendingInputs.add(mapped) - } + inputBuffer.push(mapped) } } } catch (eventErr: Object) { @@ -745,19 +762,11 @@ if (!Console.isSupported()) { continue } - val mm: Mutex = gameMutex - mm.withLock { - if (pendingInputs.size > 0) { - val toApply: List = [] - 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 - } + val toApply = inputBuffer.drain() + if (toApply.size > 0) { + for (k in toApply) { + applyKeyInput(state, k) + if (!state.running || state.gameOver) break } } if (!state.running || state.gameOver) { diff --git a/lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/console/LyngConsoleModule.kt b/lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/console/LyngConsoleModule.kt index 64a42b0..2ae8212 100644 --- a/lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/console/LyngConsoleModule.kt +++ b/lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/console/LyngConsoleModule.kt @@ -17,6 +17,7 @@ package net.sergeych.lyng.io.console +import kotlinx.coroutines.delay import net.sergeych.lyng.ModuleScope import net.sergeych.lyng.Scope import net.sergeych.lyng.ScopeFacade @@ -58,18 +59,31 @@ fun createConsoleModule(policy: ConsoleAccessPolicy, manager: ImportManager): Bo if (manager.packageNames.contains(CONSOLE_MODULE_NAME)) return false manager.addPackage(CONSOLE_MODULE_NAME) { module -> - buildConsoleModule(module, policy) + buildConsoleModule(module, policy, getSystemConsole()) } return true } fun createConsole(policy: ConsoleAccessPolicy, manager: ImportManager): Boolean = createConsoleModule(policy, manager) -private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAccessPolicy) { +internal fun createConsoleModule( + policy: ConsoleAccessPolicy, + manager: ImportManager, + console: LyngConsole, +): Boolean { + if (manager.packageNames.contains(CONSOLE_MODULE_NAME)) return false + + manager.addPackage(CONSOLE_MODULE_NAME) { module -> + buildConsoleModule(module, policy, console) + } + return true +} + +private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAccessPolicy, baseConsole: LyngConsole) { // Load Lyng declarations for console enums/types first (module-local source of truth). module.eval(Source(CONSOLE_MODULE_NAME, consoleLyng)) ConsoleEnums.initialize(module) - val console: LyngConsole = LyngConsoleSecured(getSystemConsole(), policy) + val console: LyngConsole = LyngConsoleSecured(baseConsole, policy) val consoleType = object : net.sergeych.lyng.obj.ObjClass("Console") {} @@ -179,7 +193,7 @@ private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAcces addClassFn("events") { consoleGuard { - console.events().toConsoleEventStream() + ObjConsoleEventStream { console.events() } } } @@ -210,12 +224,8 @@ private suspend inline fun ScopeFacade.consoleGuard(crossinline block: suspend ( } } -private fun ConsoleEventSource.toConsoleEventStream(): ObjConsoleEventStream { - return ObjConsoleEventStream(this) -} - private class ObjConsoleEventStream( - private val source: ConsoleEventSource, + private val sourceFactory: () -> ConsoleEventSource, ) : Obj() { override val objClass: net.sergeych.lyng.obj.ObjClass get() = type @@ -224,35 +234,61 @@ private class ObjConsoleEventStream( val type = net.sergeych.lyng.obj.ObjClass("ConsoleEventStream", ObjIterable).apply { addFn("iterator") { val stream = thisAs() - ObjConsoleEventIterator(stream.source) + ObjConsoleEventIterator(stream.sourceFactory) } } } } private class ObjConsoleEventIterator( - private val source: ConsoleEventSource, + private val sourceFactory: () -> ConsoleEventSource, ) : Obj() { private var cached: Obj? = null private var closed = false + private var source: ConsoleEventSource? = null override val objClass: net.sergeych.lyng.obj.ObjClass get() = type + private fun ensureSource(): ConsoleEventSource { + val current = source + if (current != null) return current + return sourceFactory().also { source = it } + } + + private suspend fun recycleSource(reason: String, error: Throwable? = null) { + if (error != null) { + consoleFlowDebug(reason, error) + } else { + consoleFlowDebug(reason) + } + val current = source + source = null + runCatching { current?.close() } + .onFailure { consoleFlowDebug("console-bridge: failed to close recycled source", it) } + if (!closed) delay(25) + } + private suspend fun ensureCached(): Boolean { if (closed) return false if (cached != null) return true while (!closed && cached == null) { - val event = try { - source.nextEvent() + val currentSource = try { + ensureSource() } catch (e: Throwable) { - // Consumer loops must survive source/read failures: report and keep polling. - consoleFlowDebug("console-bridge: nextEvent failed; dropping failure and continuing", e) + recycleSource("console-bridge: source creation failed; retrying", e) + continue + } + val event = try { + currentSource.nextEvent() + } catch (e: Throwable) { + // Consumer loops must survive source/read failures: rebuild the source and keep polling. + recycleSource("console-bridge: nextEvent failed; recycling source", e) continue } if (event == null) { - closeSource() - return false + recycleSource("console-bridge: source ended; recreating") + continue } cached = try { event.toObjEvent() @@ -268,10 +304,10 @@ private class ObjConsoleEventIterator( private suspend fun closeSource() { if (closed) return closed = true - // Do not close the underlying console source from VM iterator cancellation. - // CmdFrame.cancelIterators() may call cancelIteration() while user code is still - // expected to keep processing input (e.g. recover from app-level exceptions). - // The source lifecycle is managed by the console runtime. + val current = source + source = null + runCatching { current?.close() } + .onFailure { consoleFlowDebug("console-bridge: failed to close iterator source", it) } } suspend fun hasNext(): Boolean = ensureCached() diff --git a/lyngio/src/jvmMain/kotlin/net/sergeych/lyngio/console/JvmLyngConsole.kt b/lyngio/src/jvmMain/kotlin/net/sergeych/lyngio/console/JvmLyngConsole.kt index a6f0f8b..580c8d3 100644 --- a/lyngio/src/jvmMain/kotlin/net/sergeych/lyngio/console/JvmLyngConsole.kt +++ b/lyngio/src/jvmMain/kotlin/net/sergeych/lyngio/console/JvmLyngConsole.kt @@ -36,6 +36,97 @@ import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlin.time.TimeSource +internal object JvmConsoleKeyDecoder { + fun decode(firstCode: Int, nextCode: (Long) -> Int?): ConsoleEvent.KeyDown { + if (firstCode == 27) { + val next = nextCode(25L) + if (next == null || next < 0) { + return key("Escape") + } + if (next == '['.code || next == 'O'.code) { + val sb = StringBuilder() + sb.append(next.toChar()) + var i = 0 + while (i < 6) { + val c = nextCode(25L) ?: break + if (c < 0) break + sb.append(c.toChar()) + if (c.toChar().isLetter() || c == '~'.code) break + i += 1 + } + return keyFromAnsiSequence(sb.toString()) ?: key("Escape") + } + val base = decodePlainKey(next) + return ConsoleEvent.KeyDown( + key = base.key, + code = base.code, + ctrl = base.ctrl, + alt = true, + shift = base.shift, + meta = false, + ) + } + return decodePlainKey(firstCode) + } + + private fun decodePlainKey(code: Int): ConsoleEvent.KeyDown = when (code) { + 3 -> key("c", ctrl = true) + 9 -> key("Tab") + 10, 13 -> key("Enter") + 127, 8 -> key("Backspace") + 32 -> key(" ") + else -> { + if (code in 1..26) { + val ch = ('a'.code + code - 1).toChar().toString() + key(ch, ctrl = true) + } else { + val ch = code.toChar().toString() + key(ch, shift = ch.length == 1 && ch[0].isLetter() && ch[0].isUpperCase()) + } + } + } + + private fun keyFromAnsiSequence(seq: String): ConsoleEvent.KeyDown? { + val shift = seq.contains(";2") + val alt = seq.contains(";3") + val ctrl = seq.contains(";5") + val key = when { + seq.endsWith("A") -> "ArrowUp" + seq.endsWith("B") -> "ArrowDown" + seq.endsWith("C") -> "ArrowRight" + seq.endsWith("D") -> "ArrowLeft" + seq.endsWith("H") -> "Home" + seq.endsWith("F") -> "End" + seq.endsWith("~") -> when (seq.substringAfter('[').substringBefore(';').substringBefore('~')) { + "2" -> "Insert" + "3" -> "Delete" + "5" -> "PageUp" + "6" -> "PageDown" + else -> null + } + else -> null + } ?: return null + return key(key, ctrl = ctrl, alt = alt, shift = shift) + } + + private fun key( + value: String, + ctrl: Boolean = false, + alt: Boolean = false, + shift: Boolean = false, + ): ConsoleEvent.KeyDown { + require(value.isNotEmpty()) { "ConsoleEvent.KeyDown.key must never be empty" } + return ConsoleEvent.KeyDown( + key = value, + code = null, + ctrl = ctrl, + alt = alt, + shift = shift, + meta = false, + ) + } +} + /** * JVM console implementation: * - output/capabilities/input use a single JLine terminal instance @@ -508,40 +599,7 @@ object JvmLyngConsole : LyngConsole { val code = reader.read(120L) if (code == NonBlockingReader.READ_EXPIRED) return null if (code < 0) throw EOFException("non-blocking reader returned EOF") - return decodeKey(code) { timeout -> readNextCode(reader, timeout) } - } - - private fun decodeKey(code: Int, nextCode: (Long) -> Int?): ConsoleEvent.KeyDown { - if (code == 27) { - val next = nextCode(25L) - if (next == null || next < 0) { - return key("Escape") - } - if (next == '['.code || next == 'O'.code) { - val sb = StringBuilder() - sb.append(next.toChar()) - var i = 0 - while (i < 6) { - val c = nextCode(25L) ?: break - if (c < 0) break - sb.append(c.toChar()) - if (c.toChar().isLetter() || c == '~'.code) break - i += 1 - } - return keyFromAnsiSequence(sb.toString()) ?: key("Escape") - } - // Alt+key - val base = decodePlainKey(next) - return ConsoleEvent.KeyDown( - key = base.key, - code = base.code, - ctrl = base.ctrl, - alt = true, - shift = base.shift, - meta = false - ) - } - return decodePlainKey(code) + return JvmConsoleKeyDecoder.decode(code) { timeout -> readNextCode(reader, timeout) } } private fun readNextCode(reader: NonBlockingReader, timeoutMs: Long): Int? { @@ -550,53 +608,4 @@ object JvmLyngConsole : LyngConsole { if (c < 0) throw EOFException("non-blocking reader returned EOF while decoding key sequence") return c } - - - private fun decodePlainKey(code: Int): ConsoleEvent.KeyDown = when (code) { - 3 -> key("c", ctrl = true) - 9 -> key("Tab") - 10, 13 -> key("Enter") - 127, 8 -> key("Backspace") - 32 -> key(" ") - else -> { - if (code in 1..26) { - val ch = ('a'.code + code - 1).toChar().toString() - key(ch, ctrl = true) - } else { - val ch = code.toChar().toString() - key(ch, shift = ch.length == 1 && ch[0].isLetter() && ch[0].isUpperCase()) - } - } - } - - private fun keyFromAnsiSequence(seq: String): ConsoleEvent.KeyDown? = when (seq) { - "[A", "OA" -> key("ArrowUp") - "[B", "OB" -> key("ArrowDown") - "[C", "OC" -> key("ArrowRight") - "[D", "OD" -> key("ArrowLeft") - "[H", "OH" -> key("Home") - "[F", "OF" -> key("End") - "[2~" -> key("Insert") - "[3~" -> key("Delete") - "[5~" -> key("PageUp") - "[6~" -> key("PageDown") - else -> null - } - - private fun key( - value: String, - ctrl: Boolean = false, - alt: Boolean = false, - shift: Boolean = false, - ): ConsoleEvent.KeyDown { - require(value.isNotEmpty()) { "ConsoleEvent.KeyDown.key must never be empty" } - return ConsoleEvent.KeyDown( - key = value, - code = null, - ctrl = ctrl, - alt = alt, - shift = shift, - meta = false - ) - } } diff --git a/lyngio/src/jvmTest/kotlin/net/sergeych/lyng/io/console/LyngConsoleModuleTest.kt b/lyngio/src/jvmTest/kotlin/net/sergeych/lyng/io/console/LyngConsoleModuleTest.kt index de15122..163ebe7 100644 --- a/lyngio/src/jvmTest/kotlin/net/sergeych/lyng/io/console/LyngConsoleModuleTest.kt +++ b/lyngio/src/jvmTest/kotlin/net/sergeych/lyng/io/console/LyngConsoleModuleTest.kt @@ -22,16 +22,14 @@ import net.sergeych.lyng.ExecutionError import net.sergeych.lyng.Scope import net.sergeych.lyng.obj.ObjBool import net.sergeych.lyng.obj.ObjIllegalOperationException +import net.sergeych.lyngio.console.* import net.sergeych.lyngio.console.security.ConsoleAccessOp import net.sergeych.lyngio.console.security.ConsoleAccessPolicy import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy import net.sergeych.lyngio.fs.security.AccessContext import net.sergeych.lyngio.fs.security.AccessDecision import net.sergeych.lyngio.fs.security.Decision -import kotlin.test.Test -import kotlin.test.assertFalse -import kotlin.test.assertIs -import kotlin.test.assertTrue +import kotlin.test.* class LyngConsoleModuleTest { @@ -112,4 +110,63 @@ class LyngConsoleModuleTest { assertIs(error.errorObject) } } + + @Test + fun eventsIteratorRecoversAfterSourceFailure() = runBlocking { + val scope = newScope() + var eventsCalls = 0 + val console = object : LyngConsole { + override val isSupported: Boolean = true + + override suspend fun isTty(): Boolean = true + + override suspend fun geometry(): ConsoleGeometry? = null + + override suspend fun ansiLevel(): ConsoleAnsiLevel = ConsoleAnsiLevel.NONE + + override suspend fun write(text: String) {} + + override suspend fun flush() {} + + override fun events(): ConsoleEventSource { + eventsCalls += 1 + val callNo = eventsCalls + return object : ConsoleEventSource { + private var emitted = false + + override suspend fun nextEvent(timeoutMs: Long): ConsoleEvent? { + if (emitted) return null + emitted = true + return when (callNo) { + 1 -> throw IllegalStateException("synthetic source failure") + else -> ConsoleEvent.KeyDown(key = "x") + } + } + + override suspend fun close() {} + } + } + + override suspend fun setRawMode(enabled: Boolean): Boolean = enabled + } + + assertTrue(createConsoleModule(PermitAllConsoleAccessPolicy, scope.importManager, console)) + + val result = scope.eval( + """ + import lyng.io.console + + val it = Console.events().iterator() + assert(it.hasNext()) + val ev = it.next() + assert(ev is ConsoleKeyEvent) + assert((ev as ConsoleKeyEvent).key == "x") + true + """.trimIndent() + ) + + assertIs(result) + assertTrue(result.value) + assertEquals(2, eventsCalls) + } } diff --git a/lyngio/src/jvmTest/kotlin/net/sergeych/lyngio/console/JvmConsoleKeyDecoderTest.kt b/lyngio/src/jvmTest/kotlin/net/sergeych/lyngio/console/JvmConsoleKeyDecoderTest.kt new file mode 100644 index 0000000..5654038 --- /dev/null +++ b/lyngio/src/jvmTest/kotlin/net/sergeych/lyngio/console/JvmConsoleKeyDecoderTest.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package net.sergeych.lyngio.console + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class JvmConsoleKeyDecoderTest { + + private fun decode(vararg bytes: Int): ConsoleEvent.KeyDown { + var index = 1 + return JvmConsoleKeyDecoder.decode(bytes[0]) { _ -> + if (index >= bytes.size) null else bytes[index++] + } + } + + @Test + fun decodesArrowLeft() { + val ev = decode(27, '['.code, 'D'.code) + assertEquals("ArrowLeft", ev.key) + assertFalse(ev.ctrl) + assertFalse(ev.alt) + assertFalse(ev.shift) + } + + @Test + fun decodesCtrlArrowRightModifier() { + val ev = decode(27, '['.code, '1'.code, ';'.code, '5'.code, 'C'.code) + assertEquals("ArrowRight", ev.key) + assertTrue(ev.ctrl) + assertFalse(ev.alt) + assertFalse(ev.shift) + } + + @Test + fun decodesAltCharacter() { + val ev = decode(27, 'x'.code) + assertEquals("x", ev.key) + assertTrue(ev.alt) + assertFalse(ev.ctrl) + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index e0df2ea..c7aa6e2 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -4674,6 +4674,7 @@ class Compiler( ?: nameObjClass[ref.name] ?: resolveClassByName(ref.name) } + is ImplicitThisMemberRef -> resolveReceiverClassForMember(ref) is ClassScopeMemberRef -> { val targetClass = resolveClassByName(ref.ownerClassName()) ?: return null inferFieldReturnClass(targetClass, ref.name) @@ -4728,6 +4729,15 @@ class Compiler( is FastLocalVarRef -> nameTypeDecl[ref.name] ?: lookupLocalTypeDeclByName(ref.name) ?: seedTypeDeclByName(ref.name) + is ImplicitThisMemberRef -> { + val typeName = ref.preferredThisTypeName() ?: currentImplicitThisTypeName() + val targetClass = typeName?.let { resolveClassByName(it) } ?: return null + targetClass.getInstanceMemberOrNull(ref.name, includeAbstract = true)?.typeDecl?.let { return it } + classFieldTypesByName[targetClass.className]?.get(ref.name) + ?.let { return TypeDecl.Simple(it.className, false) } + classMethodReturnTypeDeclByName[targetClass.className]?.get(ref.name)?.let { return it } + null + } is FieldRef -> { val targetDecl = resolveReceiverTypeDecl(ref.target) ?: return null val targetClass = resolveTypeDeclObjClass(targetDecl) ?: resolveReceiverClassForMember(ref.target) @@ -7388,6 +7398,15 @@ class Compiler( val mutable = param.accessType?.isMutable ?: false declareSlotNameIn(classSlotPlan, param.name, mutable, isDelegated = false) } + constructorArgsDeclaration?.let { ctorDecl -> + val classParamTypeMap = slotTypeByScopeId.getOrPut(classSlotPlan.id) { mutableMapOf() } + val classParamTypeDeclMap = slotTypeDeclByScopeId.getOrPut(classSlotPlan.id) { mutableMapOf() } + for (param in ctorDecl.params) { + val slot = classSlotPlan.slots[param.name]?.index ?: continue + classParamTypeDeclMap[slot] = param.type + resolveTypeDeclObjClass(param.type)?.let { classParamTypeMap[slot] = it } + } + } val ctorForcedLocalSlots = LinkedHashMap() if (constructorArgsDeclaration != null) { val ctorDecl = constructorArgsDeclaration @@ -8873,6 +8892,7 @@ class Compiler( val targetClass = resolveReceiverClassForMember(directRef.target) inferFieldReturnClass(targetClass, directRef.name) } + is ImplicitThisMemberRef -> resolveReceiverClassForMember(directRef) is CallRef -> { val target = directRef.target when { diff --git a/lynglib/src/commonTest/kotlin/InferredCtorFieldIntRegressionTest.kt b/lynglib/src/commonTest/kotlin/InferredCtorFieldIntRegressionTest.kt new file mode 100644 index 0000000..06aff6c --- /dev/null +++ b/lynglib/src/commonTest/kotlin/InferredCtorFieldIntRegressionTest.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import kotlinx.coroutines.test.runTest +import net.sergeych.lyng.eval +import kotlin.test.Test + +class InferredCtorFieldIntRegressionTest { + + @Test + fun inferredIntFieldsFromCtorParamsStayIntInsideTypedCalls() = runTest { + eval( + """ + class GameState( + pieceId0: Int, + nextId0: Int, + next2Id0: Int, + px0: Int, + py0: Int, + ) { + var pieceId = pieceId0 + var nextId = nextId0 + var next2Id = next2Id0 + var rot = 0 + var px = px0 + var py = py0 + var score = 0 + var totalLines = 0 + var level = 1 + var running = true + var gameOver = false + var paused = false + } + + fun canPlace(board: List>, boardW: Int, boardH: Int, pieceId: Int, rot: Int, px: Int, py: Int): Bool { + true + } + + val board: List> = [] + val boardW = 10 + val boardH = 20 + + fun applyKeyInput(s: GameState, key: String) { + if (key == "ArrowLeft") { + if (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px - 1, s.py) == true) s.px-- + } + } + + val s = GameState(1, 2, 3, 4, 5) + applyKeyInput(s, "ArrowLeft") + assertEquals(3, s.px) + """.trimIndent() + ) + } +}