Check Sudoku - Unity Tutorial - 6. Suggestions

In the previous step we implemented game rules. In this one we will add suggestions helping solve the Sudoku:

Implementation

Let's start from the suggestions in the work cell. We want it to suggestion options visually, i.e. the only possible value in the column will be marked blue (#1E88E5), in the row will be marked red (#D81B60), in the 3x3 square will be marked yellow (#FFC107).

We download a free crosshair icon from pixabay.com, which, using GIMP, we turn into three other colors:


In the possibility (PossibilityRect) we add three images (OnlyValueColImage, OnlyValueRowImage, OnlyValueGroupImage) rotated so that a few of them can be displayed at the same time (if e.g. given value is the only one both in the column, and in the row):


We add methods activating suggestions to the possibility (PossibilityRect):

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;
        }
    }
}

Question 1:
Which element should turn on the suggestions in a work cell?

Possibilities:
  • The work cell (WorkCellImage)
  • The selected cell (CellImage)
Decision:
In order to figure out that a value cannot occure anywhere else in the column, row, or 3x3 square, we need access to the model. The work cell (WorkCellImage) doesn't have it, so we add setting the suggestions to the selected cell (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);
        }
    }
}
In order for this to work, the model needs a GetCellSetTypesWithOnlyValue method, which returns the cell sets, where given value is the only possible. We implement metods CanHaveValue and GetCellSetTypesWithOnlyValue in the cell model. Additionally, we implement the MustHaveValue method:

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;
    }
}
In the cell set model, we need to implement the CanAnyOtherCellHaveValue method:

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;
    }
}
Now, the work cell needs to activate suggestions whenever a possibility, that cannot occur anywhere else, is displayed. We do that in two places:

- methods DisablePossibility and EnablePossibility of the selected cell (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);
        }
    }
}
- method SelectPossibility of the work cell (WorkCellImage):

public class WorkCellImage : MonoBehaviour
{
    ...

    public void SelectPossibility(PossibilityRect possibility)
    {
        ...
        if (selectedPossibility == possibility)
        {
            ...
            selectedCell.DisplayPossibilityOnlyValue(possibility);
        }
        else
        {
            ...
            selectedCell.ClearPossibilityOnlyValue(selectedPossibility);
        }
    }
}
The next step is to display suggestions in the empty cells of the board.

We add a text field (SuggestionText) into to the selected cell (CellImage) to be able to display a suggestion:


We add methods CalculateSuggestion and SetSuggestion to the selected cell (CellImage) which calculate and set the suggestion:

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;
        }
    }
}
We turn the board (BoardImage) into a singleton, memorize all the cells, and add a method which updates all cell suggestions:

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());
        }
    }
}
We invoke this method with every value change of a cell (CellImage):

public class CellImage : MonoBehaviour
{
    ...

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

    ...
}

Finally, when a cell has a suggestion, we want to disallow selecting any other value. We do this in the SelectPossibility method of the work cell (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;
        }
        ...
    }
}

That's how we get the app from the introduction.

GitHub

Commits related to this step are here:

Next steps

The app now allows solving our example sudoku. In the next step we will add the legend, persistence of application state, and translation to other languages.

Polski | Angielski

Comments

Popular posts from this blog

Check Sudoku - Unity Tutorial - 3. App skeleton

The Beginnings

Check Sudoku - Unity Tutorial - 4. Setting values