diff --git a/src/main/kotlin/com/nano/lab2/model/Config.kt b/src/main/kotlin/com/nano/lab2/model/Config.kt index 3dba899..1d28789 100644 --- a/src/main/kotlin/com/nano/lab2/model/Config.kt +++ b/src/main/kotlin/com/nano/lab2/model/Config.kt @@ -9,7 +9,8 @@ data class AppConfig( val gridHeight: Int = 50, val simulationSpeed: Int = 10, val probability: Double = 0.5, - val oneDimensionalRule: OneDimensionalRule = OneDimensionalRule.RULE_30 + val oneDimensionalRule: OneDimensionalRule = OneDimensionalRule.RULE_30, + val killAtBoundary: Boolean = true ) @Serializable diff --git a/src/main/kotlin/com/nano/lab2/model/GameOfLife.kt b/src/main/kotlin/com/nano/lab2/model/GameOfLife.kt index 4584640..8f5ffd4 100644 --- a/src/main/kotlin/com/nano/lab2/model/GameOfLife.kt +++ b/src/main/kotlin/com/nano/lab2/model/GameOfLife.kt @@ -2,7 +2,8 @@ package model class GameOfLife( val width: Int, - val height: Int + val height: Int, + val killAtBoundary: Boolean = true ) { private var grid: Array> = Array(height) { Array(width) { false } } private var generation: Int = 0 @@ -56,9 +57,18 @@ class GameOfLife( 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++ + + if (killAtBoundary) { + val nx = x + dx + val ny = y + dy + if (nx in 0 until width && ny in 0 until height && grid[ny][nx]) { + count++ + } + } else { + val nx = (x + dx + width) % width + val ny = (y + dy + height) % height + if (grid[ny][nx]) count++ + } } } return count diff --git a/src/main/kotlin/com/nano/lab2/ui/Sidebar.kt b/src/main/kotlin/com/nano/lab2/ui/Sidebar.kt index a75c7e9..2290a38 100644 --- a/src/main/kotlin/com/nano/lab2/ui/Sidebar.kt +++ b/src/main/kotlin/com/nano/lab2/ui/Sidebar.kt @@ -7,6 +7,7 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -22,7 +23,7 @@ fun Sidebar( config: AppConfig, onConfigChange: (AppConfig) -> Unit, onRandomize: () -> Unit = {}, - onClear: () -> Unit = {} + onClear: () -> Unit = {}, ) { var isRunning by remember { mutableStateOf(false) } var showExperimentResults by remember { mutableStateOf(false) } @@ -31,17 +32,18 @@ fun Sidebar( val primaryColor = MaterialTheme.colorScheme.primary Column( - modifier = Modifier - .width(280.dp) - .fillMaxHeight() - .padding(16.dp) - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(12.dp) + modifier = + Modifier + .width(280.dp) + .fillMaxHeight() + .padding(16.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(12.dp), ) { Text( "Настройки", style = MaterialTheme.typography.titleLarge, - color = primaryColor + color = primaryColor, ) HorizontalDivider(color = primaryColor.copy(alpha = 0.3f)) @@ -49,22 +51,22 @@ fun Sidebar( Text( "Режим автомата", style = MaterialTheme.typography.labelMedium, - color = primaryColor + color = primaryColor, ) SelectAutomatonMode( mode = config.mode, - onModeChange = { onConfigChange(config.copy(mode = it)) } + onModeChange = { onConfigChange(config.copy(mode = it)) }, ) if (config.mode == AutomatonMode.ONE_DIMENSIONAL) { Text( "Правило", style = MaterialTheme.typography.labelMedium, - color = primaryColor + color = primaryColor, ) SelectRule( rule = config.oneDimensionalRule, - onRuleChange = { onConfigChange(config.copy(oneDimensionalRule = it)) } + onRuleChange = { onConfigChange(config.copy(oneDimensionalRule = it)) }, ) } @@ -73,13 +75,13 @@ fun Sidebar( Text( "Размер поля", style = MaterialTheme.typography.labelMedium, - color = primaryColor + color = primaryColor, ) if (config.mode == AutomatonMode.GAME_OF_LIFE) { Row( horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { OutlinedTextField( value = config.gridWidth.toString(), @@ -90,13 +92,14 @@ fun Sidebar( 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 - ) + colors = + OutlinedTextFieldDefaults.colors( + focusedTextColor = primaryColor, + unfocusedTextColor = primaryColor, + focusedBorderColor = primaryColor, + unfocusedBorderColor = primaryColor.copy(alpha = 0.5f), + cursorColor = primaryColor, + ), ) OutlinedTextField( value = config.gridHeight.toString(), @@ -107,13 +110,14 @@ fun Sidebar( 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 - ) + colors = + OutlinedTextFieldDefaults.colors( + focusedTextColor = primaryColor, + unfocusedTextColor = primaryColor, + focusedBorderColor = primaryColor, + unfocusedBorderColor = primaryColor.copy(alpha = 0.5f), + cursorColor = primaryColor, + ), ) } } else { @@ -126,13 +130,14 @@ fun Sidebar( 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 - ) + colors = + OutlinedTextFieldDefaults.colors( + focusedTextColor = primaryColor, + unfocusedTextColor = primaryColor, + focusedBorderColor = primaryColor, + unfocusedBorderColor = primaryColor.copy(alpha = 0.5f), + cursorColor = primaryColor, + ), ) } @@ -141,7 +146,7 @@ fun Sidebar( Text( "Скорость симуляции", style = MaterialTheme.typography.labelMedium, - color = primaryColor + color = primaryColor, ) Slider( value = config.simulationSpeed.toFloat(), @@ -149,16 +154,17 @@ fun Sidebar( valueRange = 1f..60f, steps = 58, modifier = Modifier.fillMaxWidth(), - colors = SliderDefaults.colors( - thumbColor = primaryColor, - activeTrackColor = primaryColor, - inactiveTrackColor = primaryColor.copy(alpha = 0.3f) - ) + colors = + SliderDefaults.colors( + thumbColor = primaryColor, + activeTrackColor = primaryColor, + inactiveTrackColor = primaryColor.copy(alpha = 0.3f), + ), ) Text( "${config.simulationSpeed} FPS", style = MaterialTheme.typography.bodySmall, - color = primaryColor + color = primaryColor, ) HorizontalDivider(color = primaryColor.copy(alpha = 0.3f)) @@ -167,23 +173,55 @@ fun Sidebar( Text( "Вероятность заполнения", style = MaterialTheme.typography.labelMedium, - color = primaryColor + 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) - ) + colors = + SliderDefaults.colors( + thumbColor = primaryColor, + activeTrackColor = primaryColor, + inactiveTrackColor = primaryColor.copy(alpha = 0.3f), + ), ) Text( "${(config.probability * 100).toInt()}%", style = MaterialTheme.typography.bodySmall, - color = primaryColor + color = primaryColor, + ) + + HorizontalDivider(color = primaryColor.copy(alpha = 0.3f)) + + Text( + "Граничные условия", + style = MaterialTheme.typography.labelMedium, + color = primaryColor, + ) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = config.killAtBoundary, + onCheckedChange = { onConfigChange(config.copy(killAtBoundary = it)) }, + colors = CheckboxDefaults.colors( + checkedColor = primaryColor, + uncheckedColor = primaryColor, + checkmarkColor = Color.Black + ) + ) + Text( + "Убивать клетки на границе", + color = primaryColor, + style = MaterialTheme.typography.bodySmall + ) + } + Text( + if (config.killAtBoundary) "Клетки за границей мертвы" else "Поле замыкается (тороид)", + color = primaryColor.copy(alpha = 0.7f), + style = MaterialTheme.typography.bodySmall ) } @@ -191,15 +229,16 @@ fun Sidebar( Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { Button( onClick = { isRunning = !isRunning }, modifier = Modifier.weight(1f), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary - ) + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), ) { Text(if (isRunning) "Стоп" else "Старт") } @@ -207,9 +246,10 @@ fun Sidebar( OutlinedButton( onClick = onClear, modifier = Modifier.weight(1f), - colors = ButtonDefaults.outlinedButtonColors( - contentColor = primaryColor - ) + colors = + ButtonDefaults.outlinedButtonColors( + contentColor = primaryColor, + ), ) { Text("Сброс") } @@ -219,10 +259,11 @@ fun Sidebar( Button( onClick = onRandomize, modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary - ) + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), ) { Text("Случайное заполнение") } @@ -234,7 +275,7 @@ fun Sidebar( Text( "Эксперименты", style = MaterialTheme.typography.titleMedium, - color = primaryColor + color = primaryColor, ) Button( @@ -251,10 +292,11 @@ fun Sidebar( } }, modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary - ) + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), ) { Text("Запустить эксперименты") } @@ -263,7 +305,7 @@ fun Sidebar( Text( experimentProgress, style = MaterialTheme.typography.bodySmall, - color = primaryColor + color = primaryColor, ) } @@ -273,9 +315,10 @@ fun Sidebar( }, modifier = Modifier.fillMaxWidth(), enabled = false, - colors = ButtonDefaults.outlinedButtonColors( - contentColor = primaryColor.copy(alpha = 0.5f) - ) + colors = + ButtonDefaults.outlinedButtonColors( + contentColor = primaryColor.copy(alpha = 0.5f), + ), ) { Text("Загрузить паттерн") } @@ -286,9 +329,10 @@ fun Sidebar( }, modifier = Modifier.fillMaxWidth(), enabled = false, - colors = ButtonDefaults.outlinedButtonColors( - contentColor = primaryColor.copy(alpha = 0.5f) - ) + colors = + ButtonDefaults.outlinedButtonColors( + contentColor = primaryColor.copy(alpha = 0.5f), + ), ) { Text("Сохранить паттерн") } @@ -300,55 +344,58 @@ fun Sidebar( @Composable private fun SelectAutomatonMode( mode: AutomatonMode, - onModeChange: (AutomatonMode) -> Unit + onModeChange: (AutomatonMode) -> Unit, ) { var expanded by remember { mutableStateOf(false) } val primaryColor = MaterialTheme.colorScheme.primary ExposedDropdownMenuBox( expanded = expanded, - onExpandedChange = { expanded = it } + onExpandedChange = { expanded = it }, ) { OutlinedTextField( - value = when (mode) { - AutomatonMode.GAME_OF_LIFE -> "Game of Life (2D)" - AutomatonMode.ONE_DIMENSIONAL -> "1D Cellular Automaton" - }, + 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 - ) + 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 + 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 - } + }, ) } } @@ -358,14 +405,14 @@ private fun SelectAutomatonMode( @Composable private fun SelectRule( rule: OneDimensionalRule, - onRuleChange: (OneDimensionalRule) -> Unit + onRuleChange: (OneDimensionalRule) -> Unit, ) { var expanded by remember { mutableStateOf(false) } val primaryColor = MaterialTheme.colorScheme.primary ExposedDropdownMenuBox( expanded = expanded, - onExpandedChange = { expanded = it } + onExpandedChange = { expanded = it }, ) { OutlinedTextField( value = "Rule ${rule.value}", @@ -374,22 +421,24 @@ private fun SelectRule( 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 - ) + 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 + containerColor = MaterialTheme.colorScheme.surface, ) { OneDimensionalRule.entries.forEach { r -> DropdownMenuItem( @@ -397,9 +446,9 @@ private fun SelectRule( onClick = { onRuleChange(r) expanded = false - } + }, ) } } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/nano/lab2/ui/gameoflife/GameOfLifeView.kt b/src/main/kotlin/com/nano/lab2/ui/gameoflife/GameOfLifeView.kt index ded9bf0..4046f43 100644 --- a/src/main/kotlin/com/nano/lab2/ui/gameoflife/GameOfLifeView.kt +++ b/src/main/kotlin/com/nano/lab2/ui/gameoflife/GameOfLifeView.kt @@ -26,8 +26,8 @@ fun GameOfLifeView( onActionConsumed: () -> Unit, onConfigChange: (AppConfig) -> Unit ) { - var game by remember(config.gridWidth, config.gridHeight) { - mutableStateOf(GameOfLife(config.gridWidth, config.gridHeight)) + var game by remember(config.gridWidth, config.gridHeight, config.killAtBoundary) { + mutableStateOf(GameOfLife(config.gridWidth, config.gridHeight, config.killAtBoundary)) } var isRunning by remember { mutableStateOf(false) } var generation by remember { mutableIntStateOf(0) } diff --git a/src/main/kotlin/com/nano/lab2/util/ExperimentRunner.kt b/src/main/kotlin/com/nano/lab2/util/ExperimentRunner.kt index ab7fe68..335b978 100644 --- a/src/main/kotlin/com/nano/lab2/util/ExperimentRunner.kt +++ b/src/main/kotlin/com/nano/lab2/util/ExperimentRunner.kt @@ -20,11 +20,11 @@ class ExperimentRunner { width: Int, height: Int, ): List { - val densities = listOf(0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5) + val densities = generateSequence(0.1) { it + 0.05 }.takeWhile { it <= 0.5 }.toList() val results = mutableListOf() for (density in densities) { - repeat(10) { run -> + repeat(100) { run -> val result = runSingleExperiment(width, height, density) results.add(result) println("Density: $density, Run: ${run + 1}, Stabilization: ${result.stabilizationTime}")