Orbit Arcade: Block Drop
pauljd/davidson-ci: Davidson CI / quality (push) Failing after 2m7s
pauljd/davidson-ci: Davidson CI / ai-review (push) Failing after 1m45s

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:
2026-11-05 17:25:30 +00:00
co-authored by Claude Opus 5.5
parent b0b3c8fba5
commit 07b76bc338
4 changed files with 283 additions and 3 deletions
@@ -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)
}
}