From 03ed41118ace16d7359d37158b6966d6afe27d5c Mon Sep 17 00:00:00 2001 From: george Date: Wed, 17 Jun 2026 18:29:44 +0300 Subject: [PATCH] =?UTF-8?q?=D0=AD=D1=82=D0=B0=D0=BF=208:=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B5=D0=BF=D1=8F=D1=82=D1=81=D1=82=D0=B2=D0=B8=D1=8F=20=D0=B8?= =?UTF-8?q?=20=D1=83=D1=80=D0=BE=D0=B2=D0=BD=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Прогрессия сложности: каждые 5 очков — новый уровень с новыми стенами. - obstacles (Set) и level; addObstacles() добавляет порцию стен, избегая змейки/еды/бонуса/других стен и клеток вплотную к голове - столкновение со стеной-препятствием = Game Over - еда и бонус не спавнятся в препятствиях - двустрочная шапка со счётом/рекордом и уровнем/скоростью - reset() очищает препятствия и уровень Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 8 ++- .../main/java/com/example/snake/SnakeGame.kt | 52 ++++++++++++++++--- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 0f6f439..3fe2410 100644 --- a/README.md +++ b/README.md @@ -98,4 +98,10 @@ app/src/main/ шагах игры, поэтому пауза учитывается автоматически. Смотри `bonusFood` / `bonusStepsLeft`, логику в `step()` и отрисовку (`drawArc`) в `drawBoard()`. -Идеи для следующих этапов: препятствия и уровни, режим «сквозь стены», два игрока. +- **`snake-stage-8`** — препятствия и уровни: каждые 5 очков — новый уровень, + на поле добавляется порция серых стен; врезался в стену — Game Over. Еда и + бонус не спавнятся в препятствиях, новые стены не ставятся вплотную к голове. + В шапке — «Уровень». Смотри `obstacles`/`level`, `addObstacles()`, проверку + коллизий и повышение уровня в `step()`. + +Идеи для следующих этапов: режим «сквозь стены», два игрока, выбор размера поля. diff --git a/app/src/main/java/com/example/snake/SnakeGame.kt b/app/src/main/java/com/example/snake/SnakeGame.kt index 23e6485..b505867 100644 --- a/app/src/main/java/com/example/snake/SnakeGame.kt +++ b/app/src/main/java/com/example/snake/SnakeGame.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.material3.Text @@ -58,6 +59,10 @@ private fun stepDelay(score: Int): Long = private fun speedLevel(score: Int): Int = 1 + minOf(score, ((START_STEP_MS - MIN_STEP_MS) / SPEEDUP_MS).toInt()) +// --- Уровни и препятствия (этап 8) --- +private const val POINTS_PER_LEVEL = 5 // каждые N очков — новый уровень +private const val OBSTACLES_PER_LEVEL = 4 // сколько препятствий добавляется за уровень + // --- Бонус-еда (этап 7) --- private const val BONUS_LIFETIME_STEPS = 36 // сколько шагов живёт бонус (~5 c на старте) private const val BONUS_POINTS = 3 // бонус стоит как 3 обычные еды @@ -74,6 +79,7 @@ private val BODY = Color(0xFF4CD964) private val HEAD = Color(0xFF2ECC71) private val FOOD = Color(0xFFFF453A) private val BONUS = Color(0xFFFFD60A) // золотая бонус-еда +private val OBSTACLE = Color(0xFF5A6276) // серые препятствия-стены @Composable fun SnakeGame() { @@ -91,6 +97,8 @@ fun SnakeGame() { var paused by remember { mutableStateOf(false) } // этап 6: пауза var bonusFood by remember { mutableStateOf(null) } // этап 7: бонус-еда (null = нет) var bonusStepsLeft by remember { mutableIntStateOf(0) } // сколько шагов ей осталось жить + var level by remember { mutableIntStateOf(1) } // этап 8: уровень + var obstacles by remember { mutableStateOf>(emptySet()) } // препятствия-стены // Этап 3: контекст и scope для работы с DataStore (рекорд хранится на диске) val context = LocalContext.current @@ -134,6 +142,24 @@ fun SnakeGame() { paused = false bonusFood = null bonusStepsLeft = 0 + level = 1 + obstacles = emptySet() + } + + // Добавить порцию препятствий в свободные клетки (не у головы змейки). + fun addObstacles(count: Int) { + val head = snake.first() + val avoid = (snake + obstacles + food + listOfNotNull(bonusFood)).toSet() + val added = mutableSetOf() + var tries = 0 + while (added.size < count && tries < 300) { + tries++ + val p = Cell(Random.nextInt(COLS), Random.nextInt(ROWS)) + if (p in avoid || p in added) continue + if (abs(p.x - head.x) + abs(p.y - head.y) <= 2) continue // не вплотную к голове + added += p + } + obstacles = obstacles + added } // Один шаг змейки @@ -143,7 +169,7 @@ fun SnakeGame() { val nh = Cell(head.x + dir.x, head.y + dir.y) val hitWall = nh.x < 0 || nh.y < 0 || nh.x >= COLS || nh.y >= ROWS - if (hitWall || snake.contains(nh)) { + if (hitWall || snake.contains(nh) || nh in obstacles) { gameOver = true return } @@ -165,10 +191,10 @@ fun SnakeGame() { } if (ateFood) { score++ - food = randomFreeCell(snake + listOfNotNull(bonusFood)) + food = randomFreeCell(snake + obstacles + listOfNotNull(bonusFood)) // иногда выкатываем бонус (если его сейчас нет на поле) if (bonusFood == null && Random.nextInt(BONUS_CHANCE) == 0) { - bonusFood = randomFreeCell(snake + listOf(food)) + bonusFood = randomFreeCell(snake + obstacles + listOf(food)) bonusStepsLeft = BONUS_LIFETIME_STEPS } } @@ -183,6 +209,13 @@ fun SnakeGame() { scope.launch { ScoreStore.saveBest(context, best) } } + // повышение уровня: каждые POINTS_PER_LEVEL очков добавляем препятствия + val newLevel = score / POINTS_PER_LEVEL + 1 + if (newLevel > level) { + repeat(newLevel - level) { addObstacles(OBSTACLES_PER_LEVEL) } + level = newLevel + } + // таймер бонуса: считаем в шагах, поэтому пауза учитывается автоматически if (bonusFood != null) { bonusStepsLeft-- @@ -245,17 +278,18 @@ fun SnakeGame() { // Игровое поле val bonusFrac = if (bonusStepsLeft > 0) bonusStepsLeft.toFloat() / BONUS_LIFETIME_STEPS else 0f Canvas(modifier = Modifier.fillMaxSize()) { - drawBoard(snake, food, bonusFood, bonusFrac, popCell, popProgress.value) + drawBoard(snake, food, bonusFood, bonusFrac, obstacles, popCell, popProgress.value) } - // Счёт сверху (+ индикатор скорости — видно ускорение) + // Счёт сверху: две строки (счёт/рекорд и уровень/скорость) Text( - text = "Счёт: $score Рекорд: $best Скорость: ${speedLevel(score)}", + text = "Счёт: $score Рекорд: $best\nУровень: $level Скорость: ${speedLevel(score)}", color = Color.White, fontSize = 20.sp, + textAlign = TextAlign.Center, modifier = Modifier .align(Alignment.TopCenter) - .padding(top = 48.dp) + .padding(top = 40.dp) ) // Оверлеи: старт, пауза и проигрыш @@ -287,6 +321,7 @@ private fun DrawScope.drawBoard( food: Cell, bonusFood: Cell?, bonusFrac: Float, + obstacles: Set, popCell: Cell, popProgress: Float ) { @@ -307,6 +342,9 @@ private fun DrawScope.drawBoard( drawLine(GRID, Offset(offX, y), Offset(offX + cell * COLS, y), strokeWidth = 1f) } + // препятствия-стены + obstacles.forEach { drawCell(it, OBSTACLE, cell, offX, offY) } + // еда drawCell(food, FOOD, cell, offX, offY)