Sprawdzam Sudoku - Kurs Unity - 6. Podpowiedzi

W poprzednim kroku zaimplementowaliśmy zasady gry. W tym kroku dodamy podpowiedzi pomagające rozwiązać Sudoku:

Implementacja

Zacznijmy od podpowiedzi w roboczej komórce. Chcemy, aby podpowiedziała ona niektóre opcje w sposób wizualny, tj. wartość która jest jedyną możliwą w danej kolumnie oznaczymy na niebiesko (#1E88E5), w danym wierszu, na czerwono (#D81B60), a w danym kwadracie 3x3 na żółto (#FFC107).

Pobieramy darmową ikonę celownika z pixabay.com, którą następnie korzystając z GIMP'a przerabiamy i przemalowujemy na wybrane kolory:


Do możliwej wartości (PossibilityRect) dodajemy obrazy (OnlyValueColImageOnlyValueRowImageOnlyValueGroupImage) obrócone tak, aby możliwe było wyświetlenie kilku podpowiedzi jednocześnie (jeśli dana wartość może być jedyna np. zarówno w kolumnie jak i wierszu):


Do możliwej wartości (PossibilityRect) dodajemy metody aktywujące podpowiedzi:

public class PossibilityRect : MonoBehaviour
{
    public GameObject onlyValueColImage;
    public GameObject onlyValueRowImage;
    public GameObject onlyValueGroupImage;

    ...

    public void ClearOnlyValue()
    {
        onlyValueColImage.SetActive(false);
        onlyValueRowImage.SetActive(false);
        onlyValueGroupImage.SetActive(false);
    }

    public void DisplayOnlyValueForType(CellSetType cellSetType)
    {
        switch (cellSetType)
        {
            case CellSetType.COLUMN:
                onlyValueColImage.SetActive(true);
                break;
            case CellSetType.ROW:
                onlyValueRowImage.SetActive(true);
                break;
            case CellSetType.GROUP:
                onlyValueGroupImage.SetActive(true);
                break;
        }
    }
}

Pytanie 1:
Który element powinien włączać podpowiedzi w roboczej komórce?

Możliwości:
  • Robocza komórka (WorkCellImage)
  • Wybrana komórka (CellImage)
Decyzja:
Aby stwierdzić, że w danej kolumnie, wierszu, lub kwadracie 3x3 dana wartość nie może wystąpić, potrzebny jest dostęp do modelu. Robocza komórka (WorkCellImage) tego dostępu nie ma, więc ustawianie podpowiedzi dodajemy do wybranej komórki (CellImage):

public class CellImage : MonoBehaviour {
    ...
    
    public void ClearPossibilityOnlyValue(PossibilityRect possibility)
    {
        possibility.ClearOnlyValue();
    }

    public void DisplayPossibilityOnlyValue(PossibilityRect possibility)
    {
        foreach (CellSetType cellSetType in model.GetCellSetTypesWithOnlyValue(possibility.Value))
        {
            possibility.DisplayOnlyValueForType(cellSetType);
        }
    }
}
Aby to zadziałało, model musi mieć metodę GetCellSetTypesWithOnlyValue która sprawdzi w których zbiorach komórek dana wartość jest jedyną możliwą. Implementujemy metody CanHaveValue oraz GetCellSetTypesWithOnlyValue w modelu komórki. Dodatkowo implementujemy metodę MustHaveValue:

public class CellModel
{
    ...

    public void SetValue(int value)
    {
        Debug.AssertFormat(
            CanHaveValue(value),
            "Invalid value being set: {0}", value);
        ...
    }

    public bool CanHaveValue(int value)
    {
        if (value == 0)
        {
            return true;
        }
        if (Value == 0)
        {
            return GetCellSetTypesHavingValue(value).Count == 0;
        }
        else
        {
            return value == Value;
        }
    }

    public List<CellSetType> GetCellSetTypesWithOnlyValue(int value)
    {
        List<CellSetType> result = new List<CellSetType>();
        foreach (CellSetModel cellSet in cellSets)
        {
            if (!cellSet.CanAnyOtherCellHaveValue(this, value))
            {
                result.Add(cellSet.Type);
            }
        }
        return result;
    }

    public bool MustHaveValue(int value)
    {
        return value != 0 && GetCellSetTypesWithOnlyValue(value).Count != 0;
    }
}
W modelu zbioru komórek musimy zaimplementować metodę CanAnyOtherCellHaveValue:

public class CellSetModel
{
    ...

    public bool CanAnyOtherCellHaveValue(CellModel ignoredCell, int value)
    {
        foreach (CellModel cell in cells)
        {
            if (!cell.Equals(ignoredCell) && cell.CanHaveValue(value))
            {
                return true;
            }
        }
        return false;
    }
}
Teraz robocza komórka musi aktywować podpowiedzi kiedy tylko wyświetlona jest możliwość, której wartość nie może wystąpić nigdzie indziej. Robimy to w dwóch miejscach:

- metodach DisablePossibility oraz EnablePossibility wybranej komórki (CellImage):

public class class CellImage : MonoBehaviour {
    ...
    
    public void DisablePossibility(PossibilityRect possibility)
    {
        ...
        if (possibility.Value == model.Value)
        {
            ...
        }
        else
        {
            ...
            ClearPossibilityOnlyValue(possibility);
        }
        ...
    }

    public void EnablePossibility(PossibilityRect possibility)
    {
        ...
        if (possibility.Value == model.Value)
        {
            ...
        }
        else
        {
            ...
            DisplayPossibilityOnlyValue(possibility);
        }
    }
}
- metodzie SelectPossibility roboczej komórki (WorkCellImage):

public class WorkCellImage : MonoBehaviour
{
    ...

    public void SelectPossibility(PossibilityRect possibility)
    {
        ...
        if (selectedPossibility == possibility)
        {
            ...
            selectedCell.DisplayPossibilityOnlyValue(possibility);
        }
        else
        {
            ...
            selectedCell.ClearPossibilityOnlyValue(selectedPossibility);
        }
    }
}
Następnym krokiem jest pokazanie podpowiedzi na planszy, jeżeli pusta komórka zawiera jakąkolwiek podpowiedź.

Do wybranej komórki (CellImage) dodajemy pole tekstowe (SuggestionText), aby możliwe było wyświetlenie podpowiedzi:


Do wybranej komórki (CellImage) dodajemy metody CalculateSuggestion oraz SetSuggestion wyliczające i ustawiające podpowiedź:

public class CellImage : MonoBehaviour
{
    ...
    public TextMeshProUGUI suggestionText;
    ...

    public void SetSuggestion(int value)
    {
        suggestionText.SetText(model.Value == 0 && value != 0 ? value.ToString() : "");
    }

    public int CalculateSuggestion()
    {
        int largestRequiredValue = 0;
        int largestPossibleValue = 0;
        int possibleValueCount = 0;
        for (int value = 1; value <= 9; ++value)
        {
            if (model.MustHaveValue(value))
            {
                Debug.AssertFormat(
                    largestRequiredValue == 0,
                    "Multiple required values: {0} and {1}", largestRequiredValue, value);
                Debug.AssertFormat(
                    model.CanHaveValue(value),
                    "Impossible value required: {0}", value);
                largestRequiredValue = value;
            }
            if (model.CanHaveValue(value))
            {
                ++possibleValueCount;
                largestPossibleValue = value;
            }
        }
        if (largestRequiredValue != 0)
        {
            return largestRequiredValue;
        }
        else if (possibleValueCount == 1)
        {
            return largestPossibleValue;
        }
        else
        {
            return 0;
        }
    }
}
Z planszy (BoardImage) robimy singleton, zapamiętujemy wszystkie komórki w zmiennej i dodajemy metodę uaktualniającą sugestie dla wszystkich komórek:

public class BoardImage : MonoBehaviour
{
    public static BoardImage Instance { get; private set; }

    private CellImage[] cellImages;
    ...

    private void Awake()
    {
        cellImages = GetComponentsInChildren<CellImage>();

        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            Instance = this;
        }

        model = new BoardModel(cellImages);

        ...
    }

    ...

    public void UpdateAllCellSuggestions()
    {
        foreach(CellImage cellImage in cellImages)
        {
            cellImage.SetSuggestion(cellImage.CalculateSuggestion());
        }
    }
}
Metodę tą wołamy przy każdej zmianie wartości komórki (CellImage):

public class CellImage : MonoBehaviour
{
    ...

    public void SetValue(int value)
    {
        ...
        BoardImage.Instance.UpdateAllCellSuggestions();
    }

    ...
}

Na koniec, jeżeli komórka posiada podpowiedź, chcemy uniemożliwić wybranie jakiejkolwiek innej wartości. Robimy to w metodzie SelectPossibility roboczej komórki (WorkCellImage):

public class WorkCellImage : MonoBehaviour
{
    ...

    public void SelectPossibility(PossibilityRect possibility)
    {
        Debug.Assert(selectedCell != null, "Selected cell cannot be null.");
        Debug.Assert(possibility != null, "Possibility cannot be null.");
        // Do not allow selecting possibility other than the suggestion.
        int selectedCellSuggestion = selectedCell.CalculateSuggestion();
        if (selectedCellSuggestion != 0 && selectedCellSuggestion != possibility.Value)
        {
            return;
        }
        ...
    }
}

Otrzymujemy aplikację pokazaną we wstępie.

GitHub

Zmiany związane z tym krokiem znajdują się tutaj:

Kolejne kroki

Istniejąca aplikacja pozwala całkowicie rozwiązać nasze przykładowe Sudoku. W następnym kroku dodamy legendę, zapisywanie stanu aplikacji oraz tłumaczenie na inne języki.

Polski | Angielski

Komentarze

Popularne posty z tego bloga

Sprawdzam Sudoku - Kurs Unity - 2. Konfiguracja

Sprawdzam Sudoku - Kurs Unity - 3. Szkielet aplikacji

Sprawdzam Sudoku - Kurs Unity - 1. Pomysł