private void removeCellValue(int row, int col) {
    Cell cell = board.getCell(row, col);

    // not allow to clear cell when it is locked
    if (cell.isLocked() || cell.isEmpty()) {
      return;
    }

    Step step = board.removeCellValue(row, col);
    boardView.updateCellView(cell);

    // update other cells which is affected by this cell
    for (CellSnapshot snapshot : step.snapshots) {
      Cell affectedCell = board.getCell(snapshot.row, snapshot.col);
      if (affectedCell.isLocked()) {
        continue;
      }
      boardView.getCellView(affectedCell).becomeValid();
    }

    steps.push(step);

    // current sum of group is changed
    updateGroupSumView(cell.getGroupId());
  }
  private void toggleCellHint(int row, int col, int hint) {
    Cell cell = board.getCell(row, col);

    // not allow to toggle hint when cell is holding value
    if (cell.isEmpty() == false) {
      return;
    }

    boolean isAdding = !board.getCell(row, col).getHints().contains(hint);
    Step step = isAdding ? board.addCellHint(row, col, hint) : board.subCellHint(row, col, hint);
    steps.push(step);

    boardView.getCellView(row, col).toggleHint(hint, isAdding);
  }
  private void auto() {
    Step step = null;
    try {
      step = board.auto();
    } catch (Exception e) {
      logger.error("Exception in auto?", e);
      AlertFactory.createError(e).showAndWait();
    }

    // didn't solve anything, don't remember this step
    if (step == null) {
      return;
    }

    steps.push(step);

    Set<Integer> groupIds = new HashSet<Integer>();
    for (CellSnapshot snapshot : step.snapshots) {
      Cell affectedCell = board.getCell(snapshot.row, snapshot.col);
      if (snapshot.isMain()) {
        groupIds.add(affectedCell.getGroupId());
      }
      boardView.updateCellView(affectedCell);
    }

    for (int groupId : groupIds) {
      updateGroupSumView(groupId);
    }

    // after auto, game might be solved
    checkGameIsSolved();
  }
  private void removeCellHints(int row, int col) {
    Cell cell = board.getCell(row, col);

    // if cell doesn't any hint, don't have to clear it
    if (cell.getHints().isEmpty()) {
      return;
    }

    Step step = board.removeCellHints(row, col);
    steps.push(step);

    boardView.updateCellView(cell);
  }
  private void moveFocusTo(int row, int col) {

    // update combination list if new group is selected
    Cell cell = board.getCell(row, col);
    int groupId = cell.getGroupId();
    if (groupId != board.getCurrentCell().getGroupId()) {
      combinationView.update(board.getGroup(groupId));
    }

    // don't update current cell before checking group ID changed
    board.setCurrentCell(row, col);
    boardView.focusOn(row, col);
  }
  private void updateCellValue(int row, int col, int value) {
    Cell cell = board.getCell(row, col);

    // not allows to update cell value when cell is locked, or the new value is not changed
    if (cell.isLocked() || cell.getValue() == value) {
      return;
    }

    Step step = board.updateCellValue(row, col, value, autoMode.isSelected());
    boardView.updateCellView(cell);

    for (CellSnapshot snapshot : step.snapshots) {
      Cell affectedCell = board.getCell(snapshot.row, snapshot.col);
      boardView.updateCellView(affectedCell);
    }

    steps.push(step);

    // current sum of group is changed
    updateGroupSumView(cell.getGroupId());

    // check if game is solved
    checkGameIsSolved();
  }
  private void undo() {
    if (steps.isEmpty()) {
      logger.warn("There is nothing to undo.");
      return;
    }

    Step step = steps.pop();
    Set<Integer> groupIds = board.undo(step);

    // update all affected group sum view
    for (int groupId : groupIds) {
      updateGroupSumView(groupId);
    }

    // update all cell view
    for (CellSnapshot snapshot : step.snapshots) {
      Cell cell = board.getCell(snapshot.row, snapshot.col);
      boardView.updateCellView(cell);
    }
  }