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