Implementation - GUI step

This commit is contained in:
2026-05-15 01:30:16 +03:00
parent b9df77cd4c
commit f8fc7f64cc
13 changed files with 1612 additions and 0 deletions

View 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)
}
)
}
}
}

View 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()
}
}
}

View 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)
}

View 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
}

View 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
}
}

View 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)
}
}

View 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)
}
)
}
}
}
}

View 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
}
)
}
}
}
}

View 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
)
}

View 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("Случайно")
}
}
}
}

View 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
}
}
}

View File

@@ -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
}

View 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)}%")
}
}
}