Skip to content
Snippets Groups Projects
board.dart 16.1 KiB
Newer Older
Benoît Harrault's avatar
Benoît Harrault committed
import 'dart:math';

import 'package:flutter_custom_toolbox/flutter_toolbox.dart';

import 'package:suguru/models/activity/cell.dart';
import 'package:suguru/models/activity/cell_location.dart';
Benoît Harrault's avatar
Benoît Harrault committed
import 'package:suguru/models/activity/move.dart';
Benoît Harrault's avatar
Benoît Harrault committed
import 'package:suguru/models/activity/types.dart';
Benoît Harrault's avatar
Benoît Harrault committed
import 'package:suguru/utils/suguru_solver.dart';
Benoît Harrault's avatar
Benoît Harrault committed

class Board {
  Board({
    required this.cells,
    required this.solvedCells,
  });

  BoardCells cells = const [];
  BoardCells solvedCells = const [];

  factory Board.createEmpty() {
    return Board(
      cells: [],
      solvedCells: [],
    );
  }

  factory Board.createFromCells({
    required BoardCells cells,
    required BoardCells solvedCells,
  }) {
    return Board(
      cells: cells,
      solvedCells: solvedCells,
    );
  }

  void createFromTemplate({
    required String template,
  }) {
Benoît Harrault's avatar
Benoît Harrault committed
    printlog('Creating board from template:');
    printlog(template);

Benoît Harrault's avatar
Benoît Harrault committed
    final List<String> templateParts = template.split(';');
    if (templateParts.length != 3) {
      printlog('Failed to get grid template (wrong format)...');
    }

    final String boardSizeAsString = templateParts[0];
    final String blocksDefinitionAsString = templateParts[1];
    final String fixedCellsDefinitionAsString = templateParts[2];

    final int boardSizeHorizontal = int.parse(boardSizeAsString.split('x')[0]);
    final int boardSizeVertical = int.parse(boardSizeAsString.split('x')[1]);

    const String stringValues = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';

    int index = 0;
    for (int rowIndex = 0; rowIndex < boardSizeVertical; rowIndex++) {
      final List<Cell> row = [];
      for (int colIndex = 0; colIndex < boardSizeHorizontal; colIndex++) {
        final String blockId = blocksDefinitionAsString[index];
        final String cellValueAsString = fixedCellsDefinitionAsString[index];
        index++;

        final int cellValue = stringValues.indexOf(cellValueAsString);

        row.add(Cell(
          location: CellLocation.go(rowIndex, colIndex),
          blockId: blockId,
          value: cellValue,
          isFixed: (cellValue != 0),
        ));
      }
      cells.add(row);
    }

Benoît Harrault's avatar
Benoît Harrault committed
    // Do some transformations to board
    transformBoard();
Benoît Harrault's avatar
Benoît Harrault committed

    // Force cells fixed states (all cells with value != 0)
    for (CellLocation location in getCellLocations()) {
      final Cell cell = get(location);
      cells[location.row][location.col] = Cell(
        location: location,
        blockId: cell.blockId,
        value: cell.value,
        isFixed: (cell.value != 0) ? true : false,
      );
    }

Benoît Harrault's avatar
Benoît Harrault committed
    final Board solvedBoard = SuguruSolver.resolve(this);
    solvedCells = solvedBoard.cells;

    // FIXME: for debug only
    // to start with a board (almost) automatically solved
    // cells = solvedCells;
Benoît Harrault's avatar
Benoît Harrault committed
  }

  // Helper to create board from size, with "empty" cells
  static BoardCells createEmptyBoard(final int width, final int height) {
    final BoardCells cells = [];
    for (int rowIndex = 0; rowIndex < height; rowIndex++) {
      final List<Cell> row = [];
      for (int colIndex = 0; colIndex < width; colIndex++) {
        row.add(Cell(
          location: CellLocation.go(rowIndex, colIndex),
          blockId: '',
          value: 0,
          isFixed: false,
        ));
      }
      cells.add(row);
    }

    return cells;
  }

  List<CellLocation> getCellLocations([String? blockId]) {
    if (cells.isEmpty) {
      return [];
    }

    final List<CellLocation> locations = [];

    final int boardSizeVertical = cells.length;
    final int boardSizeHorizontal = cells[0].length;
    for (int row = 0; row < boardSizeVertical; row++) {
      for (int col = 0; col < boardSizeHorizontal; col++) {
        if ((blockId == null) || (blockId == get(CellLocation.go(row, col)).blockId)) {
          locations.add(CellLocation.go(row, col));
        }
      }
    }

    return locations;
  }

Benoît Harrault's avatar
Benoît Harrault committed
  void transformBoard() {
    final int boardSizeVertical = cells.length;
    final int boardSizeHorizontal = cells[0].length;

    const List<String> allowedFlip = ['none', 'horizontal', 'vertical'];
    List<String> allowedRotate = ['none', 'left', 'right', 'upsidedown'];

    // Limit rotation if board is not symetric
    if (boardSizeVertical != boardSizeHorizontal) {
      allowedRotate = ['none', 'upsidedown'];
    }

    final Random rand = Random();
    final String flip = allowedFlip[rand.nextInt(allowedFlip.length)];
    final String rotate = allowedRotate[rand.nextInt(allowedRotate.length)];

    switch (flip) {
      case 'horizontal':
        {
          transformFlipHorizontal();
        }
        break;
      case 'vertical':
        {
          transformFlipVertical();
        }
        break;
    }

    switch (rotate) {
      case 'left':
        {
          transformRotateLeft();
        }
        break;
      case 'right':
        {
          transformRotateRight();
        }
        break;
      case 'upsidedown':
        {
          transformFlipHorizontal();
          transformFlipVertical();
        }
        break;
    }
  }

Benoît Harrault's avatar
Benoît Harrault committed
  void transformFlipHorizontal() {
    final BoardCells transformedBoard = copyCells();
    final int boardSizeVertical = cells.length;

    for (CellLocation location in getCellLocations()) {
      final Cell cell = cells[boardSizeVertical - location.row - 1][location.col];
      transformedBoard[location.row][location.col] = Cell(
        location: location,
        blockId: cell.blockId,
        value: cell.value,
        isFixed: false,
      );
    }

    cells = transformedBoard;
  }

  void transformFlipVertical() {
    if (cells.isEmpty) {
      return;
    }

    final BoardCells transformedBoard = copyCells();

    final int boardSizeHorizontal = cells[0].length;

    for (CellLocation location in getCellLocations()) {
      final Cell cell = cells[location.row][boardSizeHorizontal - location.col - 1];
      transformedBoard[location.row][location.col] = Cell(
        location: location,
        blockId: cell.blockId,
        value: cell.value,
        isFixed: false,
      );
    }

    cells = transformedBoard;
  }

  void transformRotateLeft() {
    final BoardCells transformedBoard = copyCells();
    final int boardSizeVertical = cells.length;

    for (CellLocation location in getCellLocations()) {
      final Cell cell = cells[location.col][boardSizeVertical - location.row - 1];
      transformedBoard[location.row][location.col] = Cell(
        location: location,
        blockId: cell.blockId,
        value: cell.value,
        isFixed: false,
      );
    }

    cells = transformedBoard;
  }

  void transformRotateRight() {
    if (cells.isEmpty) {
      return;
    }

    final BoardCells transformedBoard = copyCells();

    final int boardSizeHorizontal = cells[0].length;

    for (CellLocation location in getCellLocations()) {
      final Cell cell = cells[boardSizeHorizontal - location.col - 1][location.row];
      transformedBoard[location.row][location.col] = Cell(
        location: location,
        blockId: cell.blockId,
        value: cell.value,
        isFixed: false,
      );
    }

    cells = transformedBoard;
  }

Benoît Harrault's avatar
Benoît Harrault committed
  bool inBoard(CellLocation location) {
    return (location.row >= 0 &&
        location.row < cells.length &&
        location.col >= 0 &&
        location.col < cells[location.row].length);
  }

Benoît Harrault's avatar
Benoît Harrault committed
  Cell get(CellLocation location) {
Benoît Harrault's avatar
Benoît Harrault committed
    if (inBoard(location)) {
      return cells[location.row][location.col];
Benoît Harrault's avatar
Benoît Harrault committed
    }

    return Cell.none;
  }

  void setCell(CellLocation location, Cell cell) {
Benoît Harrault's avatar
Benoît Harrault committed
    if (inBoard(location)) {
      cells[location.row][location.col] = cell;
    }
Benoît Harrault's avatar
Benoît Harrault committed
  }

  void setValue(CellLocation location, int value) {
    Cell currentCell = get(location);

    setCell(
        location,
        Cell(
          blockId: currentCell.blockId,
          isFixed: currentCell.isFixed,
          location: location,
          value: value,
        ));
  }

Benoît Harrault's avatar
Benoît Harrault committed
  void applyMove(Move move) {
    // printlog('put ${move.value} in ${move.location}');
    setValue(move.location, move.value);
  }

Benoît Harrault's avatar
Benoît Harrault committed
  List<String> getBlockIds() {
    List<String> blockIds = [];
    for (CellLocation location in getCellLocations()) {
      final String blockId = get(location).blockId;
      if (!blockIds.contains(blockId)) {
        blockIds.add(blockId);
      }
    }

    return blockIds;
  }

  List<int> getValuesInBlock(String blockId) {
    final List<int> values = [];

    for (CellLocation location in getCellLocations()) {
      if (get(location).blockId == blockId) {
        values.add(get(location).value);
      }
    }

    return values;
  }

  BoardCells copyCells() {
    final BoardCells copiedGrid = [];
    for (int rowIndex = 0; rowIndex < cells.length; rowIndex++) {
      final List<Cell> row = [];
      for (int colIndex = 0; colIndex < cells[rowIndex].length; colIndex++) {
        final Cell cell = cells[rowIndex][colIndex];
        row.add(Cell(
          location: CellLocation.go(rowIndex, colIndex),
          blockId: cell.blockId,
          value: cell.value,
          isFixed: false,
        ));
      }
      copiedGrid.add(row);
    }

    return copiedGrid;
  }

  bool isSolved() {
Benoît Harrault's avatar
Benoît Harrault committed
    // first, check grid is fully completed
Benoît Harrault's avatar
Benoît Harrault committed
    for (CellLocation location in getCellLocations()) {
      if (get(location).value == 0) {
        return false;
      }
    }

    // check each block contains all values from 1 to block size
    for (String blockId in getBlockIds()) {
      List<int> values = [];
      List<int> duplicateValues = [];
      for (CellLocation location in getCellLocations()) {
        if (get(location).blockId == blockId) {
          final int value = get(location).value;
          if (value != 0) {
            if (!values.contains(value)) {
              values.add(value);
            } else {
              duplicateValues.add(value);
            }
          }
        }
      }
      for (int duplicateValue in duplicateValues) {
        for (CellLocation location in getCellLocations()) {
          if (get(location).blockId == blockId && get(location).value == duplicateValue) {
            return false;
          }
        }
      }
    }

Benoît Harrault's avatar
Benoît Harrault committed
    if (boardHasSiblingWithSameValue()) {
      return false;
    }

Benoît Harrault's avatar
Benoît Harrault committed
    return true;
  }

  int getMaxValueForBlock(String? blockId) {
    int maxValue = 0;
    for (CellLocation location in getCellLocations()) {
      if (get(location).blockId == blockId) {
        maxValue++;
      }
    }
    return maxValue;
  }

  List<int> getMissingValuesInBlock(String blockId) {
    List<int> missingValues = [];
    final List<int> values = getValuesInBlock(blockId);
    final List<int> expectedValues =
        List<int>.generate(getMaxValueForBlock(blockId), (i) => i + 1);

    for (int candidateValue in expectedValues) {
      if (!values.contains(candidateValue)) {
        missingValues.add(candidateValue);
      }
    }

    return missingValues;
  }

Benoît Harrault's avatar
Benoît Harrault committed
  List<Move> getLastEmptyCellsInBlocks() {
    List<Move> candidateCells = [];
Benoît Harrault's avatar
Benoît Harrault committed

    for (CellLocation location in getCellLocations()) {
      final Cell cell = get(location);
      if (cell.value == 0) {
        final int blockSize = getMaxValueForBlock(cell.blockId);
        final List<int> blockValues = getValuesInBlock(cell.blockId);
        blockValues.removeWhere((value) => value == 0);

        if (blockValues.length == blockSize - 1) {
          int candidateValue = 0;

          for (int value = 1; value <= blockSize; value++) {
            if (!blockValues.contains(value)) {
              candidateValue = value;
            }
          }
Benoît Harrault's avatar
Benoît Harrault committed
          candidateCells.add(Move(location: location, value: candidateValue));
Benoît Harrault's avatar
Benoît Harrault committed
        }
      }
    }

    return candidateCells;
  }

Benoît Harrault's avatar
Benoît Harrault committed
  List<Move> getOnlyCellInBlockWithoutConflict() {
    List<Move> candidateCells = [];
Benoît Harrault's avatar
Benoît Harrault committed

    for (String blockId in getBlockIds()) {
      List<int> missingValuesInBlock = getMissingValuesInBlock(blockId);

      for (int candidateValue in missingValuesInBlock) {
        final List<CellLocation> allowedCellsForThisValue = [];

        for (CellLocation location in getCellLocations(blockId)) {
          if (get(location).value == 0) {
            if (isValueAllowed(location, candidateValue)) {
              allowedCellsForThisValue.add(location);
            }
          }
        }

        if (allowedCellsForThisValue.length == 1) {
          final CellLocation candidateLocation = allowedCellsForThisValue[0];
Benoît Harrault's avatar
Benoît Harrault committed
          candidateCells.add(Move(location: candidateLocation, value: candidateValue));
Benoît Harrault's avatar
Benoît Harrault committed
        }
      }
    }

    return candidateCells;
  }

Benoît Harrault's avatar
Benoît Harrault committed
  List<Move> getEmptyCellsWithUniqueAvailableValue() {
    List<Move> candidateCells = [];
Benoît Harrault's avatar
Benoît Harrault committed

    for (CellLocation location in getCellLocations()) {
      if (get(location).value == 0) {
        int allowedValuesCount = 0;
        int candidateValue = 0;
        final int maxValueForThisCell = getMaxValueForBlock(get(location).blockId);

        for (int value = 1; value <= maxValueForThisCell; value++) {
          if (isValueAllowed(location, value)) {
            candidateValue = value;
            allowedValuesCount++;
          }
        }

        if (allowedValuesCount == 1) {
Benoît Harrault's avatar
Benoît Harrault committed
          candidateCells.add(Move(location: location, value: candidateValue));
Benoît Harrault's avatar
Benoît Harrault committed
        }
      }
    }

    return candidateCells;
  }

  bool blockContainsDuplicates(String blockId, [int? candidateValue]) {
    List<int> duplicateValues = [];

    List<int> values = [];
    if (candidateValue != null) {
      values.add(candidateValue);
    }

    for (CellLocation location in getCellLocations(blockId)) {
      final int value = get(location).value;
      if (value != 0) {
        if (!values.contains(value)) {
          values.add(value);
        } else {
          duplicateValues.add(value);
        }
      }
    }

    return duplicateValues.isNotEmpty;
  }

  bool boardHasSiblingWithSameValue() {
    for (CellLocation location in getCellLocations()) {
      final int value = get(location).value;
      if (value != 0 && cellHasSiblingWithSameValue(location)) {
        return true;
      }
    }

    return false;
  }

  bool cellHasSiblingWithSameValue(CellLocation cellLocation, [int? candidateValue]) {
    if (cells.isEmpty) {
      return false;
    }

    final int value = candidateValue ?? get(cellLocation).value;
    if (value != 0) {
Benoît Harrault's avatar
Benoît Harrault committed
      for (int deltaCol in [-1, 0, 1]) {
        for (int deltaRow in [-1, 0, 1]) {
          final CellLocation siblingLocation =
              CellLocation.go(cellLocation.row + deltaRow, cellLocation.col + deltaCol);

          if (inBoard(siblingLocation) && !(deltaRow == 0 && deltaCol == 0)) {
            if (get(siblingLocation).value == value) {
Benoît Harrault's avatar
Benoît Harrault committed
              return true;
            }
          }
        }
      }
    }

    return false;
  }

  bool isValueAllowed(CellLocation? location, int value) {
    if ((location == null) || (value == 0)) {
      return true;
    }

    // check siblings
    if (cellHasSiblingWithSameValue(location, value)) {
      return false;
    }

    // check block does not contain duplicates
    if (blockContainsDuplicates(get(location).blockId, value)) {
      return false;
    }

    return true;
  }

  void dump() {
    const String stringValues = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';

    printlog('');
    printlog('- blocks / values / solved -');
    printlog('-------');
    if (cells.isEmpty) {
      printlog('empty board');
    } else {
      for (int rowIndex = 0; rowIndex < cells.length; rowIndex++) {
        String rowBlocks = '';
        String rowValues = '';
        String rowSolved = '';
        for (int colIndex = 0; colIndex < cells[rowIndex].length; colIndex++) {
          rowBlocks += cells[rowIndex][colIndex].blockId;
          rowValues += stringValues[cells[rowIndex][colIndex].value];
          if (solvedCells.isEmpty) {
            rowSolved += '*';
          } else {
Benoît Harrault's avatar
Benoît Harrault committed
            final int solvedValue = solvedCells[rowIndex][colIndex].value;
            if (solvedValue == 0) {
              rowSolved += ' ';
            } else {
              rowSolved += stringValues[solvedCells[rowIndex][colIndex].value];
            }
Benoît Harrault's avatar
Benoît Harrault committed
          }
        }
        printlog('$rowBlocks | $rowValues | $rowSolved');
      }
    }
    printlog('-------');
    printlog('');
  }

  @override
  String toString() {
    return '$Board(${toJson()})';
  }

  Map<String, dynamic>? toJson() {
    return <String, dynamic>{
      'cells': cells,
    };
  }
}