Этап 8: препятствия и уровни
Прогрессия сложности: каждые 5 очков — новый уровень с новыми стенами. - obstacles (Set<Cell>) и level; addObstacles() добавляет порцию стен, избегая змейки/еды/бонуса/других стен и клеток вплотную к голове - столкновение со стеной-препятствием = Game Over - еда и бонус не спавнятся в препятствиях - двустрочная шапка со счётом/рекордом и уровнем/скоростью - reset() очищает препятствия и уровень Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
50e6677989
commit
03ed41118a
@ -98,4 +98,10 @@ app/src/main/
|
||||
шагах игры, поэтому пауза учитывается автоматически. Смотри `bonusFood` /
|
||||
`bonusStepsLeft`, логику в `step()` и отрисовку (`drawArc`) в `drawBoard()`.
|
||||
|
||||
Идеи для следующих этапов: препятствия и уровни, режим «сквозь стены», два игрока.
|
||||
- **`snake-stage-8`** — препятствия и уровни: каждые 5 очков — новый уровень,
|
||||
на поле добавляется порция серых стен; врезался в стену — Game Over. Еда и
|
||||
бонус не спавнятся в препятствиях, новые стены не ставятся вплотную к голове.
|
||||
В шапке — «Уровень». Смотри `obstacles`/`level`, `addObstacles()`, проверку
|
||||
коллизий и повышение уровня в `step()`.
|
||||
|
||||
Идеи для следующих этапов: режим «сквозь стены», два игрока, выбор размера поля.
|
||||
|
||||
@ -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<Cell?>(null) } // этап 7: бонус-еда (null = нет)
|
||||
var bonusStepsLeft by remember { mutableIntStateOf(0) } // сколько шагов ей осталось жить
|
||||
var level by remember { mutableIntStateOf(1) } // этап 8: уровень
|
||||
var obstacles by remember { mutableStateOf<Set<Cell>>(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<Cell>()
|
||||
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<Cell>,
|
||||
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)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user