Implementation - GUI step
This commit is contained in:
35
src/main/kotlin/com/nano/lab2/Main.kt
Normal file
35
src/main/kotlin/com/nano/lab2/Main.kt
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package com.nano.lab2
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Window
|
||||||
|
import androidx.compose.ui.window.WindowState
|
||||||
|
import androidx.compose.ui.window.application
|
||||||
|
import model.*
|
||||||
|
import ui.*
|
||||||
|
import ui.onedimensional.OneDimensionalView
|
||||||
|
import data.ConfigRepository
|
||||||
|
|
||||||
|
fun main() = application {
|
||||||
|
val configRepository = remember { ConfigRepository() }
|
||||||
|
var config by remember { mutableStateOf(configRepository.load()) }
|
||||||
|
|
||||||
|
Window(
|
||||||
|
onCloseRequest = ::exitApplication,
|
||||||
|
title = "Теория Систем - Лаб2 игра в жизнь/клеточный автомат",
|
||||||
|
state = WindowState(width = 1200.dp, height = 800.dp)
|
||||||
|
) {
|
||||||
|
CellularAutomataTheme {
|
||||||
|
MainScreen(
|
||||||
|
config = config,
|
||||||
|
onConfigChange = { newConfig ->
|
||||||
|
config = newConfig
|
||||||
|
configRepository.save(newConfig)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/main/kotlin/com/nano/lab2/data/ConfigRepository.kt
Normal file
32
src/main/kotlin/com/nano/lab2/data/ConfigRepository.kt
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import model.AppConfig
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class ConfigRepository {
|
||||||
|
private val configFile = File("config.json")
|
||||||
|
|
||||||
|
fun load(): AppConfig {
|
||||||
|
return try {
|
||||||
|
if (configFile.exists()) {
|
||||||
|
val json = configFile.readText()
|
||||||
|
Json.decodeFromString<AppConfig>(json)
|
||||||
|
} else {
|
||||||
|
AppConfig()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
AppConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(config: AppConfig) {
|
||||||
|
try {
|
||||||
|
val json = Json.encodeToString(config)
|
||||||
|
configFile.writeText(json)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/main/kotlin/com/nano/lab2/model/AppState.kt
Normal file
15
src/main/kotlin/com/nano/lab2/model/AppState.kt
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
sealed class UiAction {
|
||||||
|
data object Randomize : UiAction()
|
||||||
|
data object Clear : UiAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class AppState(
|
||||||
|
val config: AppConfig = AppConfig(),
|
||||||
|
val action: UiAction? = null
|
||||||
|
) {
|
||||||
|
fun withAction(action: UiAction?) = copy(action = action)
|
||||||
|
fun clearAction() = copy(action = null)
|
||||||
|
fun configOnly() = copy(action = null)
|
||||||
|
}
|
||||||
75
src/main/kotlin/com/nano/lab2/model/Config.kt
Normal file
75
src/main/kotlin/com/nano/lab2/model/Config.kt
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AppConfig(
|
||||||
|
val mode: AutomatonMode = AutomatonMode.GAME_OF_LIFE,
|
||||||
|
val gridWidth: Int = 50,
|
||||||
|
val gridHeight: Int = 50,
|
||||||
|
val simulationSpeed: Int = 10,
|
||||||
|
val probability: Double = 0.5,
|
||||||
|
val oneDimensionalRule: OneDimensionalRule = OneDimensionalRule.RULE_30
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class AutomatonMode {
|
||||||
|
GAME_OF_LIFE,
|
||||||
|
ONE_DIMENSIONAL
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class OneDimensionalRule(val value: Int) {
|
||||||
|
RULE_30(30),
|
||||||
|
RULE_90(90),
|
||||||
|
RULE_110(110),
|
||||||
|
RULE_184(184)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun OneDimensionalRule.applyRule(left: Boolean, center: Boolean, right: Boolean): Boolean {
|
||||||
|
val pattern = when {
|
||||||
|
left && center && right -> 7
|
||||||
|
left && center && !right -> 6
|
||||||
|
left && !center && right -> 5
|
||||||
|
left && !center && !right -> 4
|
||||||
|
!left && center && right -> 3
|
||||||
|
!left && center && !right -> 2
|
||||||
|
!left && !center && right -> 1
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
return (value shr pattern) and 1 == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fun gridToBitmap(grid: Array<Array<Boolean>>): ByteArray {
|
||||||
|
val height = grid.size
|
||||||
|
val width = grid[0].size
|
||||||
|
val bytes = (width * height + 7) / 8
|
||||||
|
val result = ByteArray(bytes)
|
||||||
|
|
||||||
|
for (y in 0 until height) {
|
||||||
|
for (x in 0 until width) {
|
||||||
|
if (grid[y][x]) {
|
||||||
|
val idx = y * width + x
|
||||||
|
val byteIdx = idx / 8
|
||||||
|
val bitIdx = idx % 8
|
||||||
|
result[byteIdx] = (result[byteIdx].toInt() or (1 shl bitIdx)).toByte()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bitmapToGrid(bitmap: ByteArray, width: Int, height: Int): Array<Array<Boolean>> {
|
||||||
|
val grid = Array(height) { Array(width) { false } }
|
||||||
|
for (y in 0 until height) {
|
||||||
|
for (x in 0 until width) {
|
||||||
|
val idx = y * width + x
|
||||||
|
val byteIdx = idx / 8
|
||||||
|
val bitIdx = idx % 8
|
||||||
|
if (byteIdx < bitmap.size) {
|
||||||
|
grid[y][x] = (bitmap[byteIdx].toInt() and (1 shl bitIdx)) != 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return grid
|
||||||
|
}
|
||||||
125
src/main/kotlin/com/nano/lab2/model/GameOfLife.kt
Normal file
125
src/main/kotlin/com/nano/lab2/model/GameOfLife.kt
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
class GameOfLife(
|
||||||
|
val width: Int,
|
||||||
|
val height: Int
|
||||||
|
) {
|
||||||
|
private var grid: Array<Array<Boolean>> = Array(height) { Array(width) { false } }
|
||||||
|
private var generation: Int = 0
|
||||||
|
|
||||||
|
fun initialize(probability: Double = 0.5) {
|
||||||
|
grid = Array(height) { row ->
|
||||||
|
Array(width) { col ->
|
||||||
|
Math.random() < probability
|
||||||
|
}
|
||||||
|
}
|
||||||
|
generation = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initializeFromPattern(pattern: List<List<Boolean>>) {
|
||||||
|
grid = Array(height) { row ->
|
||||||
|
Array(width) { col ->
|
||||||
|
pattern.getOrNull(row)?.getOrNull(col) ?: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
generation = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
grid = Array(height) { Array(width) { false } }
|
||||||
|
generation = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun step(): Int {
|
||||||
|
val newGrid = Array(height) { Array(width) { false } }
|
||||||
|
|
||||||
|
for (y in 0 until height) {
|
||||||
|
for (x in 0 until width) {
|
||||||
|
val neighbors = countNeighbors(x, y)
|
||||||
|
val isAlive = grid[y][x]
|
||||||
|
|
||||||
|
newGrid[y][x] = when {
|
||||||
|
isAlive && (neighbors == 2 || neighbors == 3) -> true
|
||||||
|
!isAlive && neighbors == 3 -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
grid = newGrid
|
||||||
|
generation++
|
||||||
|
return generation
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun countNeighbors(x: Int, y: Int): Int {
|
||||||
|
var count = 0
|
||||||
|
for (dy in -1..1) {
|
||||||
|
for (dx in -1..1) {
|
||||||
|
if (dx == 0 && dy == 0) continue
|
||||||
|
val nx = (x + dx + width) % width
|
||||||
|
val ny = (y + dy + height) % height
|
||||||
|
if (grid[ny][nx]) count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getGrid(): Array<Array<Boolean>> = grid.map { it.copyOf() }.toTypedArray()
|
||||||
|
|
||||||
|
fun getGeneration(): Int = generation
|
||||||
|
|
||||||
|
fun getLiveCellCount(): Int = grid.sumOf { row -> row.count { it } }
|
||||||
|
|
||||||
|
fun setCell(x: Int, y: Int, alive: Boolean) {
|
||||||
|
if (x in 0 until width && y in 0 until height) {
|
||||||
|
grid[y][x] = alive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleCell(x: Int, y: Int) {
|
||||||
|
if (x in 0 until width && y in 0 until height) {
|
||||||
|
grid[y][x] = !grid[y][x]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getConnectedComponents(): List<List<Pair<Int, Int>>> {
|
||||||
|
val visited = Array(height) { BooleanArray(width) }
|
||||||
|
val components = mutableListOf<List<Pair<Int, Int>>>()
|
||||||
|
|
||||||
|
for (y in 0 until height) {
|
||||||
|
for (x in 0 until width) {
|
||||||
|
if (grid[y][x] && !visited[y][x]) {
|
||||||
|
val component = bfs(x, y, visited)
|
||||||
|
components.add(component)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return components
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bfs(startX: Int, startY: Int, visited: Array<BooleanArray>): List<Pair<Int, Int>> {
|
||||||
|
val component = mutableListOf<Pair<Int, Int>>()
|
||||||
|
val queue = ArrayDeque<Pair<Int, Int>>()
|
||||||
|
queue.add(Pair(startX, startY))
|
||||||
|
visited[startY][startX] = true
|
||||||
|
|
||||||
|
while (queue.isNotEmpty()) {
|
||||||
|
val (x, y) = queue.removeFirst()
|
||||||
|
component.add(Pair(x, y))
|
||||||
|
|
||||||
|
for (dy in -1..1) {
|
||||||
|
for (dx in -1..1) {
|
||||||
|
if (dx == 0 && dy == 0) continue
|
||||||
|
val nx = x + dx
|
||||||
|
val ny = y + dy
|
||||||
|
if (nx in 0 until width && ny in 0 until height &&
|
||||||
|
grid[ny][nx] && !visited[ny][nx]) {
|
||||||
|
visited[ny][nx] = true
|
||||||
|
queue.add(Pair(nx, ny))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return component
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/main/kotlin/com/nano/lab2/model/OneDimensionalCA.kt
Normal file
51
src/main/kotlin/com/nano/lab2/model/OneDimensionalCA.kt
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
class OneDimensionalCA(
|
||||||
|
val width: Int,
|
||||||
|
val rule: OneDimensionalRule
|
||||||
|
) {
|
||||||
|
private var currentState: BooleanArray = BooleanArray(width)
|
||||||
|
private var history: MutableList<BooleanArray> = mutableListOf()
|
||||||
|
|
||||||
|
fun initialize(centerPosition: Int? = null) {
|
||||||
|
currentState = BooleanArray(width) { false }
|
||||||
|
if (centerPosition != null && centerPosition in 0 until width) {
|
||||||
|
currentState[centerPosition] = true
|
||||||
|
} else {
|
||||||
|
currentState[width / 2] = true
|
||||||
|
}
|
||||||
|
history.clear()
|
||||||
|
history.add(currentState.copyOf())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initializeRandom(probability: Double) {
|
||||||
|
currentState = BooleanArray(width) { Math.random() < probability }
|
||||||
|
history.clear()
|
||||||
|
history.add(currentState.copyOf())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun step(): BooleanArray {
|
||||||
|
val newState = BooleanArray(width)
|
||||||
|
for (i in 0 until width) {
|
||||||
|
val left = currentState[(i - 1 + width) % width]
|
||||||
|
val center = currentState[i]
|
||||||
|
val right = currentState[(i + 1) % width]
|
||||||
|
newState[i] = rule.applyRule(left, center, right)
|
||||||
|
}
|
||||||
|
currentState = newState
|
||||||
|
history.add(currentState.copyOf())
|
||||||
|
return currentState.copyOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCurrentState(): BooleanArray = currentState.copyOf()
|
||||||
|
|
||||||
|
fun getHistory(): List<BooleanArray> = history.toList()
|
||||||
|
|
||||||
|
fun getGenerations(count: Int): List<BooleanArray> {
|
||||||
|
if (count <= history.size) return history.take(count)
|
||||||
|
while (history.size < count) {
|
||||||
|
step()
|
||||||
|
}
|
||||||
|
return history.take(count)
|
||||||
|
}
|
||||||
|
}
|
||||||
70
src/main/kotlin/com/nano/lab2/ui/MainScreen.kt
Normal file
70
src/main/kotlin/com/nano/lab2/ui/MainScreen.kt
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import model.*
|
||||||
|
import ui.gameoflife.GameOfLifeView
|
||||||
|
import ui.onedimensional.OneDimensionalView
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MainScreen(
|
||||||
|
config: AppConfig,
|
||||||
|
onConfigChange: (AppConfig) -> Unit
|
||||||
|
) {
|
||||||
|
var appState by remember { mutableStateOf(AppState(config)) }
|
||||||
|
|
||||||
|
LaunchedEffect(config) {
|
||||||
|
appState = appState.copy(config = config)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxSize().background(Color.Black)) {
|
||||||
|
Sidebar(
|
||||||
|
config = appState.config,
|
||||||
|
onConfigChange = { newConfig ->
|
||||||
|
appState = appState.copy(config = newConfig)
|
||||||
|
onConfigChange(newConfig)
|
||||||
|
},
|
||||||
|
onRandomize = { appState = appState.withAction(UiAction.Randomize) },
|
||||||
|
onClear = { appState = appState.withAction(UiAction.Clear) }
|
||||||
|
)
|
||||||
|
|
||||||
|
VerticalDivider(
|
||||||
|
modifier = Modifier.fillMaxHeight(),
|
||||||
|
thickness = 2.dp,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
when (appState.config.mode) {
|
||||||
|
AutomatonMode.GAME_OF_LIFE -> GameOfLifeView(
|
||||||
|
config = appState.config,
|
||||||
|
action = appState.action,
|
||||||
|
onActionConsumed = { appState = appState.clearAction() },
|
||||||
|
onConfigChange = { newConfig ->
|
||||||
|
appState = appState.copy(config = newConfig)
|
||||||
|
onConfigChange(newConfig)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
AutomatonMode.ONE_DIMENSIONAL -> OneDimensionalView(
|
||||||
|
config = appState.config,
|
||||||
|
action = appState.action,
|
||||||
|
onActionConsumed = { appState = appState.clearAction() },
|
||||||
|
onConfigChange = { newConfig ->
|
||||||
|
appState = appState.copy(config = newConfig)
|
||||||
|
onConfigChange(newConfig)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
405
src/main/kotlin/com/nano/lab2/ui/Sidebar.kt
Normal file
405
src/main/kotlin/com/nano/lab2/ui/Sidebar.kt
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import model.*
|
||||||
|
import util.ExperimentRunner
|
||||||
|
import java.io.File
|
||||||
|
import javax.swing.JFileChooser
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun Sidebar(
|
||||||
|
config: AppConfig,
|
||||||
|
onConfigChange: (AppConfig) -> Unit,
|
||||||
|
onRandomize: () -> Unit = {},
|
||||||
|
onClear: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
var isRunning by remember { mutableStateOf(false) }
|
||||||
|
var showExperimentResults by remember { mutableStateOf(false) }
|
||||||
|
var experimentProgress by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
val primaryColor = MaterialTheme.colorScheme.primary
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(280.dp)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.padding(16.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Настройки",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = primaryColor
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider(color = primaryColor.copy(alpha = 0.3f))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"Режим автомата",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = primaryColor
|
||||||
|
)
|
||||||
|
SelectAutomatonMode(
|
||||||
|
mode = config.mode,
|
||||||
|
onModeChange = { onConfigChange(config.copy(mode = it)) }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (config.mode == AutomatonMode.ONE_DIMENSIONAL) {
|
||||||
|
Text(
|
||||||
|
"Правило",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = primaryColor
|
||||||
|
)
|
||||||
|
SelectRule(
|
||||||
|
rule = config.oneDimensionalRule,
|
||||||
|
onRuleChange = { onConfigChange(config.copy(oneDimensionalRule = it)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(color = primaryColor.copy(alpha = 0.3f))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"Размер поля",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = primaryColor
|
||||||
|
)
|
||||||
|
|
||||||
|
if (config.mode == AutomatonMode.GAME_OF_LIFE) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = config.gridWidth.toString(),
|
||||||
|
onValueChange = { value ->
|
||||||
|
val width = value.toIntOrNull() ?: config.gridWidth
|
||||||
|
onConfigChange(config.copy(gridWidth = width, gridHeight = width))
|
||||||
|
},
|
||||||
|
label = { Text("Ширина", color = primaryColor) },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
singleLine = true,
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedTextColor = primaryColor,
|
||||||
|
unfocusedTextColor = primaryColor,
|
||||||
|
focusedBorderColor = primaryColor,
|
||||||
|
unfocusedBorderColor = primaryColor.copy(alpha = 0.5f),
|
||||||
|
cursorColor = primaryColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = config.gridHeight.toString(),
|
||||||
|
onValueChange = { value ->
|
||||||
|
val height = value.toIntOrNull() ?: config.gridHeight
|
||||||
|
onConfigChange(config.copy(gridHeight = height))
|
||||||
|
},
|
||||||
|
label = { Text("Высота", color = primaryColor) },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
singleLine = true,
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedTextColor = primaryColor,
|
||||||
|
unfocusedTextColor = primaryColor,
|
||||||
|
focusedBorderColor = primaryColor,
|
||||||
|
unfocusedBorderColor = primaryColor.copy(alpha = 0.5f),
|
||||||
|
cursorColor = primaryColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = config.gridWidth.toString(),
|
||||||
|
onValueChange = { value ->
|
||||||
|
val width = value.toIntOrNull() ?: config.gridWidth
|
||||||
|
onConfigChange(config.copy(gridWidth = width))
|
||||||
|
},
|
||||||
|
label = { Text("Ширина (ячеек)", color = primaryColor) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedTextColor = primaryColor,
|
||||||
|
unfocusedTextColor = primaryColor,
|
||||||
|
focusedBorderColor = primaryColor,
|
||||||
|
unfocusedBorderColor = primaryColor.copy(alpha = 0.5f),
|
||||||
|
cursorColor = primaryColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(color = primaryColor.copy(alpha = 0.3f))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"Скорость симуляции",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = primaryColor
|
||||||
|
)
|
||||||
|
Slider(
|
||||||
|
value = config.simulationSpeed.toFloat(),
|
||||||
|
onValueChange = { onConfigChange(config.copy(simulationSpeed = it.toInt())) },
|
||||||
|
valueRange = 1f..60f,
|
||||||
|
steps = 58,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = SliderDefaults.colors(
|
||||||
|
thumbColor = primaryColor,
|
||||||
|
activeTrackColor = primaryColor,
|
||||||
|
inactiveTrackColor = primaryColor.copy(alpha = 0.3f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"${config.simulationSpeed} FPS",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = primaryColor
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider(color = primaryColor.copy(alpha = 0.3f))
|
||||||
|
|
||||||
|
if (config.mode == AutomatonMode.GAME_OF_LIFE) {
|
||||||
|
Text(
|
||||||
|
"Вероятность заполнения",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = primaryColor
|
||||||
|
)
|
||||||
|
Slider(
|
||||||
|
value = config.probability.toFloat(),
|
||||||
|
onValueChange = { onConfigChange(config.copy(probability = it.toDouble())) },
|
||||||
|
valueRange = 0.0f..1.0f,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = SliderDefaults.colors(
|
||||||
|
thumbColor = primaryColor,
|
||||||
|
activeTrackColor = primaryColor,
|
||||||
|
inactiveTrackColor = primaryColor.copy(alpha = 0.3f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"${(config.probability * 100).toInt()}%",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = primaryColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(color = primaryColor.copy(alpha = 0.3f))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = { isRunning = !isRunning },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(if (isRunning) "Стоп" else "Старт")
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = onClear,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = primaryColor
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Сброс")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.mode == AutomatonMode.GAME_OF_LIFE) {
|
||||||
|
Button(
|
||||||
|
onClick = onRandomize,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Случайное заполнение")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.mode == AutomatonMode.GAME_OF_LIFE) {
|
||||||
|
HorizontalDivider(color = primaryColor.copy(alpha = 0.3f))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"Эксперименты",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = primaryColor
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
isRunning = false
|
||||||
|
experimentProgress = "Запуск экспериментов..."
|
||||||
|
showExperimentResults = true
|
||||||
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
val runner = ExperimentRunner()
|
||||||
|
val results = runner.runExperiments(config.gridWidth, config.gridHeight)
|
||||||
|
val graphFile = File("stabilization_graph.png")
|
||||||
|
runner.generateGraph(results, graphFile.absolutePath)
|
||||||
|
experimentProgress = "Эксперименты завершены!\nГрафик сохранён в stabilization_graph.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Запустить эксперименты")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (experimentProgress.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
experimentProgress,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = primaryColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
// Загрузка паттерна - пока отключено
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = false,
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = primaryColor.copy(alpha = 0.5f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Загрузить паттерн")
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
// Сохранение паттерна - пока отключено
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = false,
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = primaryColor.copy(alpha = 0.5f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Сохранить паттерн")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun SelectAutomatonMode(
|
||||||
|
mode: AutomatonMode,
|
||||||
|
onModeChange: (AutomatonMode) -> Unit
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
val primaryColor = MaterialTheme.colorScheme.primary
|
||||||
|
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = expanded,
|
||||||
|
onExpandedChange = { expanded = it }
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = when (mode) {
|
||||||
|
AutomatonMode.GAME_OF_LIFE -> "Game of Life (2D)"
|
||||||
|
AutomatonMode.ONE_DIMENSIONAL -> "1D Cellular Automaton"
|
||||||
|
},
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
trailingIcon = {
|
||||||
|
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.menuAnchor(MenuAnchorType.PrimaryNotEditable)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedTextColor = primaryColor,
|
||||||
|
unfocusedTextColor = primaryColor,
|
||||||
|
focusedBorderColor = primaryColor,
|
||||||
|
unfocusedBorderColor = primaryColor.copy(alpha = 0.5f),
|
||||||
|
focusedTrailingIconColor = primaryColor,
|
||||||
|
unfocusedTrailingIconColor = primaryColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false },
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Game of Life (2D)", color = primaryColor) },
|
||||||
|
onClick = {
|
||||||
|
onModeChange(AutomatonMode.GAME_OF_LIFE)
|
||||||
|
expanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("1D Cellular Automaton", color = primaryColor) },
|
||||||
|
onClick = {
|
||||||
|
onModeChange(AutomatonMode.ONE_DIMENSIONAL)
|
||||||
|
expanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun SelectRule(
|
||||||
|
rule: OneDimensionalRule,
|
||||||
|
onRuleChange: (OneDimensionalRule) -> Unit
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
val primaryColor = MaterialTheme.colorScheme.primary
|
||||||
|
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = expanded,
|
||||||
|
onExpandedChange = { expanded = it }
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = "Rule ${rule.value}",
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
trailingIcon = {
|
||||||
|
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.menuAnchor(MenuAnchorType.PrimaryNotEditable)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedTextColor = primaryColor,
|
||||||
|
unfocusedTextColor = primaryColor,
|
||||||
|
focusedBorderColor = primaryColor,
|
||||||
|
unfocusedBorderColor = primaryColor.copy(alpha = 0.5f),
|
||||||
|
focusedTrailingIconColor = primaryColor,
|
||||||
|
unfocusedTrailingIconColor = primaryColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false },
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
|
) {
|
||||||
|
OneDimensionalRule.entries.forEach { r ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Rule ${r.value}", color = primaryColor) },
|
||||||
|
onClick = {
|
||||||
|
onRuleChange(r)
|
||||||
|
expanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/main/kotlin/com/nano/lab2/ui/Theme.kt
Normal file
42
src/main/kotlin/com/nano/lab2/ui/Theme.kt
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
private val GreenTerminal = Color(0xFF00FF00)
|
||||||
|
private val DarkGreen = Color(0xFF003300)
|
||||||
|
private val MediumGreen = Color(0xFF008800)
|
||||||
|
private val Black = Color(0xFF000000)
|
||||||
|
|
||||||
|
private val GreenColorScheme = darkColorScheme(
|
||||||
|
primary = GreenTerminal,
|
||||||
|
onPrimary = Black,
|
||||||
|
primaryContainer = DarkGreen,
|
||||||
|
onPrimaryContainer = GreenTerminal,
|
||||||
|
secondary = MediumGreen,
|
||||||
|
onSecondary = Black,
|
||||||
|
secondaryContainer = DarkGreen,
|
||||||
|
onSecondaryContainer = GreenTerminal,
|
||||||
|
tertiary = GreenTerminal,
|
||||||
|
onTertiary = Black,
|
||||||
|
background = Black,
|
||||||
|
onBackground = GreenTerminal,
|
||||||
|
surface = Color(0xFF001100),
|
||||||
|
onSurface = GreenTerminal,
|
||||||
|
surfaceVariant = DarkGreen,
|
||||||
|
onSurfaceVariant = GreenTerminal,
|
||||||
|
error = Color(0xFFFF3333),
|
||||||
|
onError = Black
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CellularAutomataTheme(
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = GreenColorScheme,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
242
src/main/kotlin/com/nano/lab2/ui/gameoflife/GameOfLifeView.kt
Normal file
242
src/main/kotlin/com/nano/lab2/ui/gameoflife/GameOfLifeView.kt
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
package ui.gameoflife
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import model.AppConfig
|
||||||
|
import model.GameOfLife
|
||||||
|
import model.UiAction
|
||||||
|
import ui.gameoflife.PatternClassifier.PatternInfo
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GameOfLifeView(
|
||||||
|
config: AppConfig,
|
||||||
|
action: UiAction?,
|
||||||
|
onActionConsumed: () -> Unit,
|
||||||
|
onConfigChange: (AppConfig) -> Unit
|
||||||
|
) {
|
||||||
|
var game by remember(config.gridWidth, config.gridHeight) {
|
||||||
|
mutableStateOf(GameOfLife(config.gridWidth, config.gridHeight))
|
||||||
|
}
|
||||||
|
var isRunning by remember { mutableStateOf(false) }
|
||||||
|
var generation by remember { mutableIntStateOf(0) }
|
||||||
|
var liveCells by remember { mutableIntStateOf(0) }
|
||||||
|
var detectedPatterns by remember { mutableStateOf<List<PatternInfo>>(emptyList()) }
|
||||||
|
var validationError by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
val cellColor = MaterialTheme.colorScheme.primary
|
||||||
|
val gridColor = Color(0xFF003300)
|
||||||
|
|
||||||
|
LaunchedEffect(action) {
|
||||||
|
when (action) {
|
||||||
|
UiAction.Randomize -> {
|
||||||
|
game.initialize(config.probability)
|
||||||
|
generation = 0
|
||||||
|
liveCells = game.getLiveCellCount()
|
||||||
|
detectedPatterns = PatternClassifier.classifyPatterns(game.getGrid())
|
||||||
|
}
|
||||||
|
UiAction.Clear -> {
|
||||||
|
game.clear()
|
||||||
|
generation = 0
|
||||||
|
liveCells = 0
|
||||||
|
detectedPatterns = emptyList()
|
||||||
|
isRunning = false
|
||||||
|
}
|
||||||
|
null -> {}
|
||||||
|
}
|
||||||
|
if (action != null) onActionConsumed()
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(isRunning, config.simulationSpeed) {
|
||||||
|
while (isRunning) {
|
||||||
|
delay(1000L / config.simulationSpeed)
|
||||||
|
game.step()
|
||||||
|
generation = game.getGeneration()
|
||||||
|
liveCells = game.getLiveCellCount()
|
||||||
|
detectedPatterns = PatternClassifier.classifyPatterns(game.getGrid())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier = Modifier.fillMaxSize().background(Color.Black)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text("Поколение: $generation", color = cellColor)
|
||||||
|
Text("Живых клеток: $liveCells", color = cellColor)
|
||||||
|
Text("Объектов: ${detectedPatterns.size}", color = cellColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(Color(0xFF001100))
|
||||||
|
) {
|
||||||
|
if (validationError != null) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = validationError!!,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Canvas(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.pointerInput(config.gridWidth, config.gridHeight) {
|
||||||
|
detectTapGestures { offset ->
|
||||||
|
val cellSize = minOf(
|
||||||
|
size.width.toFloat() / config.gridWidth,
|
||||||
|
size.height.toFloat() / config.gridHeight
|
||||||
|
)
|
||||||
|
val gridWidthPx = cellSize * config.gridWidth
|
||||||
|
val gridHeightPx = cellSize * config.gridHeight
|
||||||
|
val offsetX = (size.width - gridWidthPx) / 2
|
||||||
|
val offsetY = (size.height - gridHeightPx) / 2
|
||||||
|
|
||||||
|
val x = ((offset.x - offsetX) / cellSize).toInt()
|
||||||
|
.coerceIn(0, config.gridWidth - 1)
|
||||||
|
val y = ((offset.y - offsetY) / cellSize).toInt()
|
||||||
|
.coerceIn(0, config.gridHeight - 1)
|
||||||
|
|
||||||
|
if (x in 0 until config.gridWidth && y in 0 until config.gridHeight) {
|
||||||
|
game.toggleCell(x, y)
|
||||||
|
liveCells = game.getLiveCellCount()
|
||||||
|
detectedPatterns = PatternClassifier.classifyPatterns(game.getGrid())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
val cellSize = minOf(
|
||||||
|
size.width / config.gridWidth,
|
||||||
|
size.height / config.gridHeight
|
||||||
|
)
|
||||||
|
val gridWidthPx = cellSize * config.gridWidth
|
||||||
|
val gridHeightPx = cellSize * config.gridHeight
|
||||||
|
val offsetX = (size.width - gridWidthPx) / 2
|
||||||
|
val offsetY = (size.height - gridHeightPx) / 2
|
||||||
|
|
||||||
|
for (y in 0 until config.gridHeight) {
|
||||||
|
for (x in 0 until config.gridWidth) {
|
||||||
|
drawRect(
|
||||||
|
color = gridColor,
|
||||||
|
topLeft = Offset(offsetX + x * cellSize, offsetY + y * cellSize),
|
||||||
|
size = Size(cellSize, cellSize)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val grid = game.getGrid()
|
||||||
|
for (y in 0 until config.gridHeight) {
|
||||||
|
for (x in 0 until config.gridWidth) {
|
||||||
|
if (grid[y][x]) {
|
||||||
|
drawRect(
|
||||||
|
color = cellColor,
|
||||||
|
topLeft = Offset(offsetX + x * cellSize, offsetY + y * cellSize),
|
||||||
|
size = Size(cellSize - 1, cellSize - 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detectedPatterns.isNotEmpty()) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
detectedPatterns.take(5).forEach { pattern ->
|
||||||
|
Text(
|
||||||
|
"${pattern.type}: ${pattern.count}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = cellColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
validationError = null
|
||||||
|
if (config.gridWidth < 5 || config.gridHeight < 5) {
|
||||||
|
validationError = "Размер поля слишком маленький (минимум 5x5)"
|
||||||
|
} else if (config.gridWidth > 500 || config.gridHeight > 500) {
|
||||||
|
validationError = "Размер поля слишком большой (максимум 500x500)"
|
||||||
|
} else if (config.probability < 0.01 && liveCells == 0) {
|
||||||
|
validationError = "Поле пустое! Нажмите 'Случайно' или кликните по полю"
|
||||||
|
} else {
|
||||||
|
isRunning = !isRunning
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(if (isRunning) "Стоп" else "Старт")
|
||||||
|
}
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
game.step()
|
||||||
|
generation = game.getGeneration()
|
||||||
|
liveCells = game.getLiveCellCount()
|
||||||
|
detectedPatterns = PatternClassifier.classifyPatterns(game.getGrid())
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Шаг")
|
||||||
|
}
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
game.clear()
|
||||||
|
generation = 0
|
||||||
|
liveCells = 0
|
||||||
|
detectedPatterns = emptyList()
|
||||||
|
isRunning = false
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Очистить")
|
||||||
|
}
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
game.initialize(config.probability)
|
||||||
|
generation = 0
|
||||||
|
liveCells = game.getLiveCellCount()
|
||||||
|
detectedPatterns = PatternClassifier.classifyPatterns(game.getGrid())
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Случайно")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
135
src/main/kotlin/com/nano/lab2/ui/gameoflife/PatternClassifier.kt
Normal file
135
src/main/kotlin/com/nano/lab2/ui/gameoflife/PatternClassifier.kt
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package ui.gameoflife
|
||||||
|
|
||||||
|
object PatternClassifier {
|
||||||
|
data class PatternInfo(
|
||||||
|
val type: String,
|
||||||
|
val count: Int,
|
||||||
|
val examples: List<Pair<Int, Int>>
|
||||||
|
)
|
||||||
|
|
||||||
|
fun classifyPatterns(grid: Array<Array<Boolean>>): List<PatternInfo> {
|
||||||
|
val components = findConnectedComponents(grid)
|
||||||
|
val patterns = mutableListOf<PatternInfo>()
|
||||||
|
val typeCounts = mutableMapOf<String, Int>()
|
||||||
|
|
||||||
|
for (component in components) {
|
||||||
|
val patternType = classifySinglePattern(component, grid)
|
||||||
|
typeCounts[patternType] = (typeCounts[patternType] ?: 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for ((type, count) in typeCounts) {
|
||||||
|
patterns.add(PatternInfo(type, count, emptyList()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return patterns.sortedByDescending { it.count }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findConnectedComponents(grid: Array<Array<Boolean>>): List<List<Pair<Int, Int>>> {
|
||||||
|
val height = grid.size
|
||||||
|
val width = grid[0].size
|
||||||
|
val visited = Array(height) { BooleanArray(width) }
|
||||||
|
val components = mutableListOf<List<Pair<Int, Int>>>()
|
||||||
|
|
||||||
|
for (y in 0 until height) {
|
||||||
|
for (x in 0 until width) {
|
||||||
|
if (grid[y][x] && !visited[y][x]) {
|
||||||
|
val component = bfs(x, y, grid, visited)
|
||||||
|
components.add(component)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return components
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bfs(startX: Int, startY: Int, grid: Array<Array<Boolean>>, visited: Array<BooleanArray>): List<Pair<Int, Int>> {
|
||||||
|
val component = mutableListOf<Pair<Int, Int>>()
|
||||||
|
val queue = ArrayDeque<Pair<Int, Int>>()
|
||||||
|
queue.add(Pair(startX, startY))
|
||||||
|
visited[startY][startX] = true
|
||||||
|
val height = grid.size
|
||||||
|
val width = grid[0].size
|
||||||
|
|
||||||
|
while (queue.isNotEmpty()) {
|
||||||
|
val (x, y) = queue.removeFirst()
|
||||||
|
component.add(Pair(x, y))
|
||||||
|
|
||||||
|
for (dy in -1..1) {
|
||||||
|
for (dx in -1..1) {
|
||||||
|
if (dx == 0 && dy == 0) continue
|
||||||
|
val nx = x + dx
|
||||||
|
val ny = y + dy
|
||||||
|
if (nx in 0 until width && ny in 0 until height &&
|
||||||
|
grid[ny][nx] && !visited[ny][nx]) {
|
||||||
|
visited[ny][nx] = true
|
||||||
|
queue.add(Pair(nx, ny))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return component
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun classifySinglePattern(component: List<Pair<Int, Int>>, grid: Array<Array<Boolean>>): String {
|
||||||
|
val size = component.size
|
||||||
|
|
||||||
|
if (size == 1) return "Single"
|
||||||
|
if (size == 2) return "Pair"
|
||||||
|
if (size == 3) return "Triad"
|
||||||
|
|
||||||
|
val boundingBox = getBoundingBox(component)
|
||||||
|
val width = boundingBox.second.first - boundingBox.first.first + 1
|
||||||
|
val height = boundingBox.second.second - boundingBox.first.second + 1
|
||||||
|
|
||||||
|
if (width == 2 && height == 2 && size == 4) return "Block"
|
||||||
|
if (width == 3 && height == 3 && size == 6) return "Beehive"
|
||||||
|
if (width == 3 && height == 3 && size == 5) return "Loaf"
|
||||||
|
if (width == 3 && height == 2 && size == 5) return "Boat"
|
||||||
|
if (width == 3 && height == 3 && size == 4) return "Tub"
|
||||||
|
|
||||||
|
if (isGlider(component, grid)) return "Glider"
|
||||||
|
|
||||||
|
return when {
|
||||||
|
size <= 5 -> "Small"
|
||||||
|
size <= 10 -> "Medium"
|
||||||
|
else -> "Large"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBoundingBox(component: List<Pair<Int, Int>>): Pair<Pair<Int, Int>, Pair<Int, Int>> {
|
||||||
|
val minX = component.minOf { it.first }
|
||||||
|
val maxX = component.maxOf { it.first }
|
||||||
|
val minY = component.minOf { it.second }
|
||||||
|
val maxY = component.maxOf { it.second }
|
||||||
|
return Pair(Pair(minX, minY), Pair(maxX, maxY))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isGlider(component: List<Pair<Int, Int>>, grid: Array<Array<Boolean>>): Boolean {
|
||||||
|
if (component.size != 5) return false
|
||||||
|
val cells = component.toSet()
|
||||||
|
val patterns = listOf(
|
||||||
|
setOf(Pair(0, 1), Pair(1, 2), Pair(2, 0), Pair(2, 1), Pair(2, 2)),
|
||||||
|
setOf(Pair(0, 0), Pair(0, 2), Pair(1, 1), Pair(2, 1), Pair(2, 2)),
|
||||||
|
setOf(Pair(0, 0), Pair(0, 1), Pair(1, 1), Pair(2, 1), Pair(2, 2)),
|
||||||
|
setOf(Pair(0, 1), Pair(0, 2), Pair(1, 1), Pair(2, 0), Pair(2, 1))
|
||||||
|
)
|
||||||
|
return patterns.any { it == cells }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPatternName(type: String): String {
|
||||||
|
return when (type) {
|
||||||
|
"Block" -> "Устойчивый (блок)"
|
||||||
|
"Beehive" -> "Устойчивый (улей)"
|
||||||
|
"Loaf" -> "Устойчивый (буханка)"
|
||||||
|
"Boat" -> "Устойчивый (лодка)"
|
||||||
|
"Tub" -> "Устойчивый (труба)"
|
||||||
|
"Glider" -> "Движущийся (глайдер)"
|
||||||
|
"Single" -> "Одиночная клетка"
|
||||||
|
"Pair" -> "Пара"
|
||||||
|
"Triad" -> "Триада"
|
||||||
|
"Small" -> "Малая фигура"
|
||||||
|
"Medium" -> "Средняя фигура"
|
||||||
|
"Large" -> "Большая фигура"
|
||||||
|
else -> type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
package ui.onedimensional
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import model.AppConfig
|
||||||
|
import model.OneDimensionalCA
|
||||||
|
import model.OneDimensionalRule
|
||||||
|
import model.UiAction
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OneDimensionalView(
|
||||||
|
config: AppConfig,
|
||||||
|
action: UiAction?,
|
||||||
|
onActionConsumed: () -> Unit,
|
||||||
|
onConfigChange: (AppConfig) -> Unit
|
||||||
|
) {
|
||||||
|
var automaton by remember(config.gridWidth, config.oneDimensionalRule) {
|
||||||
|
mutableStateOf(OneDimensionalCA(config.gridWidth, config.oneDimensionalRule))
|
||||||
|
}
|
||||||
|
var isRunning by remember { mutableStateOf(false) }
|
||||||
|
var generations by remember { mutableStateOf<List<BooleanArray>>(emptyList()) }
|
||||||
|
var currentGeneration by remember { mutableIntStateOf(0) }
|
||||||
|
var displayMode by remember { mutableStateOf(DisplayMode.TRAIL) }
|
||||||
|
var validationError by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
val primaryColor = MaterialTheme.colorScheme.primary
|
||||||
|
|
||||||
|
LaunchedEffect(config.oneDimensionalRule, config.gridWidth) {
|
||||||
|
automaton = OneDimensionalCA(config.gridWidth, config.oneDimensionalRule)
|
||||||
|
automaton.initialize(config.gridWidth / 2)
|
||||||
|
generations = listOf(automaton.getCurrentState())
|
||||||
|
currentGeneration = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(action) {
|
||||||
|
when (action) {
|
||||||
|
UiAction.Clear -> {
|
||||||
|
generations = emptyList()
|
||||||
|
currentGeneration = 0
|
||||||
|
isRunning = false
|
||||||
|
validationError = null
|
||||||
|
}
|
||||||
|
UiAction.Randomize -> {
|
||||||
|
automaton.initializeRandom(0.5)
|
||||||
|
generations = listOf(automaton.getCurrentState())
|
||||||
|
currentGeneration = 0
|
||||||
|
isRunning = false
|
||||||
|
validationError = null
|
||||||
|
}
|
||||||
|
null -> {}
|
||||||
|
}
|
||||||
|
if (action != null) onActionConsumed()
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(isRunning, config.simulationSpeed) {
|
||||||
|
while (isRunning) {
|
||||||
|
delay(1000L / config.simulationSpeed)
|
||||||
|
automaton.step()
|
||||||
|
generations = automaton.getGenerations(generations.size + 1)
|
||||||
|
currentGeneration = generations.size - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier = Modifier.fillMaxSize().background(Color.Black)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text("Правило: ${config.oneDimensionalRule.value}", color = primaryColor)
|
||||||
|
Text("Поколение: $currentGeneration", color = primaryColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text("Режим отображения:", color = primaryColor)
|
||||||
|
FilterChip(
|
||||||
|
selected = displayMode == DisplayMode.TRAIL,
|
||||||
|
onClick = { displayMode = DisplayMode.TRAIL },
|
||||||
|
label = { Text("Трейс") },
|
||||||
|
colors = FilterChipDefaults.filterChipColors(
|
||||||
|
selectedContainerColor = primaryColor,
|
||||||
|
selectedLabelColor = Color.Black,
|
||||||
|
containerColor = Color.Black,
|
||||||
|
labelColor = primaryColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
FilterChip(
|
||||||
|
selected = displayMode == DisplayMode.CURRENT,
|
||||||
|
onClick = { displayMode = DisplayMode.CURRENT },
|
||||||
|
label = { Text("Текущее") },
|
||||||
|
colors = FilterChipDefaults.filterChipColors(
|
||||||
|
selectedContainerColor = primaryColor,
|
||||||
|
selectedLabelColor = Color.Black,
|
||||||
|
containerColor = Color.Black,
|
||||||
|
labelColor = primaryColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(Color(0xFF001100))
|
||||||
|
) {
|
||||||
|
if (validationError != null) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = androidx.compose.ui.Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = validationError!!,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Canvas(
|
||||||
|
modifier = Modifier
|
||||||
|
.width((config.gridWidth * 8).dp)
|
||||||
|
.height((generations.size * 8).dp.coerceAtMost(2000.dp))
|
||||||
|
.horizontalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
when (displayMode) {
|
||||||
|
DisplayMode.TRAIL -> {
|
||||||
|
generations.forEachIndexed { genIndex, generation ->
|
||||||
|
generation.forEachIndexed { cellIndex, isAlive ->
|
||||||
|
if (isAlive) {
|
||||||
|
drawRect(
|
||||||
|
color = primaryColor,
|
||||||
|
topLeft = Offset(cellIndex * 8f, genIndex * 8f),
|
||||||
|
size = Size(8f, 8f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DisplayMode.CURRENT -> {
|
||||||
|
if (generations.isNotEmpty()) {
|
||||||
|
val current = generations.last()
|
||||||
|
current.forEachIndexed { cellIndex, isAlive ->
|
||||||
|
if (isAlive) {
|
||||||
|
drawRect(
|
||||||
|
color = primaryColor,
|
||||||
|
topLeft = Offset(cellIndex * 8f, 0f),
|
||||||
|
size = Size(8f, 8f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
validationError = null
|
||||||
|
if (config.gridWidth < 5) {
|
||||||
|
validationError = "Слишком маленькое поле (минимум 5)"
|
||||||
|
} else if (config.gridWidth > 200) {
|
||||||
|
validationError = "Слишком большое поле (максимум 200)"
|
||||||
|
} else {
|
||||||
|
isRunning = !isRunning
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = primaryColor,
|
||||||
|
contentColor = Color.Black
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(if (isRunning) "Стоп" else "Старт")
|
||||||
|
}
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
automaton.step()
|
||||||
|
generations = automaton.getGenerations(generations.size + 1)
|
||||||
|
currentGeneration = generations.size - 1
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = primaryColor
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Шаг")
|
||||||
|
}
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
automaton.initialize(config.gridWidth / 2)
|
||||||
|
generations = listOf(automaton.getCurrentState())
|
||||||
|
currentGeneration = 0
|
||||||
|
isRunning = false
|
||||||
|
validationError = null
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = primaryColor
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Сброс (центр)")
|
||||||
|
}
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
automaton.initializeRandom(0.5)
|
||||||
|
generations = listOf(automaton.getCurrentState())
|
||||||
|
currentGeneration = 0
|
||||||
|
isRunning = false
|
||||||
|
validationError = null
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = primaryColor
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Случайно")
|
||||||
|
}
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
generations = emptyList()
|
||||||
|
currentGeneration = 0
|
||||||
|
isRunning = false
|
||||||
|
validationError = null
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = primaryColor
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Очистить")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class DisplayMode {
|
||||||
|
TRAIL,
|
||||||
|
CURRENT
|
||||||
|
}
|
||||||
133
src/main/kotlin/com/nano/lab2/util/ExperimentRunner.kt
Normal file
133
src/main/kotlin/com/nano/lab2/util/ExperimentRunner.kt
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import model.GameOfLife
|
||||||
|
import org.knowm.xchart.BitmapEncoder
|
||||||
|
import org.knowm.xchart.XYChartBuilder
|
||||||
|
import org.knowm.xchart.style.markers.SeriesMarkers
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
data class ExperimentResult(
|
||||||
|
val density: Double,
|
||||||
|
val stabilizationTime: Int,
|
||||||
|
val survived: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
class ExperimentRunner {
|
||||||
|
private val stabilityWindow = 10
|
||||||
|
|
||||||
|
fun runExperiments(
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
): List<ExperimentResult> {
|
||||||
|
val densities = listOf(0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5)
|
||||||
|
val results = mutableListOf<ExperimentResult>()
|
||||||
|
|
||||||
|
for (density in densities) {
|
||||||
|
repeat(10) { run ->
|
||||||
|
val result = runSingleExperiment(width, height, density)
|
||||||
|
results.add(result)
|
||||||
|
println("Density: $density, Run: ${run + 1}, Stabilization: ${result.stabilizationTime}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runSingleExperiment(
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
density: Double,
|
||||||
|
): ExperimentResult {
|
||||||
|
val game = GameOfLife(width, height)
|
||||||
|
game.initialize(density)
|
||||||
|
|
||||||
|
val populationHistory = mutableListOf<Int>()
|
||||||
|
var stabilized = false
|
||||||
|
var stabilizationTime = 0
|
||||||
|
var stableCount = 0
|
||||||
|
|
||||||
|
while (!stabilized && populationHistory.size < 1000) {
|
||||||
|
val currentPopulation = game.getLiveCellCount()
|
||||||
|
populationHistory.add(currentPopulation)
|
||||||
|
|
||||||
|
if (populationHistory.size >= stabilityWindow) {
|
||||||
|
val recentWindow = populationHistory.takeLast(stabilityWindow).toList()
|
||||||
|
val allSame = recentWindow.distinct().size <= 2
|
||||||
|
val twoCyclic =
|
||||||
|
recentWindow.windowed(2).all { it[0] == it[1] } ||
|
||||||
|
(recentWindow.zipWithNext { a, b -> a == b }.count { !it } <= 2)
|
||||||
|
|
||||||
|
if (allSame || twoCyclic) {
|
||||||
|
stabilized = true
|
||||||
|
stabilizationTime = populationHistory.size - stabilityWindow
|
||||||
|
stableCount = currentPopulation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPopulation == 0) {
|
||||||
|
stabilized = true
|
||||||
|
stabilizationTime = populationHistory.size
|
||||||
|
stableCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stabilized) {
|
||||||
|
game.step()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stabilized) {
|
||||||
|
stabilizationTime = populationHistory.size
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExperimentResult(
|
||||||
|
density = density,
|
||||||
|
stabilizationTime = stabilizationTime,
|
||||||
|
survived = stableCount > 0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateGraph(
|
||||||
|
results: List<ExperimentResult>,
|
||||||
|
outputPath: String,
|
||||||
|
) {
|
||||||
|
val averageResults =
|
||||||
|
results
|
||||||
|
.groupBy { it.density }
|
||||||
|
.mapValues { (_, results) ->
|
||||||
|
results.map { it.stabilizationTime }.average()
|
||||||
|
}.toSortedMap()
|
||||||
|
|
||||||
|
val chart =
|
||||||
|
XYChartBuilder()
|
||||||
|
.title("Зависимость времени стабилизации от плотности")
|
||||||
|
.xAxisTitle("Начальная плотность (p)")
|
||||||
|
.yAxisTitle("Среднее время стабилизации (поколений)")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val xValues = averageResults.keys.toList()
|
||||||
|
val yValues = averageResults.values.toList()
|
||||||
|
|
||||||
|
chart
|
||||||
|
.addSeries("Стабилизация", xValues, yValues)
|
||||||
|
.setMarker(SeriesMarkers.CIRCLE)
|
||||||
|
|
||||||
|
BitmapEncoder.saveBitmap(chart, outputPath, BitmapEncoder.BitmapFormat.PNG)
|
||||||
|
println("Graph saved to $outputPath")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun printStatistics(results: List<ExperimentResult>) {
|
||||||
|
val byDensity = results.groupBy { it.density }
|
||||||
|
|
||||||
|
println("\n=== Экспериментальные результаты ===")
|
||||||
|
for (density in byDensity.keys.sorted()) {
|
||||||
|
val experiments = byDensity[density]!!
|
||||||
|
val avgStabilization = experiments.map { it.stabilizationTime }.average()
|
||||||
|
val survivalRate = experiments.count { it.survived }.toDouble() / experiments.size * 100
|
||||||
|
|
||||||
|
println("Плотность p=$density:")
|
||||||
|
println(" Среднее время стабилизации: ${"%.1f".format(avgStabilization)} поколений")
|
||||||
|
println(" Выживаемость: ${"%.0f".format(survivalRate)}%")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user