Orbit Arcade: Block Drop
A falling-blocks puzzle for the remote (from the game list Paul approved on 2 November 2026): left and right move, up turns, down drops a row, OK drops all the way, Back pauses. Seven shapes from a shuffled bag, a landing guide, the next shape shown, rows clear for 100/300/500/800 times the level, the level rises every ten rows and quickens gravity. Orbit's own colours; best score kept on the box. In the arcade's honeycomb beside Snake and OrbitHive. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
package tv.orbit.classic
|
||||
|
||||
import android.view.KeyEvent
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
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.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.platform.LocalWindowInfo
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.tv.material3.Text
|
||||
|
||||
/**
|
||||
* Block Drop for Orbit Arcade (from the game list Paul approved on 2 November 2026): a falling-blocks puzzle played on the remote.
|
||||
* Left and right move, up turns, down drops a row, OK drops all the way, Back pauses. Seven shapes come from a shuffled bag, so
|
||||
* none is starved; full rows clear for points that grow with the level, and the level rises every ten rows. Orbit's own look.
|
||||
*/
|
||||
internal const val BLOCK_COLUMNS = 10
|
||||
internal const val BLOCK_ROWS = 20
|
||||
|
||||
/** Each shape's cells in its own box, unturned; turning rotates them a quarter within that box. */
|
||||
private val ShapeCells: List<Pair<Int, List<Pair<Int, Int>>>> = listOf(
|
||||
4 to listOf(0 to 1, 1 to 1, 2 to 1, 3 to 1), // the long bar
|
||||
2 to listOf(0 to 0, 1 to 0, 0 to 1, 1 to 1), // the square
|
||||
3 to listOf(1 to 0, 0 to 1, 1 to 1, 2 to 1), // the T
|
||||
3 to listOf(1 to 0, 2 to 0, 0 to 1, 1 to 1), // the S
|
||||
3 to listOf(0 to 0, 1 to 0, 1 to 1, 2 to 1), // the Z
|
||||
3 to listOf(0 to 0, 0 to 1, 1 to 1, 2 to 1), // the J
|
||||
3 to listOf(2 to 0, 0 to 1, 1 to 1, 2 to 1)) // the L
|
||||
|
||||
/** The cells of [kind] turned [turns] quarters, in its box. */
|
||||
internal fun shapeCells(kind: Int, turns: Int): List<Pair<Int, Int>> {
|
||||
val (box, cells) = ShapeCells[kind]
|
||||
var out = cells
|
||||
repeat(((turns % 4) + 4) % 4) { out = out.map { (x, y) -> (box - 1 - y) to x } }
|
||||
return out
|
||||
}
|
||||
|
||||
internal data class BlockPiece(val kind: Int, val turns: Int, val x: Int, val y: Int) {
|
||||
fun cells() = shapeCells(kind, turns).map { (cx, cy) -> (x + cx) to (y + cy) }
|
||||
}
|
||||
|
||||
/** The board as rows of colours (0 empty, otherwise the shape's kind + 1), the falling piece, the next, and the score. */
|
||||
internal data class BlockGame(val board: List<IntArray>, val piece: BlockPiece, val next: Int, val bag: List<Int>, val score: Int = 0, val lines: Int = 0, val alive: Boolean = true) {
|
||||
val level get() = lines / 10 + 1
|
||||
/** Gravity's pace, quickening with each level and never faster than 90 ms a row. */
|
||||
val tickMs: Long get() = maxOf(90L, 800L - (level - 1) * 70L)
|
||||
|
||||
private fun fits(p: BlockPiece) = p.cells().all { (x, y) -> x in 0 until BLOCK_COLUMNS && y < BLOCK_ROWS && (y < 0 || board[y][x] == 0) }
|
||||
|
||||
fun move(dx: Int): BlockGame = if (!alive) this else piece.copy(x = piece.x + dx).let { if (fits(it)) copy(piece = it) else this }
|
||||
|
||||
/** A quarter turn, nudged sideways (or up one) if the wall or the stack is in the way; no turn if nothing fits. */
|
||||
fun turn(): BlockGame {
|
||||
if (!alive) return this
|
||||
val turned = piece.copy(turns = piece.turns + 1)
|
||||
for ((dx, dy) in listOf(0 to 0, -1 to 0, 1 to 0, -2 to 0, 2 to 0, 0 to -1)) turned.copy(x = turned.x + dx, y = turned.y + dy).let { if (fits(it)) return copy(piece = it) }
|
||||
return this
|
||||
}
|
||||
|
||||
/** One row down by gravity or by the remote ([pressed] scores a point); a piece that can't fall locks in. */
|
||||
fun fall(pressed: Boolean = false, random: java.util.Random): BlockGame {
|
||||
if (!alive) return this
|
||||
val down = piece.copy(y = piece.y + 1)
|
||||
return if (fits(down)) copy(piece = down, score = score + if (pressed) 1 else 0) else lock(random)
|
||||
}
|
||||
|
||||
/** All the way down at once, two points a row. */
|
||||
fun drop(random: java.util.Random): BlockGame {
|
||||
if (!alive) return this
|
||||
var p = piece; var rows = 0
|
||||
while (fits(p.copy(y = p.y + 1))) { p = p.copy(y = p.y + 1); rows++ }
|
||||
return copy(piece = p, score = score + rows * 2).lock(random)
|
||||
}
|
||||
|
||||
/** Where the piece would land: drawn faintly as a guide. */
|
||||
fun landing(): BlockPiece { var p = piece; while (fits(p.copy(y = p.y + 1))) p = p.copy(y = p.y + 1); return p }
|
||||
|
||||
private fun lock(random: java.util.Random): BlockGame {
|
||||
val rows = board.map { it.copyOf() }
|
||||
var above = false
|
||||
piece.cells().forEach { (x, y) -> if (y < 0) above = true else rows[y][x] = piece.kind + 1 }
|
||||
val kept = rows.filter { row -> row.any { it == 0 } }
|
||||
val cleared = BLOCK_ROWS - kept.size
|
||||
val board2 = List(cleared) { IntArray(BLOCK_COLUMNS) } + kept
|
||||
val points = listOf(0, 100, 300, 500, 800)[cleared] * level
|
||||
val (nextKind, bag2) = draw(bag, random)
|
||||
val spawned = spawn(next)
|
||||
val game = copy(board = board2, piece = spawned, next = nextKind, bag = bag2, score = score + points, lines = lines + cleared)
|
||||
return if (above || !game.fits(spawned)) game.copy(alive = false) else game
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun spawn(kind: Int) = BlockPiece(kind, 0, (BLOCK_COLUMNS - ShapeCells[kind].first) / 2, if (kind == 0) -1 else 0)
|
||||
/** The next shape from a shuffled bag of all seven, refilled when empty. */
|
||||
fun draw(bag: List<Int>, random: java.util.Random): Pair<Int, List<Int>> {
|
||||
val full = bag.ifEmpty { (0 until ShapeCells.size).toMutableList().also { java.util.Collections.shuffle(it, random) } }
|
||||
return full.first() to full.drop(1)
|
||||
}
|
||||
fun start(random: java.util.Random): BlockGame {
|
||||
val (first, bag1) = draw(emptyList(), random); val (second, bag2) = draw(bag1, random)
|
||||
return BlockGame(List(BLOCK_ROWS) { IntArray(BLOCK_COLUMNS) }, spawn(first), second, bag2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val Marquee = FontFamily(Font(R.font.anton_regular))
|
||||
private val Honey = Color(0xFFFFD84A)
|
||||
/** Orbit's colours for the seven shapes: the channel palette, not anyone else's. */
|
||||
internal val BlockColours = listOf(Color(0xFF39C6F4), Color(0xFFFFD84A), Color(0xFFB36BFF), Color(0xFF6EDCAA), Color(0xFFFF6B6B), Color(0xFF4C7DFF), Color(0xFFFF9F43))
|
||||
|
||||
private fun DrawScope.cell(x: Int, y: Int, size: Float, origin: Offset, colour: Color, alpha: Float = 1f) {
|
||||
val at = Offset(origin.x + x * size + size * .06f, origin.y + y * size + size * .06f); val s = Size(size * .88f, size * .88f)
|
||||
drawRoundRect(colour.copy(alpha = alpha), at, s, CornerRadius(size * .18f))
|
||||
if (alpha == 1f) drawRoundRect(Color.White.copy(alpha = .22f), at, Size(s.width, s.height * .32f), CornerRadius(size * .18f))
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun BlockField(game: BlockGame, modifier: Modifier) = Canvas(modifier) {
|
||||
val size = minOf(this.size.width / BLOCK_COLUMNS, this.size.height / BLOCK_ROWS)
|
||||
val origin = Offset((this.size.width - size * BLOCK_COLUMNS) / 2, (this.size.height - size * BLOCK_ROWS) / 2)
|
||||
drawRect(Color(0xFF050E2A), origin, Size(size * BLOCK_COLUMNS, size * BLOCK_ROWS))
|
||||
for (y in 0 until BLOCK_ROWS) for (x in 0 until BLOCK_COLUMNS) {
|
||||
val v = game.board[y][x]
|
||||
if (v > 0) cell(x, y, size, origin, BlockColours[v - 1]) else drawRect(Color(0xFF0B1B44), Offset(origin.x + x * size + size * .45f, origin.y + y * size + size * .45f), Size(size * .1f, size * .1f))
|
||||
}
|
||||
if (game.alive) {
|
||||
game.landing().cells().forEach { (x, y) -> if (y >= 0) cell(x, y, size, origin, BlockColours[game.piece.kind], .22f) }
|
||||
game.piece.cells().forEach { (x, y) -> if (y >= 0) cell(x, y, size, origin, BlockColours[game.piece.kind]) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NextPiece(kind: Int, modifier: Modifier) = Canvas(modifier) {
|
||||
val size = this.size.minDimension / 4
|
||||
val cells = shapeCells(kind, 0)
|
||||
val w = cells.maxOf { it.first } + 1; val h = cells.maxOf { it.second } + 1
|
||||
val origin = Offset((this.size.width - w * size) / 2, (this.size.height - h * size) / 2)
|
||||
cells.forEach { (x, y) -> cell(x, y, size, origin, BlockColours[kind]) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun BlockDropScreen(best: Int, onBest: (Int) -> Unit, onLeave: () -> Unit) {
|
||||
val random = remember { java.util.Random() }
|
||||
var game by remember { mutableStateOf(BlockGame.start(random)) }
|
||||
var paused by remember { mutableStateOf(false) }
|
||||
val focus = remember { FocusRequester() }
|
||||
val windowFocused = LocalWindowInfo.current.isWindowFocused
|
||||
LaunchedEffect(windowFocused) { if (!windowFocused && game.alive) paused = true }
|
||||
LaunchedEffect(paused, game.alive) {
|
||||
while (!paused && game.alive) { kotlinx.coroutines.delay(game.tickMs); game = game.fall(random = random) }
|
||||
if (!game.alive && game.score > best) onBest(game.score)
|
||||
}
|
||||
val menuFocus = remember { FocusRequester() }
|
||||
LaunchedEffect(paused, game.alive) { if (paused || !game.alive) menuFocus.requestFocus() else focus.requestFocus() }
|
||||
BackHandler { if (game.alive) paused = !paused else onLeave() }
|
||||
Row(Modifier.fillMaxSize().background(Navy).padding(20.dp).focusRequester(focus).focusable().onPreviewKeyEvent { event ->
|
||||
val native = event.nativeKeyEvent
|
||||
if (native.action != KeyEvent.ACTION_DOWN || paused || !game.alive) return@onPreviewKeyEvent false
|
||||
val next = when (native.keyCode) {
|
||||
KeyEvent.KEYCODE_DPAD_LEFT -> game.move(-1)
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT -> game.move(1)
|
||||
KeyEvent.KEYCODE_DPAD_UP -> game.turn()
|
||||
KeyEvent.KEYCODE_DPAD_DOWN -> game.fall(pressed = true, random = random)
|
||||
KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_ENTER -> game.drop(random)
|
||||
else -> null
|
||||
}
|
||||
if (next != null) game = next
|
||||
next != null
|
||||
}, horizontalArrangement = Arrangement.spacedBy(20.dp)) {
|
||||
Column(Modifier.width(190.dp).fillMaxHeight(), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text("BLOCK DROP", color = Honey, fontFamily = Marquee, fontSize = 34.sp)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Text("SCORE", color = Muted, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 2.sp)
|
||||
Text(game.score.toString(), color = Color.White, fontFamily = Marquee, fontSize = 32.sp)
|
||||
Text("BEST", color = Muted, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 2.sp)
|
||||
Text(maxOf(best, game.score).toString(), color = Color.White, fontFamily = Marquee, fontSize = 32.sp)
|
||||
Text("LEVEL ${game.level} · ROWS ${game.lines}", color = Muted, fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
|
||||
Spacer(Modifier.weight(1f))
|
||||
Text("◄► Move ▲ Turn", color = Muted, fontSize = 13.sp)
|
||||
Text("▼ Down OK Drop", color = Muted, fontSize = 13.sp)
|
||||
Text("Back Pause", color = Muted, fontSize = 13.sp)
|
||||
}
|
||||
Box(Modifier.weight(1f).fillMaxHeight().border(2.dp, Blue), contentAlignment = Alignment.Center) {
|
||||
BlockField(game, Modifier.fillMaxSize().padding(6.dp))
|
||||
if (paused || !game.alive) Column(Modifier.background(Navy, RoundedCornerShape(6.dp)).border(2.dp, Honey, RoundedCornerShape(6.dp)).padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(if (game.alive) "PAUSED" else "GAME OVER", color = Honey, fontFamily = Marquee, fontSize = 30.sp)
|
||||
if (!game.alive) Text(if (game.score > best) "New best: ${game.score}" else "Score: ${game.score}", color = Color.White, fontSize = 16.sp)
|
||||
OrbitButton(if (game.alive) "Carry on" else "Play again", Modifier.width(200.dp).focusRequester(menuFocus), onClick = {
|
||||
if (!game.alive) game = BlockGame.start(random)
|
||||
paused = false
|
||||
})
|
||||
OrbitButton("Leave the game", Modifier.width(200.dp), onClick = onLeave)
|
||||
}
|
||||
}
|
||||
Column(Modifier.width(130.dp).fillMaxHeight(), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text("NEXT", color = Muted, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 2.sp)
|
||||
NextPiece(game.next, Modifier.size(110.dp).background(Color(0xFF050E2A), RoundedCornerShape(6.dp)).padding(10.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -303,7 +303,8 @@ private fun OrbitHiveScreen(onLeave: () -> Unit) {
|
||||
|
||||
private enum class ArcadeGame(val title: String, val blurb: String) {
|
||||
Snake("SNAKE", "Eat, grow, and don't hit the walls or yourself."),
|
||||
Hive("ORBITHIVE", "Fire flowers into the honeycomb, and clear the hive before it reaches you.")
|
||||
Hive("ORBITHIVE", "Fire flowers into the honeycomb, and clear the hive before it reaches you."),
|
||||
Blocks("BLOCK DROP", "Turn and drop the falling blocks, and clear full rows before the stack reaches the top.")
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -311,7 +312,7 @@ private fun GameTile(game: ArcadeGame, focus: FocusRequester?, nav: FocusRequest
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
Box((if (focus != null) Modifier.focusRequester(focus) else Modifier).size(135.dp).focusProperties { if (nav != null) up = nav }.onFocusChanged { focused = it.isFocused; if (it.isFocused) onFocus() }.clickable(onClick = onPlay), contentAlignment = Alignment.Center) {
|
||||
Canvas(Modifier.fillMaxSize()) { drawPath(hexagon(center, size.minDimension / 2), if (focused) Yellow else Panel) }
|
||||
Text(if (game == ArcadeGame.Snake) "Snake" else "OrbitHive", color = if (focused) Navy else Color.White, fontSize = 18.sp, fontWeight = FontWeight.SemiBold)
|
||||
Text(when (game) { ArcadeGame.Snake -> "Snake"; ArcadeGame.Hive -> "OrbitHive"; ArcadeGame.Blocks -> "Block Drop" }, color = if (focused) Navy else Color.White, fontSize = 18.sp, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,24 +322,31 @@ internal fun OrbitArcade(modifier: Modifier, onPlaying: (Boolean) -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val settings = remember { OrbitSettings(context) }
|
||||
var best by remember { mutableIntStateOf(settings.snakeBest) }
|
||||
var blockBest by remember { mutableIntStateOf(settings.blockDropBest) }
|
||||
var playing by remember { mutableStateOf<ArcadeGame?>(null) }
|
||||
var chosen by remember { mutableStateOf(ArcadeGame.Snake) }
|
||||
DisposableEffect(playing) { onPlaying(playing != null); onDispose { onPlaying(false) } }
|
||||
when (playing) {
|
||||
ArcadeGame.Snake -> { SnakeScreen(best, onBest = { best = it; settings.snakeBest = it }, onLeave = { playing = null }); return }
|
||||
ArcadeGame.Hive -> { OrbitHiveScreen(onLeave = { playing = null }); return }
|
||||
ArcadeGame.Blocks -> { BlockDropScreen(blockBest, onBest = { blockBest = it; settings.blockDropBest = it }, onLeave = { playing = null }); return }
|
||||
null -> {}
|
||||
}
|
||||
val first = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { first.requestFocus() }
|
||||
val nav = LocalNavFocus.current
|
||||
val preview = remember { SnakeGame(16, 9, listOf(SnakeCell(9, 4), SnakeCell(8, 4), SnakeCell(7, 4), SnakeCell(6, 4), SnakeCell(6, 5), SnakeCell(5, 5), SnakeCell(4, 5)), SnakeDirection.Right, SnakeDirection.Right, SnakeCell(12, 4)) }
|
||||
val blockPreview = remember {
|
||||
val board = List(BLOCK_ROWS) { y -> IntArray(BLOCK_COLUMNS) { x -> if (y >= 16 && (x + y) % 9 != 0) 1 + (x * 3 + y) % 7 else 0 } }
|
||||
BlockGame(board, BlockPiece(2, 1, 4, 9), 0, emptyList())
|
||||
}
|
||||
Column(modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Row(Modifier.fillMaxWidth().weight(1f), horizontalArrangement = Arrangement.spacedBy(20.dp)) {
|
||||
Column(Modifier.weight(1f).fillMaxHeight().background(Panel, RoundedCornerShape(6.dp)).padding(14.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Image(painterResource(R.drawable.orbit_arcade_logo), "Orbit Arcade", Modifier.width(256.dp).height(90.dp))
|
||||
Box(Modifier.fillMaxWidth().weight(1f).background(Color(0xFF050E2A))) {
|
||||
if (chosen == ArcadeGame.Snake) SnakeField(preview, Modifier.fillMaxSize())
|
||||
else if (chosen == ArcadeGame.Blocks) BlockField(blockPreview, Modifier.fillMaxSize().padding(8.dp))
|
||||
else Canvas(Modifier.fillMaxSize()) {
|
||||
val colours = listOf(Color(0xFFE53935), Color(0xFF1E66D0), Color(0xFF2E9D3A), Color(0xFFF4F4F4), Color(0xFFF5A623), Color(0xFF8E44AD))
|
||||
val r = size.height / 9
|
||||
@@ -349,7 +357,7 @@ internal fun OrbitArcade(modifier: Modifier, onPlaying: (Boolean) -> Unit) {
|
||||
}
|
||||
}
|
||||
Text(chosen.title, color = Honey, fontFamily = Marquee, fontSize = 30.sp)
|
||||
Text(if (chosen == ArcadeGame.Snake && best > 0) "Your best: $best" else chosen.blurb, color = Color.White, fontSize = 15.sp, maxLines = 2)
|
||||
Text(if (chosen == ArcadeGame.Snake && best > 0) "Your best: $best" else if (chosen == ArcadeGame.Blocks && blockBest > 0) "Your best: $blockBest" else chosen.blurb, color = Color.White, fontSize = 15.sp, maxLines = 2)
|
||||
}
|
||||
Column(Modifier.width(290.dp).fillMaxHeight(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
@@ -357,10 +365,13 @@ internal fun OrbitArcade(modifier: Modifier, onPlaying: (Boolean) -> Unit) {
|
||||
GameTile(ArcadeGame.Hive, null, nav, { chosen = ArcadeGame.Hive }, { playing = ArcadeGame.Hive })
|
||||
}
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
GameTile(ArcadeGame.Blocks, null, null, { chosen = ArcadeGame.Blocks }, { playing = ArcadeGame.Blocks })
|
||||
Box(Modifier.size(110.dp), contentAlignment = Alignment.Center) {
|
||||
Canvas(Modifier.fillMaxSize()) { drawPath(hexagon(center, size.minDimension / 2), Panel.copy(alpha = .4f)) }
|
||||
Text("More soon", color = Muted, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Text("◄► Choose OK Play Back Main menu", color = Muted, fontSize = 11.sp)
|
||||
|
||||
@@ -44,6 +44,9 @@ internal class OrbitSettings(context: Context) {
|
||||
var snakeBest: Int
|
||||
get() = saved.getInt("snake_best", 0)
|
||||
set(value) { saved.edit().putInt("snake_best", value).apply() }
|
||||
var blockDropBest: Int
|
||||
get() = saved.getInt("block_drop_best", 0)
|
||||
set(value) { saved.edit().putInt("block_drop_best", value).apply() }
|
||||
var subtitlesOn: Boolean
|
||||
get() = saved.getBoolean("subtitles_on", false)
|
||||
set(value) { saved.edit().putBoolean("subtitles_on", value).apply() }
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package tv.orbit.classic
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
|
||||
/** Block Drop: moves, turns with nudges off the walls, full rows clear for points, the bag deals every shape, and the stack ends it. */
|
||||
class BlockDropTest {
|
||||
private val random = java.util.Random(7)
|
||||
private fun empty() = List(BLOCK_ROWS) { IntArray(BLOCK_COLUMNS) }
|
||||
|
||||
@Test fun fourTurnsComeBackToTheStart() {
|
||||
for (kind in 0 until 7) assertEquals(shapeCells(kind, 0).toSet(), shapeCells(kind, 4).toSet())
|
||||
assertEquals(4, shapeCells(0, 1).size)
|
||||
}
|
||||
|
||||
@Test fun movesStopAtTheWallsAndATurnIsNudgedOffThem() {
|
||||
var g = BlockGame(empty(), BlockPiece(0, 1, 0, 2), 1, emptyList()) // the bar, upright
|
||||
repeat(12) { g = g.move(-1) }
|
||||
assertTrue(g.piece.cells().all { it.first >= 0 })
|
||||
val turned = g.turn()
|
||||
assertNotEquals(g.piece.turns, turned.piece.turns) // turned, nudged clear of the wall
|
||||
assertTrue(turned.piece.cells().all { it.first in 0 until BLOCK_COLUMNS })
|
||||
}
|
||||
|
||||
@Test fun aFullRowClearsAndScoresByLevel() {
|
||||
val board = empty().also { b -> for (x in 0 until BLOCK_COLUMNS - 4) b[BLOCK_ROWS - 1][x] = 3 }
|
||||
val g = BlockGame(board, BlockPiece(0, 0, BLOCK_COLUMNS - 4, 5), 1, emptyList()) // the flat bar fills the gap
|
||||
val after = g.drop(random)
|
||||
assertEquals(1, after.lines)
|
||||
assertTrue(after.board[BLOCK_ROWS - 1].all { it == 0 })
|
||||
assertEquals(100 + (BLOCK_ROWS - 2 - 5) * 2, after.score) // the row, plus two points a row dropped
|
||||
assertTrue(after.alive)
|
||||
}
|
||||
|
||||
@Test fun theBagDealsEveryShapeBeforeRepeating() {
|
||||
var bag = emptyList<Int>(); val seen = mutableListOf<Int>()
|
||||
repeat(7) { val (k, b) = BlockGame.draw(bag, random); seen += k; bag = b }
|
||||
assertEquals((0 until 7).toSet(), seen.toSet())
|
||||
}
|
||||
|
||||
@Test fun aStackToTheTopEndsTheGameAndTheLevelQuickens() {
|
||||
val board = empty().also { b -> for (y in 2 until BLOCK_ROWS) for (x in 0 until BLOCK_COLUMNS) if (x != y % BLOCK_COLUMNS) b[y][x] = 1 }
|
||||
val over = BlockGame(board, BlockPiece(1, 0, 4, 0), 1, emptyList()).drop(random)
|
||||
assertFalse(over.alive)
|
||||
assertTrue(BlockGame(empty(), BlockPiece(1, 0, 4, 0), 1, emptyList(), lines = 50).tickMs < BlockGame.start(random).tickMs)
|
||||
assertEquals(90L, BlockGame(empty(), BlockPiece(1, 0, 4, 0), 1, emptyList(), lines = 500).tickMs)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user