⎇ 7f7209e4457a345d22fccfe27a68f342ae9df18e
nonet/src/main/java/com/rizoma/nonet/ui/MenuScreen.kt
Ñawpa willay
ch'uya
Nonet: variantes Argyle y Cadenas + arreglo de generación de regiones
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}