Skip to content
Snippets Groups Projects
activity.dart 13 KiB
Newer Older
Benoît Harrault's avatar
Benoît Harrault committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430
import 'dart:math';

import 'package:flutter_custom_toolbox/flutter_toolbox.dart';

import 'package:suguru/config/application_config.dart';
import 'package:suguru/cubit/activity/activity_cubit.dart';
import 'package:suguru/data/game_data.dart';
import 'package:suguru/models/activity/board.dart';
import 'package:suguru/models/activity/cell.dart';
import 'package:suguru/models/activity/cell_location.dart';
import 'package:suguru/models/activity/types.dart';

class Activity {
  Activity({
    // Settings
    required this.activitySettings,

    // State
    this.isRunning = false,
    this.isStarted = false,
    this.isFinished = false,
    this.animationInProgress = false,
    this.boardAnimated = const [],

    // Base data
    required this.board,
    required this.boardSizeHorizontal,
    required this.boardSizeVertical,

    // Game data
    this.boardConflicts = const [],
    this.selectedCell,
    this.showConflicts = false,
    this.givenTipsCount = 0,
    this.buttonTipsCountdown = 0,
  });

  // Settings
  final ActivitySettings activitySettings;

  // State
  bool isRunning;
  bool isStarted;
  bool isFinished;
  bool animationInProgress;
  AnimatedBoard boardAnimated;

  // Base data
  final Board board;
  final int boardSizeHorizontal;
  final int boardSizeVertical;

  // Game data
  ConflictsCount boardConflicts;
  Cell? selectedCell;
  bool showConflicts;
  int givenTipsCount;
  int buttonTipsCountdown;

  factory Activity.createEmpty() {
    return Activity(
      // Settings
      activitySettings: ActivitySettings.createDefault(appConfig: ApplicationConfig.config),
      // Base data
      board: Board.createEmpty(),
      boardSizeHorizontal: 0,
      boardSizeVertical: 0,
      // Game data
      givenTipsCount: 0,
    );
  }

  factory Activity.createNew({
    ActivitySettings? activitySettings,
  }) {
    final ActivitySettings newActivitySettings = activitySettings ??
        ActivitySettings.createDefault(appConfig: ApplicationConfig.config);

    final List<String> templates =
        GameData.templates[newActivitySettings.get(ApplicationConfig.parameterCodeBoardSize)]
                ?[newActivitySettings.get(ApplicationConfig.parameterCodeDifficultyLevel)] ??
            [];
    final String template = templates.elementAt(Random().nextInt(templates.length)).toString();

    final List<String> templateParts = template.split(';');
    if (templateParts.length != 3) {
      printlog('Failed to get grid template (wrong format)...');
      return Activity.createEmpty();
    }

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

    // Build empty conflicts board
    ConflictsCount nonConflictedBoard = [];
    for (int row = 0; row < boardSizeVertical; row++) {
      List<int> line = [];
      for (int col = 0; col < boardSizeHorizontal; col++) {
        line.add(0);
      }
      nonConflictedBoard.add(line);
    }

    // Build empty animated background
    AnimatedBoard notAnimatedBoard = [];
    for (int row = 0; row < boardSizeVertical; row++) {
      List<bool> line = [];
      for (int col = 0; col < boardSizeHorizontal; col++) {
        line.add(false);
      }
      notAnimatedBoard.add(line);
    }

    // Build main game board
    final Board board = Board.createEmpty();
    board.createFromTemplate(template: template);

    return Activity(
      // Settings
      activitySettings: newActivitySettings,
      // State
      isRunning: true,
      boardAnimated: notAnimatedBoard,
      // Base data
      board: board,
      boardSizeHorizontal: boardSizeHorizontal,
      boardSizeVertical: boardSizeVertical,
      // Game data
      boardConflicts: nonConflictedBoard,
      selectedCell: null,
    );
  }

  bool get canBeResumed => isStarted && !isFinished;

  bool get gameWon => isRunning && isStarted && isFinished;

  bool get canGiveTip => (buttonTipsCountdown == 0);

  bool checkBoardIsSolved() {
    for (CellLocation location in board.getCellLocations()) {
      if (board.get(location).value == 0 ||
          board.get(location).value != board.solvedCells[location.row][location.col].value) {
        return false;
      }
    }

    printlog('-> ok suguru solved!');

    return true;
  }

  ConflictsCount computeConflictsInBoard() {
    final BoardCells cells = board.cells;
    final ConflictsCount conflicts = boardConflicts;

    // reset conflict states
    for (int row = 0; row < boardSizeVertical; row++) {
      for (int col = 0; col < boardSizeHorizontal; col++) {
        conflicts[row][col] = 0;
      }
    }

    // check siblings
    for (int row = 0; row < boardSizeVertical; row++) {
      for (int col = 0; col < boardSizeHorizontal; col++) {
        final int value = cells[row][col].value;
        if (value != 0) {
          for (int deltaRow = -1; deltaRow <= 1; deltaRow++) {
            for (int deltaCol = -1; deltaCol <= 1; deltaCol++) {
              if (row + deltaRow >= 0 &&
                  row + deltaRow < boardSizeHorizontal &&
                  col + deltaCol >= 0 &&
                  col + deltaCol < boardSizeVertical &&
                  (deltaRow * deltaCol != 0)) {
                final int siblingValue = cells[row + deltaRow][col + deltaCol].value;

                if (siblingValue == value) {
                  conflicts[row][col]++;
                }
              }
            }
          }
        }
      }
    }

    // check blocks does not contain duplicates
    for (String blockId in board.getBlockIds()) {
      List<int> values = [];
      List<int> duplicateValues = [];
      for (int row = 0; row < boardSizeVertical; row++) {
        for (int col = 0; col < boardSizeHorizontal; col++) {
          if (cells[row][col].blockId == blockId) {
            final int value = cells[row][col].value;
            if (value != 0) {
              if (!values.contains(value)) {
                values.add(value);
              } else {
                duplicateValues.add(value);
              }
            }
          }
        }
      }
      for (int duplicateValue in duplicateValues) {
        for (int row = 0; row < boardSizeVertical; row++) {
          for (int col = 0; col < boardSizeHorizontal; col++) {
            if (cells[row][col].blockId == blockId &&
                cells[row][col].value == duplicateValue) {
              conflicts[row][col]++;
            }
          }
        }
      }
    }

    return conflicts;
  }

  void showTip(ActivityCubit activityCubit) {
    bool tipGiven = false;

    if (selectedCell == null) {
      // no selected cell -> pick one
      tipGiven = helpSelectCell(activityCubit);
    } else {
      // currently selected cell -> set value
      tipGiven = helpFillCell(activityCubit);
    }

    if (tipGiven) {
      activityCubit.increaseGivenTipsCount();
    }
  }

  bool helpSelectCell(ActivityCubit activityCubit) {
    // pick one of wrong value cells, if found
    final List<List<int>> wrongValueCells = getCellsWithWrongValue();
    if (wrongValueCells.isNotEmpty) {
      printlog('pick from wrongValueCells');
      return pickRandomFromList(activityCubit, wrongValueCells);
    }

    // pick one of conflicting cells, if found
    final List<List<int>> conflictingCells = getCellsWithConflicts();
    if (conflictingCells.isNotEmpty) {
      printlog('pick from conflictingCells');
      return pickRandomFromList(activityCubit, conflictingCells);
    }

    // pick one from "easy" cells (unique empty cell in block)
    final List<List<int>> easyFillableCells = board.getLastEmptyCellsInBlocks();
    if (easyFillableCells.isNotEmpty) {
      printlog('pick from easyFillableCells');
      return pickRandomFromList(activityCubit, easyFillableCells);
    }

    // pick one from cells with unique non-conflicting candidate value
    final List<List<int>> candidateCells = board.getEmptyCellsWithUniqueAvailableValue();
    if (candidateCells.isNotEmpty) {
      printlog('pick from candidateCells');
      return pickRandomFromList(activityCubit, candidateCells);
    }

    // pick one from "only cell in this block for this value"
    final List<List<int>> onlyCellsWithoutConflict = board.getOnlyCellInBlockWithoutConflict();
    if (onlyCellsWithoutConflict.isNotEmpty) {
      printlog('pick from onlyCellsWithoutConflict');
      return pickRandomFromList(activityCubit, onlyCellsWithoutConflict);
    }

    printlog('no easy cell to select...');
    return false;
  }

  List<List<int>> getCellsWithWrongValue() {
    final BoardCells cells = board.cells;
    final BoardCells cellsSolved = board.solvedCells;

    List<List<int>> cellsWithWrongValue = [];

    for (int row = 0; row < boardSizeVertical; row++) {
      for (int col = 0; col < boardSizeHorizontal; col++) {
        if (cells[row][col].value != 0 &&
            cells[row][col].value != cellsSolved[row][col].value) {
          cellsWithWrongValue.add([row, col]);
        }
      }
    }

    return cellsWithWrongValue;
  }

  List<List<int>> getCellsWithConflicts() {
    List<List<int>> cellsWithConflict = [];

    for (int row = 0; row < boardSizeVertical; row++) {
      for (int col = 0; col < boardSizeHorizontal; col++) {
        if (boardConflicts[row][col] != 0) {
          cellsWithConflict.add([row, col]);
        }
      }
    }

    return cellsWithConflict;
  }

  bool pickRandomFromList(ActivityCubit activityCubit, List<List<int>> cellsCoordinates) {
    if (cellsCoordinates.isNotEmpty) {
      cellsCoordinates.shuffle();
      final List<int> cell = cellsCoordinates[0];
      activityCubit.selectCell(CellLocation.go(cell[0], cell[1]));
      return true;
    }

    return false;
  }

  bool helpFillCell(ActivityCubit activityCubit) {
    // Will clean cell if no eligible value found
    int eligibleValue = 0;

    // Ensure there is only one eligible value for this cell
    int allowedValuesCount = 0;
    final int maxValueForThisCell = board.getMaxValueForBlock(selectedCell?.blockId);
    for (int value = 1; value <= maxValueForThisCell; value++) {
      if (board.isValueAllowed(selectedCell?.location, value)) {
        allowedValuesCount++;
        eligibleValue = value;
      }
    }

    if (allowedValuesCount == 1) {
      activityCubit.updateCellValue(selectedCell!.location, eligibleValue);
      activityCubit.unselectCell();
      return true;
    }

    activityCubit.unselectCell();
    return false;
  }

  void checkAllTemplates() {
    printlog('###############################');
    printlog('##                           ##');
    printlog('##      CHECK TEMPLATES      ##');
    printlog('##                           ##');
    printlog('###############################');

    final List<String> allowedLevels = ApplicationConfig.config
        .getFromCode(ApplicationConfig.parameterCodeDifficultyLevel)
        .allowedValues;
    final List<String> allowedSizes = ApplicationConfig.config
        .getFromCode(ApplicationConfig.parameterCodeBoardSize)
        .allowedValues;

    for (String level in allowedLevels) {
      printlog('* level: $level');
      for (String size in allowedSizes) {
        printlog('** size: $size');
        final List<String> templates = GameData.templates[size]?[level] ?? [];
        printlog('*** templates count: ${templates.length}');

        for (String template in templates) {
          printlog(' checking $template');

          final Board testBoard = Board.createEmpty();
          testBoard.createFromTemplate(template: template);
          testBoard.dump();
        }
      }
    }
  }

  void dump() {
    printlog('');
    printlog('## Current game dump:');
    printlog('');
    printlog('$Activity:');
    printlog('  Settings');
    activitySettings.dump();
    printlog('  State');
    printlog('    isRunning: $isRunning');
    printlog('    isStarted: $isStarted');
    printlog('    isFinished: $isFinished');
    printlog('    animationInProgress: $animationInProgress');
    printlog('  Base data');
    printlog('    boardSizeHorizontal: $boardSizeHorizontal');
    printlog('    boardSizeVertical: $boardSizeVertical');
    printlog('  Game data');
    printlog('    selectedCell: ${selectedCell?.toString() ?? ''}');
    printlog('    showConflicts: $showConflicts');
    printlog('    givenTipsCount: $givenTipsCount');
    printlog('    buttonTipsCountdown: $buttonTipsCountdown');
    printlog('');

    board.dump();
    printlog('');
  }

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

  Map<String, dynamic>? toJson() {
    return <String, dynamic>{
      // Settings
      'activitySettings': activitySettings.toJson(),
      // State
      'isRunning': isRunning,
      'isStarted': isStarted,
      'isFinished': isFinished,
      'animationInProgress': animationInProgress,
      'boardAnimated': boardAnimated,
      // Base data
      'board': board.toJson(),
      'boardSizeHorizontal': boardSizeHorizontal,
      'boardSizeVertical': boardSizeVertical,
      // Game data
      'boardConflicts': boardConflicts,
      'selectedCell': selectedCell?.toJson(),
      'showConflicts': showConflicts,
      'givenTipsCount': givenTipsCount,
      'buttonTipsCountdown': buttonTipsCountdown,
    };
  }
}