Guia per desenvolupar un joc de Sudoku en Android.
Els principals objectius son:
Se suposa que coneixes les bases de Android.
Crea un nou projecte amb una EmptyActivity
i puja el repo a Git.
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.
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:
onItemSelected
: és on activarem la lògica del nostre joc.onNothingSelected
: és obligatoria implementar-la, però segurament no hi posarem res de codi.
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».
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
:
<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.
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:
getVal
: per obtenir una dada del model.setVal
: per intentar canviar un número de la partida de Sudoku. Si no pot settejar-lo (el valor és incorrecte), ens retorna el valor -1 (el 0 el reservem per a la cel·la buida.comprovaFila
: comprova que la fila indicada als paràmetres compleix les normes del Sudoku.comprovaCol
: ídem per la columna.comprovaQuad
: ídem pel quadrant.creaPartida
: ens crea uns quants elements aleatoris per iniciar una partida. Aquests elements aleatoris han de complir amb les regles del Sudoku.Implementa el model per al Sudoku com s'ha descrit.
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.
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
.
refrescaGUI()
: traspassa les dades del model a la interfície gràfica (els Spinners).
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.
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.
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.
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.
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.
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.