Skip to content

Commit

Permalink
Merge pull request #1 from hbmartin/load-rle-file
Browse files Browse the repository at this point in the history
Load saved RLE files, compress RLE files when saving, timing in info
  • Loading branch information
hbmartin authored Sep 7, 2024
2 parents 6688215 + e6deed4 commit 0c7ded7
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 32 deletions.
31 changes: 17 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,43 @@
[![CodeFactor](https://www.codefactor.io/repository/github/hbmartin/openrndr-game-of-life/badge)](https://www.codefactor.io/repository/github/hbmartin/openrndr-game-of-life)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=hbmartin_openrndr-game-of-life&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=hbmartin_openrndr-game-of-life)

<img src=".idea/icon.svg" width="50" align="right">
<img src=".idea/icon.svg" width="50" align="right" alt="icon">

An implementation of [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) using [OPENRNDR](https://openrndr.org/) in Kotlin.

Inspired by [Golly](https://golly.sourceforge.io/) and the [Edwin Jakobs KotlinConf talk](https://www.youtube.com/watch?v=GysSoSwmLYo).

<img src="media/screenshot.png" width="800">
## Get Started

Download the latest [release](https://github.com/hbmartin/openrndr-game-of-life/releases) for your platform and use the [controls](#controls) to interact.

<img src="media/screenshot.png" width="800" alt="screenshot">


## Controls
- Draw on the canvas with a mouse to create new life
- Click and drag on the canvas to create new life
- Scroll up and down to control generation time
- Press space to pause / resume the simulation
- Press `esc` to reset current pattern to the initial state
- Press `r` to reset to a randomly chosen pattern
- Press `c` to reset to a random canvas
- `space` to pause / resume the simulation
- `esc` to reset current pattern to the initial state
- `r` to reset to a randomly chosen pattern
- `c` to reset to a randomized canvas
- `s` to save the current state to an RLE / Golly file
- `l` to load a state from an RLE / Golly file
- `i` to toggle info text (generation # and FPS)
- (TODO) Press 'g' to toggle grid visibility
- Press `s` to save the current state to an RLE / Golly file
- (TODO) Press 'l' to load a state from a file
- Press `i` to toggle info text (generation # and FPS)
- Press `q` to quit the program
- (TODO) Press 'h' to show the help screen
- (TODO) Press 'f' to toggle fullscreen mode
- `q` to quit the program

## Settings
- Press period `.` or comma `,` to open the settings panel
- Select a new background color
- Choose a new pattern then press apply
- Choose a new pattern


## Gradle tasks

- `./gradlew run` runs the TemplateProgram (Use `gradlew.bat run` under Windows)
- `./gradlew shadowJar` creates an executable platform specific jar file with all dependencies. Run the resulting program by typing `java -jar build/libs/openrndr-template-1.0.0-all.jar` in a terminal from the project root.
- `./gradlew shadowJar` creates an executable platform specific jar file with all dependencies. Run the resulting program by typing `java -jar build/libs/<name-version>.jar` in a terminal from the project root.
- `./gradlew jpackageZip` creates a zip with a stand-alone executable for the current platform (works with Java 14 only)

## Cross builds
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

group = "me.haroldmartin.gameoflife"
version = "1.0.0"
version = "1.0.1"

val applicationMainClass = "GameOfLifeKt"

Expand Down
6 changes: 5 additions & 1 deletion src/main/kotlin/GameOfLife.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ fun main() =
if (vm.settings.isInfoVisible) {
val fps = 1 / (seconds - lastRender)
drawer.text(
text = "Generation: ${vm.generation}, " + "FPS: %.0f".format(fps),
text =
"Generation: ${vm.generation}, " +
"Compute ms: ${vm.lastGenerationTime}, " +
"Expected ms: ${vm.delayTimeMillis}, " +
"FPS: %.0f".format(fps),
x = INFO_TEXT_POS,
y = INFO_TEXT_POS,
)
Expand Down
42 changes: 39 additions & 3 deletions src/main/kotlin/GolController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ class GolController(
operator fun get(coords: Pair<Int, Int>): Boolean = grid[coords.first][coords.second]

override fun toString(): String =
grid.joinToString("$") {
it.joinToString("") { cell -> if (cell) "A" else "." }
grid.joinToString("$\n") {
it.compress()
}

fun reset(pattern: Patterns?) {
Expand All @@ -105,8 +105,44 @@ class GolController(
randomGrid(rows, columns)
}
}

fun reset(pattern: String) {
generation = 0u
grid = centerPattern(parsePattern(pattern), rows, columns)
}
}

@Suppress("AvoidVarsExceptWithDelegate")
private fun BooleanArray.compress(): String {
if (this.isEmpty()) return ""

val result = StringBuilder()
var count = 1
var currentChar = this[0].asChar()

for (i in 1 until this.size) {
if (this[i].asChar() == currentChar) {
count++
} else {
if (count > 1) result.append(count)
result.append(currentChar)
currentChar = this[i].asChar()
count = 1
}
}

if (count > 1) result.append(count)
result.append(currentChar)

return result.toString()
}

private fun Boolean.asChar(): Char =
when (this) {
true -> 'A'
false -> '.'
}

private fun String.toBirthRule(): BooleanArray {
val rule = this.uppercase().substringAfter("B").substringBefore("/")
return BooleanArray(RULE_SIZE) { it.toString() in rule }
Expand Down Expand Up @@ -170,7 +206,7 @@ private fun wrappedIndex(
// eg. A.A$3.A$3.A$A2.A$.3A!
private fun parsePattern(pattern: String): Array<BooleanArray> {
require(pattern.isNotEmpty()) { "Pattern cannot be empty" }
val illegalChar = pattern.find { it !in "Ao.b0123456789$!" }
val illegalChar = pattern.find { it !in "Ao.b0123456789$!\n" }
@Suppress("NullableToStringCall")
require(illegalChar == null) { "Illegal character in pattern: $illegalChar" }
return pattern.split('$').mapNotNull { parseRow(it) }.toTypedArray()
Expand Down
39 changes: 26 additions & 13 deletions src/main/kotlin/GolViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import org.openrndr.KEY_ESCAPE
import org.openrndr.KEY_SPACEBAR
import org.openrndr.Program
import org.openrndr.color.ColorRGBa
import org.openrndr.dialogs.openFileDialog
import org.openrndr.extra.gui.GUI
import org.openrndr.extra.parameters.ActionParameter
import org.openrndr.extra.parameters.BooleanParameter
import org.openrndr.extra.parameters.ColorParameter
import org.openrndr.extra.parameters.Description
import org.openrndr.extra.parameters.OptionParameter
import java.io.File
import java.time.Instant.now
import kotlin.system.measureTimeMillis

private const val DEFAULT_DELAY: Long = 200
private const val DELAY_CHANGE_ON_SCROLL: Long = 50
Expand All @@ -21,11 +22,16 @@ class GolViewModel(
columns: Int,
) {
@Suppress("AvoidVarsExceptWithDelegate")
private var delayTimeMillis: Long = DEFAULT_DELAY
var delayTimeMillis: Long = DEFAULT_DELAY
private set

@Suppress("AvoidVarsExceptWithDelegate")
private var pattern = DEFAULT_PATTERN

@Suppress("AvoidVarsExceptWithDelegate")
var lastGenerationTime: Long = 0
private set

private val controller =
GolController(
rows = rows,
Expand All @@ -50,11 +56,6 @@ class GolViewModel(

@OptionParameter("\n", order = 99)
var pattern: Patterns = DEFAULT_PATTERN

@ActionParameter("Apply", order = 100)
fun doApply() {
controller.reset(pattern)
}
}

init {
Expand All @@ -65,14 +66,21 @@ class GolViewModel(
gui.compartmentsCollapsedByDefault = false
gui.add(settings)
gui.onChange { _, value ->
(value as? Patterns)?.let {
pattern = it
(value as? Patterns)?.let { newPat ->
pattern = newPat
controller.reset(pattern)
}
}

return gui
}

fun loadFile() {
openFileDialog(supportedExtensions = listOf("RLE" to listOf("rle"))) { file ->
controller.reset(file.readText())
}
}

@Suppress("LabeledExpression", "CognitiveComplexMethod")
fun listenToMouseEvents(program: Program) {
program.mouse.buttonUp.listen { event ->
Expand Down Expand Up @@ -110,13 +118,15 @@ class GolViewModel(
KEY_SPACEBAR -> delayTimeMillis = if (delayTimeMillis > 0) 0 else DEFAULT_DELAY
KEY_ESCAPE -> controller.reset(pattern)
else -> when (it.name) {
"." -> gui.visible = !gui.visible
"," -> gui.visible = !gui.visible
"." -> gui.visible = !gui.visible
"c" -> controller.reset(null)
"r" -> controller.reset(Patterns.entries.random())
"i" -> settings.isInfoVisible = !settings.isInfoVisible
"s" -> File("gol-${now().toEpochMilli()}.rle").writeText(controller.toString())
"l" -> loadFile()
"q" -> program.application.exit()
"r" -> controller.reset(Patterns.entries.random())
"s" -> File("ogol-${now().toEpochMilli()}.rle").writeText(controller.toString())
else -> Unit
}
}
}
Expand All @@ -126,7 +136,10 @@ class GolViewModel(
while (true) {
if (delayTimeMillis > 0) {
delay(delayTimeMillis)
controller.update()
lastGenerationTime =
measureTimeMillis {
controller.update()
}
} else {
@Suppress("MagicNumber")
delay(250L)
Expand Down

0 comments on commit 0c7ded7

Please sign in to comment.