Skip to content

Commit

Permalink
Add wasm
Browse files Browse the repository at this point in the history
  • Loading branch information
Dmytro Marchuk committed Oct 2, 2024
1 parent 58aaf79 commit 55bbb42
Show file tree
Hide file tree
Showing 13 changed files with 192 additions and 38 deletions.
4 changes: 4 additions & 0 deletions common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
@file:OptIn(ExperimentalWasmDsl::class)
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl

plugins {
alias(libs.plugins.kotlinMultiplatform)
}

kotlin {
jvm()
js { browser() }
wasmJs { browser() }
}
2 changes: 1 addition & 1 deletion gui/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ korge {

targetJvm()
targetJs()
// targetWasmJs()
targetWasmJs()
serializationJson()
}

Expand Down
6 changes: 4 additions & 2 deletions gui/src/commonMain/kotlin/io/github/smaugfm/game2048/korge.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import korlibs.inject.Injector
import korlibs.korge.Korge
import korlibs.render.GameWindow

var usingWasm = false
var usingWasm: Boolean? = null

suspend fun startKorge(injector: Injector) {
Korge(
Expand All @@ -32,10 +32,12 @@ suspend fun startKorge(injector: Injector) {
}

suspend fun createInjector(): Injector {
val search = SearchImpl()
search.init()
val injector = Injector().apply {
mapInstance(Heuristics::class, Board4Heuristics())
mapInstance(BoardFactory::class, Board4)
mapSingleton(Search::class) { SearchImpl() }
mapSingleton(Search::class) { search }
GameState(this)
UIConstants(this, UIConstants.Resources.load())
History(this)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package io.github.smaugfm.game2048.search

expect class SearchImpl(log: Boolean = true) : Search {
override fun platformDepthLimit(distinctTiles: Int): Int
override suspend fun getExpectimaxResults(requests: List<SearchRequest>): List<SearchResult>
override fun combineStats(one: SearchStats, two: SearchStats): SearchStats

public override suspend fun init()
override fun platformDepthLimit(distinctTiles: Int): Int
override suspend fun getExpectimaxResults(requests: List<SearchRequest>): List<SearchResult>
override fun combineStats(one: SearchStats, two: SearchStats): SearchStats
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ import korlibs.image.color.Colors
import korlibs.image.text.TextAlignment
import korlibs.inject.Injector
import korlibs.io.async.ObservableProperty
import korlibs.korge.input.*
import korlibs.korge.input.keys
import korlibs.korge.input.onClickSuspend
import korlibs.korge.input.onDown
import korlibs.korge.input.onOut
import korlibs.korge.input.onOver
import korlibs.korge.input.onOverSuspend
import korlibs.korge.input.onUp
import korlibs.korge.view.Container
import korlibs.korge.view.RoundRect
import korlibs.korge.view.Text
Expand Down Expand Up @@ -99,7 +105,16 @@ class StaticUi(
addUnderBoardLabel(
gs.aiElapsedMs,
{
if (usingWasm && gs.showAiStats.value) "wasm" else ""
if (gs.showAiStats.value) {
if (usingWasm == true)
"WebAssembly implementation"
else if (usingWasm == false)
"JS implementation (use Chrome for speed-up)"
else
""
} else {
""
}
}
) {
alignRightToRightOf(uiBoard)
Expand Down Expand Up @@ -318,7 +333,7 @@ class StaticUi(
keys.down {
when (it.key) {
Key.ENTER, Key.SPACE -> handleTryAgainClick()
else -> Unit
else -> Unit
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,44 @@
package io.github.smaugfm.game2048.search

import io.github.smaugfm.game2048.board.Direction
import io.github.smaugfm.game2048.board.Direction.Companion.directions
import io.github.smaugfm.game2048.usingWasm
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeout
import org.w3c.dom.Worker
import kotlin.math.max

actual class SearchImpl actual constructor(log: Boolean) : Search() {
private val usingWasm = false
private val workers =
directions.associateWith {
Worker(
if (usingWasm)
"./wasm.js"
else
"./js.js"
)
private lateinit var workers: Map<Direction, Worker>

private fun loadWorkers(srcCode: String): Map<Direction, Worker> =
directions.associateWith { Worker(srcCode) }

public actual override suspend fun init() {
workers = loadWorkers("./wasmJs.js")

//Without this pingPongCheck's do not succeed for some reason
delay(100)

if (workers.values.all { it.pingPongCheck() }) {
usingWasm = true
consoleLogBold("Using WebAssembly Expectimax implementation")
} else {
usingWasm = false
consoleLogBold("Falling back to Javascript Expectimax implementation")
workers = loadWorkers("./js.js")
if (workers.values.any { !it.pingPongCheck() }) {
consoleLogBold("JS web worker did not load correctly")
}
}
}

actual override fun platformDepthLimit(distinctTiles: Int) =
distinctTiles - if (usingWasm) 3 else 6
distinctTiles - if (usingWasm == true) 3 else 6

actual override suspend fun getExpectimaxResults(requests: List<SearchRequest>): List<SearchResult> =
requests.map(::webWorkerSearch)
Expand All @@ -45,18 +63,51 @@ actual class SearchImpl actual constructor(log: Boolean) : Search() {
workers[req.dir]!!.computeScore(req.serialize())

companion object {
fun consoleLogBold(str: String) {
console.log(
"%c$str%c",
"font-weight: bold",
"font-weight: normal"
)
}

suspend fun Worker.pingPongCheck(): Boolean {
val res = CompletableDeferred<Boolean>()
onmessage = { e ->
if (e.data.toString() == "pong")
res.complete(true)
else {
console.log(e.data)
res.complete(false)
}
}
onerror = { e ->
console.error(e)
res.complete(false)
}
postMessage("ping")
return try {
withTimeout(50) {
res.await()
}
} catch (e: TimeoutCancellationException) {
console.log("Timed out waiting for 'pong' from wasm worker")
false
}
}

fun Worker.computeScore(data: String): Deferred<SearchResult?> {
val completableDeferred = CompletableDeferred<SearchResult?>()
this.onmessage = { messageEvent ->
val result = SearchResult.deserialize(messageEvent.data.toString())
completableDeferred.complete(result)
val res = CompletableDeferred<SearchResult?>()
onmessage = { e ->
val result = SearchResult.deserialize(e.data.toString())
res.complete(result)
}
this.onerror = { event ->
completableDeferred.completeExceptionally(RuntimeException(event.type))
onerror = { e ->
res.completeExceptionally(RuntimeException(e.type))
}
this.postMessage(data)
postMessage(data)

return completableDeferred
return res
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.github.smaugfm.game2048.util

import kotlinx.coroutines.delay

suspend fun checkWasmSupport(): Boolean {
while (true) {
try {
return js("hasWasmSupport").unsafeCast<Boolean>()
} catch (e: Throwable) {
delay(50)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ actual class SearchImpl actual constructor(log: Boolean) : Search(log) {
val result: CompletableDeferred<SearchResult?>,
)

public actual override suspend fun init() {
//do nothing
}

actual override fun platformDepthLimit(distinctTiles: Int) =
distinctTiles - 2

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.github.smaugfm.game2048.search

actual class SearchImpl actual constructor(log: Boolean) : Search() {
public actual override suspend fun init() {
throw UnsupportedOperationException()
}

actual override fun platformDepthLimit(distinctTiles: Int): Int {
throw UnsupportedOperationException()
}

actual override suspend fun getExpectimaxResults(requests: List<SearchRequest>): List<SearchResult> {
throw UnsupportedOperationException()
}

actual override fun combineStats(one: SearchStats, two: SearchStats): SearchStats {
throw UnsupportedOperationException()
}
}
5 changes: 5 additions & 0 deletions solve/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
@file:OptIn(ExperimentalWasmDsl::class)

import korlibs.korge.gradle.BuildVersions
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalDistributionDsl
import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsTargetDsl
Expand Down Expand Up @@ -30,6 +33,7 @@ val distribution: NamedDomainObjectProvider<Configuration> by configurations.reg
kotlin {
jvm()
js { configureJsOrWasm() }
wasmJs { configureJsOrWasm() }
sourceSets {
all {
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
Expand Down Expand Up @@ -57,6 +61,7 @@ kotlin {
//https://stackoverflow.com/questions/63858392/gradle-copy-submodules-output-into-other-submodule-resources/76489643
artifacts {
add(distribution.name, tasks.named("jsBrowserDistribution"))
add(distribution.name, tasks.named("wasmJsBrowserDistribution"))
}

fun KotlinSourceSet.kotlinxDeps() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ abstract class Search protected constructor(
return max(MIN_DEPTH_LIMIT, platformDepthLimit(distinctTiles))
}

protected abstract suspend fun init()
protected abstract fun platformDepthLimit(distinctTiles: Int): Int

protected abstract suspend fun getExpectimaxResults(
Expand Down
24 changes: 13 additions & 11 deletions solve/src/jsMain/kotlin/io/github/smaugfm/game2048/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,24 @@ import io.github.smaugfm.game2048.transposition.Long2LongMapTranspositionTable
import org.w3c.dom.DedicatedWorkerGlobalScope

fun main() {
println("Web worker (js) started")
val table = Long2LongMapTranspositionTable()

val self = js("self") as DedicatedWorkerGlobalScope
println("Web worker (js) started")

self.onmessage = { messageEvent ->
try {
val requestStr = messageEvent.data.toString()

val request = SearchRequest.deserialize(requestStr)!!

val scoreResult = ExpectimaxSearch(table).score(request)
self.postMessage(
scoreResult
?.serialize()
?.asDynamic()
)
if (requestStr == "ping") {
self.postMessage("pong".asDynamic())
} else {
val request = SearchRequest.deserialize(requestStr)!!
val scoreResult = ExpectimaxSearch(table).score(request)
self.postMessage(
scoreResult
?.serialize()
?.asDynamic()
)
}
null
} catch (e: Throwable) {
println("Unhandled exception in web worker (js):")
Expand Down
36 changes: 36 additions & 0 deletions solve/src/wasmJsMain/kotlin/io/github/smaugfm/game2048/Main.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.github.smaugfm.game2048

import io.github.smaugfm.game2048.search.ExpectimaxSearch
import io.github.smaugfm.game2048.search.SearchRequest
import io.github.smaugfm.game2048.transposition.Long2LongMapTranspositionTable
import org.w3c.dom.DedicatedWorkerGlobalScope

private fun getWorkerGlobalScope(): DedicatedWorkerGlobalScope =
js("self")

fun main() {
val table = Long2LongMapTranspositionTable()
val self = getWorkerGlobalScope()
println("Web-worker (wasm) started")

self.onmessage = { messageEvent ->
try {
val requestStr = messageEvent.data.toString()
if (requestStr == "ping")
self.postMessage("pong".toJsString())
else {
val request = SearchRequest.deserialize(requestStr)!!

val scoreResult = ExpectimaxSearch(table).score(request)
self.postMessage(
scoreResult
?.serialize()
?.toJsString()
)
}
} catch (e: Throwable) {
println("Unhandled exception in web worker (wasm):")
println(e)
}
}
}

0 comments on commit 55bbb42

Please sign in to comment.