Brewsterware

September 30, 2020

Sudoku cracker for Dynamics 365 Finance and Operations

Filed under: 365 for Finance and Operations — Joe Brewer @ 12:44 pm

It’s been a long time since I have done any coding just for fun, and I rarely do it with X++, but a sudoku cracker for Finance and Operations seemed like a good challenge.

This cracker will crack even the most toughest of puzzles by using recursion and backtracking for when there are multiple possibilities for values in a cell.

/// <summary>
/// Sudoku puzzle cracker
/// </summary>
class SudokuCracker
{
    // this macro allows us to specify the line and column of the grid array
    #localMacro.GridIndex
        (%1 - 1) * 9 + %2
    #endMacro

    private const int TotalRows = 9;
    private const int TotalColumns = 9;

    private int grid[TotalRows, TotalColumns];
    private int cellsFilled;

    /// <summary>
    /// Runs the class with the specified arguments.
    /// </summary>
    /// <param name = "_args">The specified arguments.</param>
    public static void main(Args _args)
    {
        SudokuCracker cracker = new SudokuCracker();

        cracker.loadGrid();
        cracker.solve();
        cracker.saveGrid();
    }

    /// <summary>
    /// import a file with the known numbers and populate a 2 dimensional array with them
    /// </summary>
    public void loadGrid()
    {
        System.String stringLine;
        str line;
        int gridLine = 0;
        var fileUpload = File::GetFileFromUser() as FileUploadTemporaryStorageResult;

        using (var reader = new System.IO.StreamReader(fileUpload.openResult()))
        {
            stringLine = reader.ReadLine();

            while (!System.String::IsNullOrEmpty(stringLine))
            {
                line = strKeep(stringLine, '123456789 ');

                if (line)
                {
                    gridLine++;

                    for (int i = 1; i <= TotalRows; i++)
                    {
                        int gridIndex = #GridIndex(gridLine, i);
                        int value = str2Int(subStr(line, i, 1));

                        grid[gridIndex] = value;

                        if (value)
                        {
                            cellsFilled++;
                        }
                    }
                }

                stringLine = reader.ReadLine();
            }
        }
    }

    /// <summary>
    /// create a pretty grid with the numbers filled in, and send it back to the user
    /// </summary>
    public void saveGrid()
    {
        TextBuffer output;

        output = new TextBuffer();

        for (int line = 1; line <= TotalRows; line++)
        {
            output.appendText(strFmt('%1%2%3|%4%5%6|%7%8%9\r\n', 
                grid[#GridIndex(line, 1)], 
                grid[#GridIndex(line, 2)], 
                grid[#GridIndex(line, 3)],
                grid[#GridIndex(line, 4)],
                grid[#GridIndex(line, 5)],
                grid[#GridIndex(line, 6)],
                grid[#GridIndex(line, 7)],
                grid[#GridIndex(line, 8)],
                grid[#GridIndex(line, 9)]));

            if (line mod 3 == 0 &&
                line != TotalRows)
            {
                output.appendText('---+---+---\r\n');
            }
        }

        File::SendStringAsFileToUser(output.getText(), 'solved.txt');
    }

    /// <summary>
    /// method to determine whether a value is valid at a specific line and column
    /// </summary>
    /// <param name="_line">The line number of the grid</param>
    /// <param name="_column">The column number of the grid</param>
    /// <param name="_value">An integer value to be tested</param>
    /// <returns>true if the value is valid, false if not</returns>
    private boolean isValuePossible(int _line, int _column, int _value)
    {
        // check to see whether the value is possible in the line
        for (int i = 1; i <= TotalRows; i++)
        {
            if (grid[#GridIndex(_line, i)] == _value)
            {
                return false;
            }
        }

        // check to see whether the value is possible in the column
        for (int i = 1; i <= TotalColumns; i++)
        {
            if (grid[#GridIndex(i, _column)] == _value)
            {
                return false;
            }
        }

        // check to see whether the value is possible in the square

        // work out the starting point for the square
        int line = real2int(roundDownDec((_line - 1) / 3, 0)) * 3;
        int column = real2int(roundDownDec((_column - 1) / 3, 0)) * 3;

        for (int i = 1; i <= 3; i++)
        {
            for (int j = 1; j <= 3; j++)
            {
                int gridIndex = #GridIndex(line + i, column + j);

                if (grid[gridIndex] == _value)
                {
                    return false;
                }
            }
        }

        return true;
    }

    /// <summary>
    /// recursive backtracking method which fills in the missing numbers to solve the puzzle
    /// </summary>
    public void solve()
    {
        for (int line = 1; line <= TotalRows; line++)
        {
            for (int column = 1; column <= TotalColumns; column++)
            {
                if (grid[#GridIndex(line, column)] == 0)
                {
                    for (int value = 1; value <= 9; value++)
                    {
                        if (this.isValuePossible(line, column, value))
                        {
                            cellsFilled++;
                            grid[#GridIndex(line, column)] = value;

                            this.solve();

                            // have we finished?
                            if (cellsFilled == (TotalRows * TotalColumns))
                            {
                                return;
                            }

                            // start backtracking
                            cellsFilled--;
                            grid[#GridIndex(line, column)] = 0;
                        }
                    }

                    // nothing to see here, move along please.....
                    return;
                }
            }
        }
    }
}

The format of the file that this class uses is an ASCII based grid using pipes, hyphens and plus symbols to separate the nine squares of the sudoku grid. Below is an example that can be used to test the code – copy it into notepad and save it somewhere where the 365FO client can access it.

  9| 85|   
 6 |   |  9
 78|   | 14
---+---+---
   |   |   
  5| 18|   
   |7  |482
---+---+---
   |  7| 4 
2  |6 9|   
 8 |   | 7 

Run the class and choose and upload the file that you created in the above step. After a few moments you will be prompted to download a file which should contain the solution.

Happy cracking!

Powered by WordPress