Laboration 3. Sudoku

Uppgiften är att konstruera ett program i vilket man kan lösa Sudoku-problem. Som du säkert vet utgörs en Sudoku-plan av ett rutnät med nio rader och nio kolumner. Denna plan är dessutom indelad i nio delplaner, s.k. block, bestående av tre rader och tre kolumner. Detta demonstreras i nedanstående figur.

Varje ruta kan innehålla en siffra eller vara blank. I varje Sudoku-problem är siffrorna i vissa rutor givna. Dessa har markerats med grå bakgrundsfärg i exemplet. Det gäller sedan att fylla i planen på ett sådant sätt att varje rad, kolumn och block innehåller exakt en förekomst av siffrorna ett till nio, så som visas i nästa figur.

När man kör programmet kan man ladda in ett nytt Sudoku-problem genom att klicka på knappen New och sedan välja en textfil som innehåller problemet. Filen skall innehålla nio rader med nio tecken i varje rad. Om ett tecken inte är en siffra i intervallet ett till nio tolkas det som en blank ruta. Filen med problemet ovan har t.ex. följande utseende:

3...8....
...7....5
1........
......36.
..2..4...
.7.......
....6.13.
.452.....
......8..

Den som kör programmet kan sedan försöka fylla i lämpliga siffror i rutorna. Om man försöker fylla i en siffra som redan finns i den aktuella raden eller kolumnen eller i det aktuella blocket skall programmet vägra att lägga in denna siffra och istället ge ifrån sig ett ljud. Man skall också kunna ångra siffror man fyllt i genom att klicka på knappen Undo. Detta skall kunna göras inte bara för den sista siffran, utan för alla inmatade siffror. På motsvarande sätt skall man med hjälp av knappen Redo återställa siffror som man ångrat. Om man vill att programmet skall visa lösningen på problemet klickar man på knappen Solve. Knappen Make solvable, slutligen, tar bort de senaste siffrorna man lagt in tills alla siffror i planen är korrekta och problemet därför är lösbart.

I uppgiften skall den s.k. Model-View-Controller tekniken användas för att strukturera programmet på ett bra sätt. För att kunna använda denna teknik bör du ha läst avsnitt 12.10 i kursboken. Laborationen kan lösas i tre steg där man i varje steg koncentrerar sig på var och en av dessa komponenter.

Steg 1. Modellen

1 A. Grundläggande egenskaper

I detta steg skall du konstruera en modell som beskriver en Sudoku-plan. Det naturliga sättet att beskriva en sådan är att använda sig av en tvådimensionell array med nio rader och nio kolumner där elementen har typen int och bara får innehålla värden i intervallet 0-9. Man låter då värdet 0 betyda en blank ruta. Konstruera därför en klass med namnet MySudokuModel som innehåller en sådan array som privat instansvariabel. Modellen skall vara så utformad att den alltid garanterar att det inte finns flera siffror av samma slag i en rad, en kolumn eller ett block. (Däremot skall det vara möjligt att lägga in data som beskriver icke lösbara problem eller problem som inte har en unik lösning.)

Klassen MySudokuModel skall ha två konstruktorer, en parameterlös som initierar modellen så att alla rutor blir tomma och en kopieringskonstruktor som skapar en kopia av en redan existerande modell.

I klassen skall också metoden toString överskuggas (override). Denna skall ge samma resultat som metoden getBoard (se nedan).

Utforma klassen så att den (till att börja med)  implementerar följande gränssnitt SudokuModel:

public interface SudokuModel {
  void clear();
  void setBoard(int row, int col, int val);
  void setBoard(String input);
  int  getBoard(int row, int col);
  String getBoard();
  boolean isLegal(int row, int col, int val);
  boolean solve();
  boolean isSolvable();
  boolean isUnique();
}

Metoden clear är enkel. Den skall nollställa alla rutorna i modellen.

Det första versionen av setBoard skall sätta värdet i en viss ruta. Det andra versionen av setBoard skall sätta värden i alla rutorna. Som indata får den en String som skall bestå av nio rader med nio tecken på varje rad. (Raderna avgränsas med tecknet \n.) Alla tecken som inte är siffror i intervallet1-9 skall uppfattas som tomma rutor och skall alltså representeras med talet 0 i modellen. Båda versionerna av setBoard skall kontrollera att man inte försöker placera flera siffror av samma slag i en rad, i en kolumn eller i ett block. I så fall skall en exception av typen IllegalArgumentException genereras. Den andra versionen av  av setBoard skall dessutom kontrollera att den String den får som indata innehåller det rätta antalet rader och antal tecken per rad. Om så inte är fallet skall den också då generera en  exception av typen IllegalArgumentException. Exceptions som genereras skall innehålla lämpliga felmeddelanden.

Det första versionen av getBoard skall avläsa värdet i en viss ruta. Den andra versionen av getBoard skall som resultat ge en String som innehåller värdet av samtliga rutor.  Formatet på denna String skall vara samma in inparametern till den andra versionen av setBoard. Blanka rutor kan enklast representeras med tecknet ´0´.

Metoden isLegal skall kontrollera att det går att lägga in det angivna värdet val i en viss ruta. Den skall alltså kontrollera att det i så fall inte kommer att finnas flera siffror av samma slag i en rad, i en kolumn eller i ett block. Tänk på att det alltid skall gå att lägga in en blank ruta och att om modellen hittills är legal så skall man alltid i en ruta kunna lägga in samma siffra som den redan innehåller.

De tre sista metoderna i gränssnittet ovan blir förstås lite mer komplicerade. Metoden solve skall försöka lösa Sudoku-problemet utgående från det tillstånd modellen befinner sig i. Om solve hittar en lösning skall den fylla i lösningen i modellen och returnera värdet true. Om solve inte hittar någon lösning skall den lämna modellen i samma tillstånd som när den anropades och returnera värdet false. Metoden solve skall vara rekursiv enligt följande idé:

- Om det inte finns några fler tomma rutor (dvs. rutor med värdet 0) så har lösningen hittats.
- Välj annars ut en lämplig (se nedan) tom ruta och prova för alla värden 1-9 om det går att sätta in detta värde i rutan. Om detta är möjligt så sätt in värdet i modellen och prova om det i så fall går att lösa problemet. (Använd rekursion!). Om det inte är möjligt så återställ rutan så att den blir tom och prova med nästa värde. Om det inte gick att sätta in något av värdena 1-9 i den valda rutan så går det inte att lösa problemet utgående från modellens aktuella tillstånd.

För att denna teknik skall vara effektiv är det viktigt att man väljer en tom ruta för vilken det finns så få siffror som möjligt som kan läggas i rutan. Man kan därför välja en ruta för vilken summan av antalet tomma rutor i dess rad, kolumn och block är minimal.

Metoden isSolvable blir nu enkel. Eftersom metoden solve returnerar ett värde av typen boolean kan man anropa solve för att se om ett problem är lösbart. Men om man anropar metoden solve för den aktuella modellen kommer den att fyllas i med lösningen och detta är inte meningen att isSolvable skall göra. Ett enkelt knep är därför att skapa en kopia av den aktuella modellen och anropa metoden solve för kopian.

Metoden isUnique kan faktiskt göras förvånansvärt enkel med hjälp av ett annat knep. Man anropar solve och låter solve bortse från den första (och kanske enda) lösning den finner. Om solve därefter finner någon lösning, så kan inte lösningen vara unik. Det behövs alltså en tillståndsvariabel som anger om man undersöker om lösningen är unik. När solve har funnit en lösning undersöker den om man söker en unik lösning. I så fall bortser den från lösningen och markerar att man inte längre söker efter en unik lösning.

När du fått färdigt klassen MySudokuModel så här långt kan du testa den med hjälp av det färdiga klassen SudokuTest1. Denna innehåller en main-metod som låter dig läsa in Sudoku-problem från filer och som försöker lösa dem med hjälp av din modell-klass. Det finns ett stort antal filer med Sudoku-problem i mappen Sudokuproblem. Där finns också filer med felaktiga data så att man kan testa att metoden getBoard gör de rätta kontrollerna.

Gå inte vidare till nästa moment förrän din modell-klass fungerar som den skall.

1 B. Infoga modellen i MVC-konceptet

Nu skall klassen MySudokuModel utökas så att den kan användas i Model-View-Controller konceptet. Utgå från följande gränssnitt med namnet Model:

import java.beans.*;
import java.io.*;

public interface Model extends Serializable {
  void addPropertyChangeListener(PropertyChangeListener l);
  void removePropertyChangeListener(PropertyChangeListener l);
}

och justera sedan gränssnittet SudokuModel från förra avsnittet så att det utökar detta gränssnitt:

public interface SudokuModel extends Model {
  som tidigare
}

Detta innebär att din klass MySudokuModel måste implementera de två metoderna i gränssnittet Model. Använd dig av hjälpklassen PropertyChangeSupport på det sätt som diskuteras i kursboken och som visas i Javas API. Förutom att implementera de två metoderna måste klassen MySudokuModel utökas så att den rapporterar ändringar i modellen till eventuella lyssnare. På tre ställen måste det rapporteras att hela modellen ändrats. Detta gäller när man nollställt hela modellen, när man lagt in ett nytt sudoku-problem med hjälp av den andra versionen av metoden setBoard samt när man löst problemet och fyllt i lösningen i modellen. Då kan man använda metoden firePropertyChange. Man skall också  i den första versionen av metoden setBoard rapportera när en enstaka ruta ändrats. Det är då lämpligt att använda metoden fireIndexedPropertyChange. Denna skall som extra parameter ha ett index som anger vilken egenskap som ändrats. Man kan där ge rutans nummer (radnummer * 9 + kolumnnummer).

Steg 2. Vyn

2 A. Skapa det grafiska gränssnittet

Vyn utgörs förstås av av en panel som innehåller ett rutnät med nio gånger nio rutor såsom framgår av figurerna ovan. Skriv en klass MySudokuView som beskriver denna panel. Se då till att de nio blocken också markeras. Eftersom vyn skall vara lyssnare till händelser som uppstår i modellen måste den implementera gränssnittet PropertyChangeListener. Konstruktorn måste ha två parametrar: en referens av typen SudokuModel och en referens av typen SudokuController. Den första behövs för att vyn skall kunna fråga modellen vilket värde som finns i en viss ruta. Den andra behövs för att de enskilda rutorna i vyn också kommer att fungera som inmatningsfält i vilka användaren kan skriva data. (Detta diskuteras senare).

Börja med att  i Vyns konstruktor skapa rutnätet. Ett bra tips är att låta varje ruta vara ett objekt av en egendefinierad subklass Square till JTextField. Låt objekt av klassen Square förutom texten (som den ärver från sin superklass) även innehålla rutans rad och kolumnnummer. (Detta underlättar programmeringen senare när du skall skriva lyssnare för inmatade värden.)

2 B. Skapa lyssnare

När händelser av typen PropertyChangeEvent mottas från modellen skall vyn uppdateras. Om händelsen är av subklassen IndexedPropertyChangeEvent skall bara den enskilda ruta det gäller ändras, annars måste alla rutorna ritas om. I det senare fallet kan alla rutor som inte är tomma sättas som icke ändringsbara. Det är nämligen då fråga om att man läst in ett nytt problem eller att man visar en lösning.

När du kommit så här långt kan du testa din Vyklass med hjälp av testprogrammet SudokuTest2. Detta program visar vyn och kopplar ihop den med din modellklass. När du klickar på knapparna Fill och Clear i programmet skall din vy uppdateras.

2 C. Ta hand om inmatade värden

Användaren måste kunna skriva in siffror i rutorna. För att rutorna i vyn också skall kunna användas för inmatning, måste man i klassen MySudokuView lyssna efter händelser i dessa. Det hade kunnat gå att använda händelser av typen ActionPerformed, men det hade haft nackdelen att användaren hade varit tvungen att trycka på Enter-tangenten efter varje inmatning. Det är därför bättre att lyssna efter händelser av typen keyReleased. Man kan då direkt, genom att anropa metoden getKeyChar, känna vilket tecken användaren skrev.

Vyn måste rapportera till controllern vad som matats in. Controllern skall implementera gränssnittet SudokuController, vilket  är mycket enkelt. Det innehåller bara en metod, med namnet input. Det är denna metod som vyn skall anropa för att skicka det inmatade tecknet vidare till controllern.

public interface SudokuController {
  boolean input(int row, int col, char value);
}

Metoden  input ger som resultat värdet true om värdet som skickades vidare till controllern var korrekt och kunde läggas in i modellen. Annars returneras false. I så fall kan man i vyn anropa metoden beep och lägga i en blank text i den aktuella rutan.

(Det är alltså omöjligt att helt upprätthålla skillnaden mellan vy och controller. Men om man inte behandlar några indata i vyn utan bara skickar dem vidare, så kan strukturen hållas ren i alla fall.)

Du kan använda testprogrammet SudokuTest2 också för att testa att din vyklass tar hand om inmatade data på rätt sätt.  När ett tecken matats in skall programmet skriva ut vilket tecken som matats in och i vilken ruta det skett.

Steg 3: Controllern

Controllern skall sköta kommunikationen med användaren och uppdatera modellen. Grafiskt utgörs controllern av raden med knappar längst ner i fönstret. Den tar också emot rapporter från vyn när användaren skrivit något i en ruta. Konstruera en klass med namnet MySudokuController. Klassen skall vara en JPanel och dessutom implementera gränssnittet  SudokuController. Den måste förstås också kunna lyssna efter knapptryckningar.

Börja med att skriva den del av klassen som skall exekveras när användaren klickar på knappen New. Då skall en fildialogruta (se avsnitt 14.7 i kursboken) öppnas i vilken användaren kan välja en fil med ett Sudoku-problem. Om filen inte går att öppna skall ett felmeddelande visas. Därefter skall informationen från filen läggas in i modellen. Ett lämpligt felmeddelande skall visas om filen visade sig innehålla felaktiga data. Man skall också visa ett felmeddelande om det inte finns någon lösning till problemet eller om en lösning inte är unik.

Skriv därefter metoden input som anropas från vyn när användaren skrivit ett tecken i en ruta. När detta är gjort går det att börja testa hela programmet. Detta kan du göra med hjälp av den färdiga klassen SudokuMain som skapar modellen, vyn och controllern, kopplar ihop dem och lägger in vyn och controllern i ett fönster. Så här ser denna klass ut:

import java.awt.*;
import javax.swing.*;

public class SudokuMain extends JFrame {

  public SudokuMain() {
    SudokuModel model       = new MySudokuModel();
    MySudokuController ctrl = new MySudokuController(model);
    MySudokuView view       = new MySudokuView(model, ctrl);
    add(view, BorderLayout.CENTER);
    add(ctrl, BorderLayout.SOUTH);
    setSize(420,420);
    setLocationRelativeTo(null); // centrera
    setVisible(true);
    setDefaultCloseOperation(EXIT_ON_CLOSE);
  }

  public static void main(String[] arg) {
    new SudokuMain();
  }
}

Det kan sedan vara lämpligt att skriva de delar av programmet som hanterar tryckningar på knappen Solve. Tänk på att ett problem kan ha blivit olösbart om användaren skrivit in siffror i rutorna. Vill du lösa problemet kan du behöva ladda in ursprungsproblemet igen.

Historiemekanismen (frivillig uppgift)

Det sista momentet är att implementera den historiemekanism som behövs för att hantera tryckningar på knapparna Undo, Redo och Make solvable. Man måste för varje drag, dvs. siffra som användaren placerar ut i spelplanen, komma ihåg vilken ruta det gäller och  rutans tidigare och nya värde. För detta kan man använda objekt av en enkel hjälpklass Move (som man förstås måste konstruera själv). Man kan sedan lägga Move-objekt i en lista som utökas för varje nytt drag. Om användaren klickar på Undo kan man då gå tillbaka och återställa det tidigare värdet i en ruta. Man kan emellertid inte då utan vidare stryka de drag som är gjorda från listan eftersom det skall vara möjligt att göra Redo. Om användaren har backat och sedan skriver in något nytt värde i en ruta kan man emellertid ta bort de drag som ligger senare i listan.

När användaren klickar på Make solvable kan man helt enkelt backa i listan tills problemet är lösbart. Det bör man också göra när användaren klickat på  Solve.

Fler frivilliga tillägg