Taula de continguts

Sudoku en Android

Guia per desenvolupar un joc de Sudoku en Android.

Els principals objectius son:

, , , , ,

Abans que res

Se suposa que coneixes les bases de Android.

Crea un nou projecte amb una EmptyActivity i puja el repo a Git.


Creant el taulell de la GUI amb Spinners

Treballarem amb TableLayout d'Android.

Com que volem fer-ho programàticament, millor mira aquest exemple comparatiu per construir TableLayout amb XML o programàticament.

Primer caldrà crear un layout general de MainActivity on posarem un TableLayout.

Segon, al onCreate de MainActivity afegim al TableLayout objectes Spinner fins formar una quadrícula de 9×9.

Mira l'article Android Spinner per veure com crear un Spinner programàticament.

Et caldrà retocar l'estil dels Spinners per tal que et càpiguen, eliminant la fletxa del dropdown:

Spinner spinner = new Spinner(this);
spinner.setBackground(null);
spinner.setPadding(5, 5, 5, 5);

Crea el taulell del Sudoku (9×9 cel·les) amb Android Spinner amb nombres fixes de l'1 al 9 mes un valor buit per deixar la cel·la sense omplir.

Crea un espai diferenciat per separar els quadrants de 3×3. Pots intentar jugar amb els paddings (més senzill) o bé intentar pintar la quadrícula.


Connectant les callback dels Spinner

Llegeix bé l'apartat Connectant les callback dels spinner de l'article d'Android Spinner.

Al final caldrà que d'una manera o altra, connectis els spinner a les funcions:

Implementa les callback necessàries fent que quan es canvii el valor d'un Spinner ens mostri un Log indicant-nos el text «Spinner modificat».


Aplicant Tags als Spinners per identificar-los

Quan rebem una crida de callback al Listener (sigui un objecte a part o una funció de la MainActivity, necessitarem identificar els Spinner.

Una forma molt còmoda és utilitzar els setTag i getTag, unes funcions genèriques de totes les Views. Podem aplicar-ho al crear l'element al onCreate:

spinner.setTag(R.id.fila,fila);
spinner.setTag(R.id.col,col);

Així, quan rebem una crida a onItemSelected sabrem quin element s'ha modificat fent:

int fila = (int) adapterView.getTag(R.id.fila);
int col = (int) adapterView.getTag(R.id.col);

Abans, perquè aquest codi funcioni, cal definir l'identificador R.id.fila i R.id.columna dins de l'arxiu values/strings.xml:

values/strings.xml
<item name="fila" type="id" />
<item name="col" type="id" />

Implementa aquests mecanismes i fen que quan es modifiqui el valor d'algun Spinner aparegui un Toast informant-nos identificant el item (fila, columna) i el seu nou valor.


MVC Sudoku: creant el model

Seguint el patró de disseny MVC (Model - Vista - Controlador) ja tenim la part de Vista. Ens faltarà el Model i Controlador. Aquesta part la farem implementant una class SudokuModel que pugui ser suficientment genèrica com perquè pugui reutilitzar-se en altres entorns Java (com per exemple en una aplicació desktop implementada amb Swing o JavaFX).

El core del model serà, òbviament, una matriu de int de 9×9.

Haurà de tenir, al menys, els següents mètodes:

Implementa el model per al Sudoku com s'ha descrit.

Millora de la implementació del model com a llibreria

Pot ser interessant implementar el model en un Java package independent, i en un repositori independent, si volem reutilitzar el codi en altres projectes. Per exemple, podríem tenir el model de Sudoku i reutilitzar-lo en una App Android i també en una aplicació desktop implementada amb Swing. Per insertar un projecte dins d'un altre amb git convé utilitzar Git Submodules.

Seguint en aquesta línia, pots treballar el model de Sudoku en un altre IDE com Eclipse o IntelliJ, i quan tinguis el package funcionant, l'exportes com a JAR file dins la carpeta app/libs del projecte Android. Així podràs utilitzar la llibreria que has creat dins el projecte Android.


Refrescant la View o GUI

A part, fora del model, necessitarem una funció per refrescar les dades de visualització de la partida al GUI. Com que no és una funció del Model, sinó que és de visualització, no ha d'estar a SudokuModel sinó que la implementarem a MainActivity.

En aquesta funció caldrà agafar totes les dades del Model i refrescar la Vista (els Spinners). Per tant, ara necessitarem tenir tots els Spinner ben localitzats en una matriu estàtica : Spinner[][]. Així serà molt fàcil accedir al Spinner adequat (fila, columna).

Implementa el model de Sudoku en un classe nova amb els mètodes descrits.

Implementa el mètode creaPartida amb una formula senzilla, com per exemple posar números de l'1 al 9 (només 1 de cada) aleatòriament o, si vols, un en cada fila. Després ja aniràs sofisticant la creació de partida.

Comprova que si creem una partida el mètode refrescaGUI() funciona mostrant-la en els Spinners.


Init bug !

Quan s'inicialitzen els Spinner programàticament se'ns dispararà la funció de callback onItemSelected. És el què es coneix per init bug.

Cal fer un arreglo que eviti aquest efecte, ja que en aquest inici la callback es cridarà… 81 cops! I Això farà anar molt lent el dispositiu i fins i tot el pot penjar (sobretot si hi hem posat un Toast).

Aquí teniu una solució per a l'init bug utilitzant Tags, com ja hem fet abans.


Jugar la partida i arribar al FINAL

Per poder oferir una partida al jugador caldrà que omplim algunes cel·les i les fem fixes. Això vol dir que en aquelles cel·les fixes no haurem de poder entrar valors al Spinner (es pot fer un disable).

És important acabar el joc, of course.

Implementa els canvis necessaris (atributs i mètodes) al SudokuModel i a les Views per poder disposar de cel·les fixes (un nombre predeterminat de cel·les amb valors aleatoris) a l'inici de la partida.

Quan totes les cel·les estiguin plenes i alhora compleixin les regles del Sudoku, donarem per finalitzada la partida i traurem un Dialog per felicitar l'usuari.


Solver de Sudoku amb algorisme de backtracking

Secció només per a les més agosarades!

Si has resolt fins aquí i ets dels més agosarats, voldràs provar de trobar solució a la partida que has generat de forma automàtica. Això es pot fer amb un clàssic algorisme de backtracking. En el fons, aquest el què fa és provar sistemàticament totes les combinacions possibles fins que troba una de vàlida, similarment al que coneixem per un «atac de força bruta».

Aquí tens una breu explicació i psuedo-codi de l'algorisme de backtrack recursiu per al Sudoku.

helloacm.com_wp-content_uploads_2020_08_sudoku-solver.jpg

Hi ha moltes tècniques de resolució de sudokus com joc (X-wing, Y-wing, etc). Aquestes són «fàcils» d'utilitzar per persones, però no son fàcils d'implementar en un programa.

En canvi, el backtrack seria una tècnica molt difícil que segueixi una persona, però més fàcil d'implementar en un programa. No és un algorisme òptim, en el sentit en què triga molt i consumeix CPU i memòria, però funciona.

Una optimització senzilla del backtrack que pot disminuir molt el temps de computació seria disposar d'una matriu extra on posaríem els valors vàlids per a cada cel·la, descartant valors impossibles (els que hi hagi a la fila, columna i quadrant de la cel·la). Llavors el bucle que fem pels possibles valors de la cel·la dins elbacktrack es redueix i el temps de càlcul es redueix considerablement.

  1. Implementar el backtrack del Sudoku en el model i testejar-ho amb test unitari.
  2. Posar un botó «Resol» en la app, que llanci el backtrack en un Android Threads secundari (com en el de comunicacions) i que quan acabi, refresqui la view amb el mètode implementat abans.


Check partida resoluble

Quan inicialitzem una partida aleatòriament, encara que els nombres que posem compleixin les regles del Sudoku (no repetir números en files, columnes ni quadrants) això no assegura que aquesta partida sigui resoluble.

Per assegurar-nos de que una partida és resoluble, quan la creem i abans de passar-la a l'usuari, li aplicarem el Solver que hem realitzat a l'exercici anterior, el qual ens dirà si té solució o no (i en un temps raonable, pel què ha d'estar optimitzat).

A més, les regles del Sudoku inclouen un altre requisit: que la partida no tingui vàries solucions, sinó només una de sola. Per tant, haurem de modificar el nostre solver perquè també ens determini si té més d'una solució. Si ens dona una 2a solució, tornarem a descartar la partida.

Crea una partida i, abans de passar-la a l'usuari, comprova que té només 1 possible solució, fent ús del solver.