Skip to content

Instantly share code, notes, and snippets.

@garyttierney
Last active September 18, 2017 23:17
Show Gist options
  • Save garyttierney/ffe55d2e35d41a510f52222bb7ac03ce to your computer and use it in GitHub Desktop.
Save garyttierney/ffe55d2e35d41a510f52222bb7ac03ce to your computer and use it in GitHub Desktop.
package org.apollo.game.action
import java.util.*
import kotlin.coroutines.experimental.Continuation
import kotlin.coroutines.experimental.CoroutineContext
import kotlin.coroutines.experimental.EmptyCoroutineContext
import kotlin.coroutines.experimental.RestrictsSuspension
import kotlin.coroutines.experimental.intrinsics.COROUTINE_SUSPENDED
import kotlin.coroutines.experimental.intrinsics.createCoroutineUnchecked
import kotlin.coroutines.experimental.intrinsics.suspendCoroutineOrReturn
typealias ActionPredicate = () -> Boolean
typealias ActionBlock = suspend ActionCoroutine.() -> Unit
interface ActionCoroutineCondition {
/**
* Called once every tick to check if `Continuation` associated with this condition should be resumed.
*/
fun resume(): Boolean
}
/**
* A coroutine condition that waits on a given number of pulses.
*/
class AwaitPulses(var pulses: Int) : ActionCoroutineCondition {
override fun resume(): Boolean {
return --pulses <= 0
}
}
/**
* A coroutine condition that waits until a predicate is fufilled.
*/
class AwaitPredicate(val predicate: ActionPredicate) : ActionCoroutineCondition {
override fun resume(): Boolean {
return predicate.invoke()
}
}
/**
* A suspend point in an `ActionCoroutine` that has its `coroutine` resumed whenever the `condition` evaluates
* to `true`.
*/
data class ActionCoroutineStep(val condition: ActionCoroutineCondition, internal val coroutine: Continuation<Unit>)
@RestrictsSuspension
class ActionCoroutine : Continuation<Unit> {
companion object {
/**
* Create a new `ActionCoroutine` and immediately execute the given `block`, returning a coroutine that
* can be resumed.
*/
fun start(block: ActionBlock) : ActionCoroutine {
val coroutine = ActionCoroutine()
block.createCoroutineUnchecked(coroutine, coroutine).resume(Unit)
return coroutine
}
}
override val context: CoroutineContext = EmptyCoroutineContext
override fun resume(value: Unit) {}
override fun resumeWithException(exception: Throwable) = throw exception
/**
* The next `step` in this `ActionCoroutine` saved as a resume point.
*/
private var next: ActionCoroutineStep? = null
/**
* Check if this coroutine has no more steps to execute.
*/
fun stopped(): Boolean {
return next == null
}
/**
* Update this coroutine and check if the condition for the next step to be resumed is satisfied.
*/
fun pulse() {
val nextStep = next
if (nextStep == null) {
return
}
val condition = nextStep.condition
val coroutine = nextStep.coroutine
if (condition.resume()) {
next = null
coroutine.resume(Unit)
}
}
private suspend fun awaitCondition(condition: ActionCoroutineCondition) {
return suspendCoroutineOrReturn { cont ->
next = ActionCoroutineStep(condition, cont)
COROUTINE_SUSPENDED
}
}
/**
* Wait `pulses` game updates before resuming this coroutine.
*/
suspend fun wait(pulses: Int) = awaitCondition(AwaitPulses(pulses))
/**
* Wait until the `predicate` returns `true` before resuming this coroutine.
*/
suspend fun wait(predicate: ActionPredicate) = awaitCondition(AwaitPredicate(predicate))
}
import org.junit.Assert.*
import org.junit.Test
class ActionCoroutineTest {
@Test
fun `Coroutine execution resumes after a pulse() call`() {
var num = 0
val coroutine = ActionCoroutine.start {
wait(1)
num = 1
}
coroutine.pulse()
// Check that the data was modified and the continuation finished.
assertEquals(1, num)
assertTrue(coroutine.stopped())
}
@Test
fun `Coroutine suspends on wait() calls`() {
var data = 0
val coroutine = ActionCoroutine.start {
wait(1)
data = 1
}
// Check that the data was unmodified and the continuation is unfinished.
assertEquals(0, data)
assertFalse(coroutine.stopped())
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment