tawasuyu

sergio/humanoid

HTML 50Markdown 1TOML 10 Sutichaykuna

⎇ 7f7209e4457a345d22fccfe27a68f342ae9df18e nonet/src/main/java/com/rizoma/nonet/ui/MenuScreen.kt 307 siq'ikuna · 14.6 KB Ñawpa willay ch'uya

Nonet: variantes Argyle y Cadenas + arreglo de generación de regiones 7f7209e44 · Sergio ·

1package com.rizoma.nonet.ui
2
3import android.graphics.Paint
4import android.graphics.Typeface
5import androidx.compose.foundation.Canvas
6import androidx.compose.foundation.background
7import androidx.compose.foundation.border
8import androidx.compose.foundation.clickable
9import androidx.compose.foundation.layout.Arrangement
10import androidx.compose.foundation.layout.Box
11import androidx.compose.foundation.layout.Column
12import androidx.compose.foundation.layout.Row
13import androidx.compose.foundation.layout.Spacer
14import androidx.compose.foundation.layout.WindowInsets
15import androidx.compose.foundation.layout.fillMaxSize
16import androidx.compose.foundation.layout.fillMaxWidth
17import androidx.compose.foundation.layout.height
18import androidx.compose.foundation.layout.padding
19import androidx.compose.foundation.layout.size
20import androidx.compose.foundation.layout.systemBars
21import androidx.compose.foundation.layout.windowInsetsPadding
22import androidx.compose.foundation.pager.HorizontalPager
23import androidx.compose.foundation.pager.rememberPagerState
24import androidx.compose.foundation.rememberScrollState
25import androidx.compose.foundation.shape.CircleShape
26import androidx.compose.foundation.shape.RoundedCornerShape
27import androidx.compose.foundation.verticalScroll
28import androidx.compose.material3.Text
29import androidx.compose.runtime.Composable
30import androidx.compose.runtime.LaunchedEffect
31import androidx.compose.runtime.remember
32import androidx.compose.ui.Alignment
33import androidx.compose.ui.Modifier
34import androidx.compose.ui.draw.clip
35import androidx.compose.ui.geometry.Offset
36import androidx.compose.ui.graphics.Color
37import androidx.compose.ui.graphics.PathEffect
38import androidx.compose.ui.graphics.drawscope.DrawScope
39import androidx.compose.ui.graphics.drawscope.Stroke
40import androidx.compose.ui.graphics.nativeCanvas
41import androidx.compose.ui.graphics.toArgb
42import androidx.compose.ui.text.font.FontWeight
43import androidx.compose.ui.unit.dp
44import androidx.compose.ui.unit.sp
45import com.rizoma.nonet.NonetViewModel
46import com.rizoma.nonet.game.Motor
47import com.rizoma.nonet.model.Dificultad
48import com.rizoma.nonet.model.Variant
49import com.rizoma.nonet.ui.theme.MuyTenue
50import com.rizoma.nonet.ui.theme.NeonAmbar
51import com.rizoma.nonet.ui.theme.NeonCian
52import com.rizoma.nonet.ui.theme.NeonRosa
53import com.rizoma.nonet.ui.theme.NeonVerde
54import com.rizoma.nonet.ui.theme.SuperficieAlta
55import com.rizoma.nonet.ui.theme.Tenue
56import com.rizoma.nonet.ui.theme.Vacio
57
58@Composable
59fun MenuScreen(vm: NonetViewModel) {
60    val variantes = remember { Variant.entries }
61    val pager = rememberPagerState(initialPage = variantes.indexOf(vm.variante)) { variantes.size }
62    // La página visible fija la variante activa (para "nuevo" y la lista).
63    LaunchedEffect(pager.currentPage) { vm.elegirVariante(variantes[pager.currentPage]) }
64
65    Column(
66        Modifier.fillMaxSize().background(Vacio).windowInsetsPadding(WindowInsets.systemBars),
67    ) {
68        Spacer(Modifier.height(20.dp))
69        Row(Modifier.fillMaxWidth().padding(horizontal = 22.dp), verticalAlignment = Alignment.CenterVertically) {
70            Column {
71                Text("nonet", color = Color.White, fontSize = 30.sp, fontWeight = FontWeight.Bold)
72                Text("desliza para ver cada variante", color = MuyTenue, fontSize = 12.sp)
73            }
74            Spacer(Modifier.weight(1f))
75            Text("${pager.currentPage + 1}/${variantes.size}", color = MuyTenue, fontSize = 13.sp)
76        }
77        // Puntos de página.
78        Row(
79            Modifier.fillMaxWidth().padding(horizontal = 22.dp, vertical = 10.dp),
80            horizontalArrangement = Arrangement.spacedBy(5.dp),
81        ) {
82            variantes.indices.forEach { idx ->
83                val on = idx == pager.currentPage
84                Box(Modifier.size(if (on) 8.dp else 6.dp).clip(CircleShape).background(if (on) NeonCian else MuyTenue))
85            }
86        }
87
88        HorizontalPager(state = pager, modifier = Modifier.weight(1f)) { page ->
89            Pagina(vm, variantes[page])
90        }
91    }
92}
93
94@Composable
95private fun Pagina(vm: NonetViewModel, v: Variant) {
96    Column(
97        Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 22.dp),
98    ) {
99        Spacer(Modifier.height(8.dp))
100        Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
101            FormaVariante(v, Modifier.size(168.dp))
102        }
103        Spacer(Modifier.height(14.dp))
104        Text(v.titulo, color = Color.White, fontSize = 22.sp, fontWeight = FontWeight.Bold)
105        Spacer(Modifier.height(4.dp))
106        Text(v.regla, color = Tenue, fontSize = 13.sp)
107
108        Spacer(Modifier.height(18.dp))
109        Text("DIFICULTAD", color = MuyTenue, fontSize = 11.sp, letterSpacing = 3.sp)
110        Spacer(Modifier.height(8.dp))
111        Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
112            Dificultad.entries.forEach { d ->
113                Box(Modifier.weight(1f)) {
114                    Chip(d.titulo, d == vm.dificultad, NeonVerde, fill = true) { vm.elegirDificultad(d) }
115                }
116            }
117        }
118
119        Spacer(Modifier.height(20.dp))
120        val n = vm.cuenta(v, vm.dificultad)
121        Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
122            Text("PUZZLES", color = MuyTenue, fontSize = 11.sp, letterSpacing = 3.sp)
123            Spacer(Modifier.weight(1f))
124            Text("$n generado${if (n == 1) "" else "s"}", color = MuyTenue, fontSize = 11.sp)
125        }
126        Spacer(Modifier.height(10.dp))
127
128        (0 until n).toList().chunked(5).forEach { fila ->
129            Row(Modifier.fillMaxWidth().padding(vertical = 5.dp), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
130                fila.forEach { idx ->
131                    val id = vm.puzzleId(v, vm.dificultad, idx)
132                    val estado = when {
133                        id in vm.completados -> NeonVerde
134                        id in vm.enCurso -> NeonAmbar
135                        else -> MuyTenue
136                    }
137                    Box(Modifier.weight(1f), contentAlignment = Alignment.Center) {
138                        PuzzleNode(idx + 1, estado) { vm.abrir(v, vm.dificultad, idx) }
139                    }
140                }
141                repeat(5 - fila.size) { Spacer(Modifier.weight(1f)) }
142            }
143        }
144        Spacer(Modifier.height(14.dp))
145        Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
146            Chip("✦  nuevo puzzle", false, NeonCian) { vm.generarNuevo(v, vm.dificultad) }
147        }
148        Spacer(Modifier.height(40.dp))
149    }
150}
151
152/** Dibujo demostrativo de la forma particular de cada variante. */
153@Composable
154private fun FormaVariante(v: Variant, modifier: Modifier) {
155    val paint = remember {
156        Paint().apply { isAntiAlias = true; typeface = Typeface.DEFAULT_BOLD; textAlign = Paint.Align.CENTER }
157    }
158    val regionesEj = remember(v) {
159        when (v.tipo) {
160            Variant.Tipo.JIGSAW -> Motor.jigsawEjemplo()
161            Variant.Tipo.CHAIN -> Motor.chainEjemplo()
162            else -> null
163        }
164    }
165    Box(modifier.clip(RoundedCornerShape(10.dp)).background(SuperficieAlta).padding(10.dp)) {
166        Canvas(Modifier.fillMaxSize()) {
167            val cell = size.width / 9f
168            fun cx(c: Int) = (c + 0.5f) * cell
169            fun cy(r: Int) = (r + 0.5f) * cell
170            val amber = NeonAmbar.copy(alpha = 0.30f)
171
172            // Sombreados que van DEBAJO de la rejilla.
173            when (v.realce) {
174                Variant.Realce.CELDAS -> for (i in v.highlightCells()) drawRect(amber, Offset((i % 9) * cell, (i / 9) * cell), androidx.compose.ui.geometry.Size(cell, cell))
175                Variant.Realce.POSICION -> for (i in 0 until 81) {
176                    val pos = ((i / 9) % 3) * 3 + (i % 9) % 3
177                    drawRect(Color.hsv(pos * 40f, 0.55f, 0.9f).copy(alpha = 0.16f), Offset((i % 9) * cell, (i / 9) * cell), androidx.compose.ui.geometry.Size(cell, cell))
178                }
179                Variant.Realce.PARIDAD -> for (i in 0 until 81) if ((i / 9 + i % 9) % 2 == 0)
180                    drawRect(Color.White.copy(alpha = 0.10f), Offset((i % 9) * cell, (i / 9) * cell), androidx.compose.ui.geometry.Size(cell, cell))
181                else -> {}
182            }
183
184            // Rejilla fina.
185            for (k in 0..9) {
186                drawLine(MuyTenue.copy(alpha = 0.5f), Offset(k * cell, 0f), Offset(k * cell, size.height), 1f)
187                drawLine(MuyTenue.copy(alpha = 0.5f), Offset(0f, k * cell), Offset(size.width, k * cell), 1f)
188            }
189            // Regiones gruesas: jigsaw o cajas 3×3.
190            if (regionesEj != null) {
191                for (r in 0 until 9) for (c in 0 until 9) {
192                    val i = r * 9 + c
193                    if (c < 8 && regionesEj[i] != regionesEj[i + 1]) drawLine(Tenue, Offset((c + 1) * cell, r * cell), Offset((c + 1) * cell, (r + 1) * cell), 3f)
194                    if (r < 8 && regionesEj[i] != regionesEj[i + 9]) drawLine(Tenue, Offset(c * cell, (r + 1) * cell), Offset((c + 1) * cell, (r + 1) * cell), 3f)
195                }
196                drawRect(Tenue, Offset(0f, 0f), androidx.compose.ui.geometry.Size(size.width, size.height), style = Stroke(3f))
197            } else {
198                for (k in 0..9 step 3) {
199                    drawLine(Tenue, Offset(k * cell, 0f), Offset(k * cell, size.height), 3f)
200                    drawLine(Tenue, Offset(0f, k * cell), Offset(size.width, k * cell), 3f)
201                }
202            }
203
204            // Rasgo distintivo encima.
205            when (v) {
206                Variant.X -> {
207                    drawLine(NeonRosa, Offset(0f, 0f), Offset(size.width, size.height), 3f)
208                    drawLine(NeonRosa, Offset(size.width, 0f), Offset(0f, size.height), 3f)
209                }
210                Variant.CONSECUTIVO -> {
211                    drawLine(Color.White, Offset(4 * cell, cy(4) - cell * 0.2f), Offset(4 * cell, cy(4) + cell * 0.2f), 5f)
212                }
213                Variant.XV -> {
214                    paint.textSize = cell * 0.7f; paint.color = NeonCian.toArgb()
215                    texto(this, paint, "X", 4 * cell, cy(3)); texto(this, paint, "V", 6 * cell, cy(5))
216                }
217                Variant.MAYOR_QUE -> {
218                    paint.textSize = cell * 0.8f; paint.color = NeonVerde.toArgb()
219                    texto(this, paint, "‹", 4 * cell, cy(4)); texto(this, paint, "˄", cx(5), 4 * cell)
220                }
221                Variant.KILLER -> {
222                    val dash = PathEffect.dashPathEffect(floatArrayOf(5f, 5f))
223                    val ins = cell * 0.12f
224                    // jaula L sobre {(2,2),(2,3),(3,2)}
225                    drawCage(this, listOf(2 to 2, 2 to 3, 3 to 2), cell, ins, dash)
226                    paint.textSize = cell * 0.42f; paint.color = NeonAmbar.toArgb(); paint.textAlign = Paint.Align.LEFT
227                    drawIntoCanvasM(this) { it.drawText("15", 2 * cell + ins + 2f, 2 * cell + ins + cell * 0.4f, paint) }
228                    paint.textAlign = Paint.Align.CENTER
229                }
230                Variant.SKYSCRAPER -> {
231                    paint.textSize = cell * 0.55f; paint.color = NeonCian.toArgb()
232                    texto(this, paint, "2", cx(1), cy(0)); texto(this, paint, "3", cx(4), cy(0)); texto(this, paint, "1", cx(7), cy(0))
233                    texto(this, paint, "2", cx(0), cy(3)); texto(this, paint, "4", cx(0), cy(6))
234                }
235                Variant.LITTLE_KILLER -> {
236                    paint.textSize = cell * 0.6f; paint.color = NeonAmbar.toArgb()
237                    texto(this, paint, "↘", cx(1), cy(0)); paint.textSize = cell * 0.45f
238                    texto(this, paint, "17", cx(1), cy(1))
239                }
240                Variant.ROSSINI -> {
241                    paint.textSize = cell * 0.7f; paint.color = NeonVerde.toArgb()
242                    texto(this, paint, "↓", cx(3), cy(0)); texto(this, paint, "→", cx(0), cy(5))
243                }
244                Variant.ARGYLE -> for (g in v.argyleDiagonals()) {
245                    val a = g.first(); val z = g.last()
246                    drawLine(NeonRosa, Offset(cx(a % 9), cy(a / 9)), Offset(cx(z % 9), cy(z / 9)), 2.5f)
247                }
248                else -> {}
249            }
250        }
251    }
252}
253
254private fun drawCage(scope: DrawScope, rc: List<Pair<Int, Int>>, cell: Float, ins: Float, dash: PathEffect) {
255    val set = rc.toHashSet()
256    for ((r, c) in rc) {
257        val l = c * cell + ins; val rt = (c + 1) * cell - ins; val t = r * cell + ins; val b = (r + 1) * cell - ins
258        if ((r to (c - 1)) !in set) scope.drawLine(NeonAmbar, Offset(l, t), Offset(l, b), 1.5f, pathEffect = dash)
259        if ((r to (c + 1)) !in set) scope.drawLine(NeonAmbar, Offset(rt, t), Offset(rt, b), 1.5f, pathEffect = dash)
260        if (((r - 1) to c) !in set) scope.drawLine(NeonAmbar, Offset(l, t), Offset(rt, t), 1.5f, pathEffect = dash)
261        if (((r + 1) to c) !in set) scope.drawLine(NeonAmbar, Offset(l, b), Offset(rt, b), 1.5f, pathEffect = dash)
262    }
263}
264
265private fun texto(scope: DrawScope, paint: Paint, s: String, x: Float, y: Float) {
266    drawIntoCanvasM(scope) {
267        val dy = (paint.descent() + paint.ascent()) / 2f
268        it.drawText(s, x, y - dy, paint)
269    }
270}
271
272private inline fun drawIntoCanvasM(scope: DrawScope, block: (android.graphics.Canvas) -> Unit) {
273    block(scope.drawContext.canvas.nativeCanvas)
274}
275
276@Composable
277private fun Chip(text: String, selected: Boolean, accent: Color, fill: Boolean = false, onClick: () -> Unit) {
278    Box(
279        (if (fill) Modifier.fillMaxWidth() else Modifier)
280            .clip(RoundedCornerShape(50))
281            .background(if (selected) accent.copy(alpha = 0.18f) else SuperficieAlta)
282            .border(1.dp, if (selected) accent else Color.Transparent, RoundedCornerShape(50))
283            .clickable(onClick = onClick)
284            .padding(horizontal = 16.dp, vertical = 10.dp),
285        contentAlignment = Alignment.Center,
286    ) {
287        Text(
288            text, color = if (selected) accent else Tenue, fontSize = 14.sp,
289            fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal, maxLines = 1,
290        )
291    }
292}
293
294@Composable
295private fun PuzzleNode(numero: Int, accent: Color, onClick: () -> Unit) {
296    Column(horizontalAlignment = Alignment.CenterHorizontally) {
297        Box(
298            Modifier.size(52.dp).clip(RoundedCornerShape(14.dp)).background(SuperficieAlta)
299                .border(1.5.dp, accent.copy(alpha = 0.7f), RoundedCornerShape(14.dp)).clickable(onClick = onClick),
300            contentAlignment = Alignment.Center,
301        ) {
302            Text("$numero", color = accent, fontSize = 18.sp, fontWeight = FontWeight.SemiBold)
303        }
304        Spacer(Modifier.height(4.dp))
305        Box(Modifier.size(6.dp).clip(CircleShape).background(accent))
306    }
307}