Skip to content

Commit

Permalink
add serialization
Browse files Browse the repository at this point in the history
  • Loading branch information
halotukozak committed Jan 17, 2024
1 parent 3839496 commit e121db3
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 28 deletions.
4 changes: 4 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
plugins {
kotlin("jvm") version "1.9.21"
kotlin("plugin.serialization") version "1.9.22"

id("org.openjfx.javafxplugin") version "0.1.0"
id("application")
}
Expand Down Expand Up @@ -39,6 +41,8 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0-RC2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.8.0-RC2")

implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")

testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion")
testImplementation("io.kotest:kotest-assertions-core:$kotestVersion")
Expand Down
12 changes: 11 additions & 1 deletion src/main/kotlin/backend/config/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ package backend.config

import backend.config.ConfigField.Companion.default
import backend.config.PlantGrowthVariant.EQUATOR
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import tornadofx.*
import java.io.File
import kotlin.reflect.full.companionObjectInstance
import kotlin.reflect.full.createInstance

@Serializable
data class Config(
val mapWidth: Int,
val mapHeight: Int,
Expand Down Expand Up @@ -33,6 +38,9 @@ data class Config(
val csvExportEnabled: Boolean,
val filename: String,
) {
fun toFile(file: File) {
file.writeText(Json.encodeToString(this))
}

init {
require(initialPlants <= mapWidth * mapHeight) { "Initial plants must be less or equal to map size" }
Expand Down Expand Up @@ -90,7 +98,7 @@ data class Config(
gens = false,
genomes = false,
csvExportEnabled = false,
filename = "",
filename = "stat.csv",
)
val default = Config(
mapWidth = default<MapGroup.MapWidth>().value,
Expand Down Expand Up @@ -118,6 +126,8 @@ data class Config(
csvExportEnabled = default<CsvExportEnabled>().value,
filename = default<Filename>().value,
)

fun fromFile(file: File) = Json.decodeFromString<Config>(file.readText()) // catch exceptions
}
}

Expand Down
8 changes: 7 additions & 1 deletion src/main/kotlin/backend/config/StatisticsConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ data class StatisticsConfig(
val genomes: Genomes = default(),
val csvExportEnabled: CsvExportEnabled = default(),
val filename: Filename = default(),
)
) {
init {
require(!csvExportEnabled.value || Filename.isValid(filename.value)) {
"Filename must be valid when csv export is enabled"
}
}
}

abstract class BooleanConfigFieldInfo : ConfigFieldInfo<Boolean>() {
override val errorMessage: String = ""
Expand Down
4 changes: 3 additions & 1 deletion src/main/kotlin/frontend/components/View.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.javafx.JavaFx
import org.kordamp.ikonli.material2.Material2SharpAL
Expand Down Expand Up @@ -74,6 +75,7 @@ abstract class View(

protected inline fun <reified U : ConfigField<T>, reified T : Any> EventTarget.input(
property: MutableStateFlow<T?>,
required: StateFlow<Boolean> = MutableStateFlow(true), //todo does not work
crossinline op: TextField.() -> Unit = {},
) = field(ConfigField.label<U>()) {
helpTooltip(ConfigField.description<U>())
Expand All @@ -82,7 +84,7 @@ abstract class View(
decorators.forEach { it.undecorate(this) }
decorators.clear()
when {
text.isNullOrBlank() -> "This field is required"
required.value && text.isNullOrBlank() -> "This field is required"
T::class == Int::class && !text.isInt() -> "This field must be an integer number"
T::class == Double::class && !text.isDouble() -> "This field must be a double number"
!ConfigField.validate<U>(text) -> ConfigField.errorMessage<U>()
Expand Down
23 changes: 21 additions & 2 deletions src/main/kotlin/frontend/config/ConfigView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package frontend.config

import backend.config.*
import frontend.components.View
import frontend.components.inputGroup
import tornadofx.*


Expand All @@ -13,6 +14,7 @@ class ConfigView : View("Config editor") {
item("Simulation Config", expanded = true) {
vbox {
form {
errorLabel(configError)
fieldset("Map") {
errorLabel(mapGroupError)
input<MapGroup.MapWidth, _>(mapWidth)
Expand Down Expand Up @@ -44,6 +46,23 @@ class ConfigView : View("Config editor") {
}

borderpane {
left {
fieldset("Config File") {
inputGroup {
button("Import") {
action {
importConfig()
}
}

button("Export") {
action {
exportConfig()
}
}
}
}
}
right {
button("Save") {
enableWhen(isValid)
Expand All @@ -68,13 +87,13 @@ class ConfigView : View("Config editor") {
}

fieldset("Csv Export") {
errorLabel(exportStatisticsGroupError)
toggleSwitch<CsvExportEnabled>(csvExportEnabled)
input<Filename, _>(filename) {
input<Filename, _>(filename, csvExportEnabled) {
enableWhen(csvExportEnabled)
}
}


borderpane {
right {
button("Save") {
Expand Down
91 changes: 68 additions & 23 deletions src/main/kotlin/frontend/config/ConfigViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package frontend.config
import backend.config.*
import frontend.components.ViewModel
import frontend.simulation.SimulationView
import javafx.stage.FileChooser
import kotlinx.coroutines.flow.*
import shared.*
import tornadofx.*

class ConfigViewModel(currentConfig: Config = Config.debug) : ViewModel() {

Expand Down Expand Up @@ -51,7 +53,8 @@ class ConfigViewModel(currentConfig: Config = Config.debug) : ViewModel() {
val plantGroupError = MutableStateFlow("")
val animalGroupError = MutableStateFlow("")
val genomeGroupError = MutableStateFlow("")
val configFieldError = MutableStateFlow("")
val configError = MutableStateFlow("")
val exportStatisticsGroupError = MutableStateFlow("")

private inline fun <T> safeFieldInit(errorStateFlow: MutableStateFlow<String>, block: () -> T): T? = try {
errorStateFlow.update { "" }
Expand Down Expand Up @@ -116,10 +119,6 @@ class ConfigViewModel(currentConfig: Config = Config.debug) : ViewModel() {
}
}.stateIn(this)

isValid = combine(mapGroup, plantGroup, animalGroup, genomeGroup) { args ->
args.none { it == null }
}.stateIn(this)

statisticsConfig = combine(
births,
deaths,
Expand All @@ -130,21 +129,26 @@ class ConfigViewModel(currentConfig: Config = Config.debug) : ViewModel() {
gens,
genomes,
csvExportEnabled,
) { (births, deaths, population, plantDensity, dailyAverageEnergy, dailyAverageAge, gens, genomes, csvExportEnabled) ->
filename.value?.let {
StatisticsConfig(
Births(births),
Deaths(deaths),
Population(population),
PlantDensity(plantDensity),
DailyAverageEnergy(dailyAverageEnergy),
DailyAverageAge(dailyAverageAge),
Gens(gens),
Genomes(genomes),
CsvExportEnabled(csvExportEnabled),
Filename(it),
)
}
) { arrayOf(*it) }
.zip(filename) { (births, deaths, population, plantDensity, dailyAverageEnergy, dailyAverageAge, gens, genomes, csvExportEnabled), filename ->
safeFieldInit(exportStatisticsGroupError) {
StatisticsConfig(
Births(births),
Deaths(deaths),
Population(population),
PlantDensity(plantDensity),
DailyAverageEnergy(dailyAverageEnergy),
DailyAverageAge(dailyAverageAge),
Gens(gens),
Genomes(genomes),
CsvExportEnabled(csvExportEnabled),
Filename(filename ?: ""),
)
}
}.stateIn(this)

isValid = combine(mapGroup, plantGroup, animalGroup, genomeGroup, statisticsConfig) { args ->
args.none { it == null }
}.stateIn(this)

simulationConfig = combine(
Expand All @@ -155,7 +159,7 @@ class ConfigViewModel(currentConfig: Config = Config.debug) : ViewModel() {
statisticsConfig,
) { mapGroup, plantGroup, animalGroup, genomeGroup, statisticsConfig ->
isValid.value.ifTrue {
safeFieldInit(configFieldError) {
safeFieldInit(configError) {
Config(
mapWidth = mapGroup!!.mapWidth.value,
mapHeight = mapGroup.mapHeight.value,
Expand All @@ -179,15 +183,56 @@ class ConfigViewModel(currentConfig: Config = Config.debug) : ViewModel() {
dailyAverageAge = statisticsConfig.dailyAverageAge.value,
gens = statisticsConfig.gens.value,
genomes = statisticsConfig.genomes.value,
csvExportEnabled = true,
filename = "statistics.csv",
csvExportEnabled = statisticsConfig.csvExportEnabled.value,
filename = statisticsConfig.filename.value,
)
}
}
}.stateIn(this)
}
}

fun importConfig() =
chooseFile("Choose a file to import", arrayOf(FileChooser.ExtensionFilter("Json", "*.json"))).firstOrNull()?.let {
val config = Config.fromFile(it)

mapWidth.update { config.mapWidth } //todo it's duplicated again and again
mapHeight.update { config.mapHeight }
initialPlants.update { config.initialPlants }
nutritionScore.update { config.nutritionScore }
plantsPerDay.update { config.plantsPerDay }
plantGrowthVariant.update { config.plantGrowthVariant }
initialAnimals.update { config.initialAnimals }
initialAnimalEnergy.update { config.initialAnimalEnergy }
satietyEnergy.update { config.satietyEnergy }
reproductionEnergyRatio.update { config.reproductionEnergyRatio }
minMutations.update { config.minMutations }
maxMutations.update { config.maxMutations }
mutationVariant.update { config.mutationVariant }
genomeLength.update { config.genomeLength }
births.update { config.births }
deaths.update { config.deaths }
population.update { config.population }
plantDensity.update { config.plantDensity }
dailyAverageEnergy.update { config.dailyAverageEnergy }
dailyAverageAge.update { config.dailyAverageAge }
gens.update { config.gens }
genomes.update { config.genomes }
csvExportEnabled.update { config.csvExportEnabled }
filename.update { config.filename }
}

fun exportConfig() = chooseFile(
"Choose a file to export",
arrayOf(FileChooser.ExtensionFilter("Json", "*.json")),
mode = FileChooserMode.Save,
)
.firstOrNull()
?.let {
simulationConfig.value?.toFile(it)
}


fun saveConfig() = simulationConfig.value?.let {
SimulationView(it).openWindow(resizable = false)
}
Expand Down

0 comments on commit e121db3

Please sign in to comment.