package com.example.snake import android.media.AudioAttributes import android.media.SoundPool import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.material3.Text import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlin.math.abs import kotlin.random.Random // --- Параметры игры --- private const val COLS = 20 private const val ROWS = 30 // --- Скорость и ускорение (этап 2) --- private const val START_STEP_MS = 150L // стартовая задержка между шагами (медленнее = безопаснее старт) private const val MIN_STEP_MS = 60L // потолок скорости: быстрее этого змейка не станет private const val SPEEDUP_MS = 6L // на сколько мс ускоряемся за каждую съеденную еду /** Текущая задержка шага: чем больше счёт, тем меньше задержка (но не ниже MIN_STEP_MS). */ private fun stepDelay(score: Int): Long = (START_STEP_MS - score * SPEEDUP_MS).coerceAtLeast(MIN_STEP_MS) /** Уровень скорости для индикатора: растёт со счётом, пока не упёрлись в потолок. */ private fun speedLevel(score: Int): Int = 1 + minOf(score, ((START_STEP_MS - MIN_STEP_MS) / SPEEDUP_MS).toInt()) /** Клетка поля / сегмент змейки. Direction тоже храним как Cell (dx, dy). */ private data class Cell(val x: Int, val y: Int) // --- Цвета --- private val BG = Color(0xFF10131A) private val FIELD = Color(0xFF10131A) private val GRID = Color(0xFF171C28) private val BODY = Color(0xFF4CD964) private val HEAD = Color(0xFF2ECC71) private val FOOD = Color(0xFFFF453A) @Composable fun SnakeGame() { // Начальная змейка из 3 клеток, смотрит вверх val startSnake = listOf(Cell(COLS / 2, ROWS / 2), Cell(COLS / 2, ROWS / 2 + 1), Cell(COLS / 2, ROWS / 2 + 2)) var snake by remember { mutableStateOf(startSnake) } var dir by remember { mutableStateOf(Cell(0, -1)) } // текущее направление var nextDir by remember { mutableStateOf(Cell(0, -1)) } // заданное игроком на след. шаг var food by remember { mutableStateOf(randomFreeCell(startSnake)) } var score by remember { mutableIntStateOf(0) } var best by remember { mutableIntStateOf(0) } var started by remember { mutableStateOf(false) } var gameOver by remember { mutableStateOf(false) } // Этап 3: контекст и scope для работы с DataStore (рекорд хранится на диске) val context = LocalContext.current val scope = rememberCoroutineScope() // Подгружаем сохранённый рекорд один раз при запуске экрана LaunchedEffect(Unit) { best = ScoreStore.bestFlow(context).first() } // Этап 4: звук поедания еды через SoundPool (короткий эффект, низкая задержка). // Пул и загруженный сэмпл создаём один раз и освобождаем при уходе с экрана. val soundPool = remember { SoundPool.Builder() .setMaxStreams(4) .setAudioAttributes( AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_GAME) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .build() ) .build() } val eatSoundId = remember { soundPool.load(context, R.raw.eat, 1) } DisposableEffect(Unit) { onDispose { soundPool.release() } } fun reset() { snake = startSnake dir = Cell(0, -1) nextDir = Cell(0, -1) food = randomFreeCell(startSnake) score = 0 gameOver = false } // Один шаг змейки fun step() { dir = nextDir val head = snake.first() 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)) { gameOver = true return } val grew = nh == food // голова добавляется в начало; хвост убираем, если не съели еду snake = if (grew) listOf(nh) + snake else listOf(nh) + snake.dropLast(1) if (grew) { score++ // звук поедания (left=1f, right=1f, prio=1, без зацикливания, обычная скорость) soundPool.play(eatSoundId, 1f, 1f, 1, 0, 1f) if (score > best) { best = score // новый рекорд — сохраняем на диск (асинхронно, UI не блокируется) scope.launch { ScoreStore.saveBest(context, best) } } food = randomFreeCell(snake) } } // Игровой цикл на корутине — пока компонент в композиции. // Задержка пересчитывается каждый шаг из текущего счёта, поэтому // змейка плавно ускоряется по мере поедания еды. LaunchedEffect(Unit) { while (true) { delay(stepDelay(score)) if (started && !gameOver) step() } } androidx.compose.foundation.layout.Box( modifier = Modifier .fillMaxSize() .background(BG) // Тапы: старт игры и рестарт после проигрыша .pointerInput(Unit) { detectTapGestures { if (gameOver) { reset() started = true } else if (!started) { started = true } } } // Свайпы: смена направления (и старт первым свайпом) .pointerInput(Unit) { var total = Offset.Zero detectDragGestures( onDragStart = { total = Offset.Zero if (!started && !gameOver) started = true }, onDragEnd = { if (started && !gameOver) { if (abs(total.x) > abs(total.y)) { if (total.x > 0 && dir.x != -1) nextDir = Cell(1, 0) else if (total.x < 0 && dir.x != 1) nextDir = Cell(-1, 0) } else { if (total.y > 0 && dir.y != -1) nextDir = Cell(0, 1) else if (total.y < 0 && dir.y != 1) nextDir = Cell(0, -1) } } } ) { change, drag -> total += drag change.consume() } } ) { // Игровое поле Canvas(modifier = Modifier.fillMaxSize()) { drawBoard(snake, food) } // Счёт сверху (+ индикатор скорости — видно ускорение) Text( text = "Счёт: $score Рекорд: $best Скорость: ${speedLevel(score)}", color = Color.White, fontSize = 20.sp, modifier = Modifier .align(Alignment.TopCenter) .padding(top = 48.dp) ) // Оверлеи: старт и проигрыш if (!started) { Overlay(title = "ЗМЕЙКА", subtitle = "Свайпни, чтобы начать", modifier = Modifier.align(Alignment.Center)) } else if (gameOver) { Overlay(title = "GAME OVER", subtitle = "Счёт: $score · тапни для рестарта", modifier = Modifier.align(Alignment.Center)) } } } @Composable private fun Overlay(title: String, subtitle: String, modifier: Modifier = Modifier) { Column( modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text(text = title, color = Color.White, fontSize = 44.sp, fontWeight = FontWeight.Bold) Text(text = subtitle, color = Color(0xFFB0B6C0), fontSize = 16.sp, modifier = Modifier.padding(top = 12.dp)) } } /** Вся отрисовка поля внутри DrawScope. */ private fun DrawScope.drawBoard(snake: List, food: Cell) { val cell = minOf(size.width / COLS, size.height / ROWS) val offX = (size.width - cell * COLS) / 2f val offY = (size.height - cell * ROWS) / 2f // фон поля drawRect(color = FIELD, topLeft = Offset(offX, offY), size = Size(cell * COLS, cell * ROWS)) // сетка for (i in 0..COLS) { val x = offX + i * cell drawLine(GRID, Offset(x, offY), Offset(x, offY + cell * ROWS), strokeWidth = 1f) } for (j in 0..ROWS) { val y = offY + j * cell drawLine(GRID, Offset(offX, y), Offset(offX + cell * COLS, y), strokeWidth = 1f) } // еда drawCell(food, FOOD, cell, offX, offY) // змейка (голова — другим цветом) snake.forEachIndexed { idx, c -> drawCell(c, if (idx == 0) HEAD else BODY, cell, offX, offY) } } private fun DrawScope.drawCell(c: Cell, color: Color, cell: Float, offX: Float, offY: Float) { val pad = cell * 0.08f drawRoundRect( color = color, topLeft = Offset(offX + c.x * cell + pad, offY + c.y * cell + pad), size = Size(cell - 2 * pad, cell - 2 * pad), cornerRadius = CornerRadius(cell * 0.25f, cell * 0.25f) ) } /** Случайная свободная клетка (не занятая змейкой) — для появления еды. */ private fun randomFreeCell(snake: List): Cell { while (true) { val p = Cell(Random.nextInt(COLS), Random.nextInt(ROWS)) if (!snake.contains(p)) return p } }