Skip to content

New classic level - #29

Merged
khlebobul merged 7 commits into
mainfrom
new_classic_level
Jan 28, 2026
Merged

khlebobul merged 7 commits into
mainfrom
new_classic_level

Conversation

@khlebobul

@khlebobul khlebobul commented Jan 25, 2026 •

Copy link
Copy Markdown
Owner

Checklist

Change type

  • bugfix
  • new lvl
  • new feature
  • other

Summary by CodeRabbit

Version 1.8.0

  • New Features

    • Introduces fractional level IDs (e.g., 0.1, 0.2); Level 0 renamed to 0.1 and new Level 0.2 added.
  • Improvements

    • Database migration preserves existing progress while switching to string-based level IDs.
    • Level list and display now show actual level identifiers (e.g., "0.1") for clearer navigation.
    • Default store link updated to Google Play.

✏️ Tip: You can customize this high-level summary in your review settings.

@khlebobul khlebobul self-assigned this Jan 25, 2026
@khlebobul khlebobul added feature New feature new levels New levels labels Jan 25, 2026
@coderabbitai

coderabbitai Bot commented Jan 25, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Migrates level identifiers from integers to strings to support fractional IDs (e.g., 0.1, 0.2). Adds level 0.2, bumps app and database versions, includes DB migration converting INTEGER level_id → TEXT, and updates providers, UI, router, widgets, and tests to use String level IDs.

Changes

Cohort / File(s) Summary
Version & Manifests
CHANGELOG.md, pubspec.yaml, ios/Runner.xcodeproj/project.pbxproj
Bumps app version to 1.8.0 and build metadata (MARKETING_VERSION -> 1.8.0, build number updated to +31).
Database Schema & Migration
lib/core/database/database_helper.dart
DB version 5→6; migration converts level_id from INTEGER → TEXT for completed_levels and saved_games, maps legacy level 0 → "0.1", renames/drops tables to preserve data, and changes public API to accept/return String levelId types.
Routing
lib/core/router/app_router.dart
Route /game/:levelId no longer parses levelId to int — passes raw string to GameScreen.
Level Asset
lib/data/levels/level_0.2.json
Adds new level asset file (level_0.2.json) with 7x7 board data.
Providers & Generated Wiring
lib/presentation/providers/*
lib/presentation/providers/game_provider.g.dart
Providers updated to use String level IDs: levelsProvider, completedLevelsProvider, isLevelCompletedProvider.family, game provider and its generated wiring all accept/return String level IDs; sorting updated to parse strings as doubles for numeric order.
Screens & Widgets
lib/presentation/screens/game/game_screen.dart, lib/presentation/screens/home/home_screen.dart, lib/presentation/widgets/game/game_board.dart
levelId fields/constructors changed from int → String; game navigation no longer increments numeric IDs — uses index-based lookup in levels list; home screen shows asset level ID string.
Utilities
lib/core/utils/market_helper.dart
Default market URL switched to Google Play constant.
Tests
test/levels_uniqueness_test.dart, test/levels_solvability_test.dart
Tests updated to use String level IDs, load level_$levelId.json, sort by parsed double, and skip tutorial fractional levels where specified.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Router
    participant GameScreen
    participant GameProvider
    participant Database
    Note over Database: On app upgrade (DB v5→v6) migration runs

    User->>Router: navigate /game/:levelId (e.g., "0.2")
    Router->>GameScreen: instantiate with levelId (String)
    GameScreen->>GameProvider: request build/loadLevel(levelId: "0.2")
    GameProvider->>Database: getSavedGameState(levelId: "0.2") / isLevelCompleted("0.2")
    Database-->>GameProvider: return TEXT-keyed data
    GameProvider-->>GameScreen: provide game state
    GameScreen->>User: render level "0.2"
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Level star fix #14: Changes to game_provider.dart / generated provider wiring — closely related to the provider argument-type migration to String.
  • Loading indicator #17: Modifies lib/core/database/database_helper.dart — related to DB migration and saved-games handling updates.
  • New levels 51 #23: Alters DB migration and saved-games clearing logic — overlaps with schema/type migration and clearSavedGamesForLevels signature changes.

Poem

🐰 I hopped from int to string with glee,

0.1 and 0.2 join the spree,
Migrations hummed, assets in tow,
Version 1.8 — onward we go! 🎮

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning Description contains only a checkbox checklist with no explanatory text; it lacks detail about the actual changes, migration impact, or rationale. Add substantive description beyond the checklist explaining what was changed, why the changes matter, and any migration or compatibility notes.
Title check ❓ Inconclusive Title is vague and generic, using non-descriptive phrasing that doesn't convey the specific changes made. Clarify the title to reflect the main changes: fractional level IDs (0.1, 0.2), database schema upgrade, and API changes from int to String.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@lib/core/database/database_helper.dart`:
- Around line 66-108: The migration block guarded by "if (oldVersion < 6)" must
be executed inside a database transaction to avoid partial failure: replace the
current sequence of await db.execute(...) calls for creating
completed_levels_new, saved_games_new, the INSERT ... SELECT migrations, DROP
TABLE completed_levels/saved_games and the ALTER TABLE ... RENAME operations
with a single transactional call (e.g. await db.transaction((txn) async { ... })
) and run each DDL/DML against the transaction handle
(txn.execute/txn.rawInsert) so any exception triggers an automatic rollback and
prevents loss of completed_levels and saved_games data.

In `@lib/core/utils/market_helper.dart`:
- Around line 41-44: The Makefile's set-google-play sed pattern no longer
matches because the code default was changed to return
GeneralConsts.otherAppsGooglePlayLink; update the sed in the set-google-play
target so it matches either variant and forces the return to
GeneralConsts.otherAppsGooglePlayLink (e.g. use a regex that matches
GeneralConsts.otherAppsRustoreLink|GeneralConsts.otherAppsGooglePlayLink and
replaces with GeneralConsts.otherAppsGooglePlayLink); also ensure the
set-rustore target is updated symmetrically to always replace either symbol with
GeneralConsts.otherAppsRustoreLink so _getCurrentOtherAppsUrl and the
GeneralConsts.otherAppsGooglePlayLink/otherAppsRustoreLink swaps work reliably.
🧹 Nitpick comments (3)
lib/presentation/screens/game/game_screen.dart (1)

271-271: Consider: indexOf returns -1 if level not found.

If widget.levelId is not in the levels list (e.g., during async loading race), displayLevelNumber will be -1. Line 288 handles this by showing "0", but this may be confusing to users if the level truly isn't found.

lib/presentation/providers/levels_provider.dart (1)

18-23: Note: Non-numeric level IDs will sort to position 0.

The double.tryParse(a) ?? 0 fallback means any level ID that isn't a valid number (e.g., "tutorial") will be treated as 0 for sorting purposes. This clusters all non-numeric IDs at the start, which may not be intentional.

If non-numeric IDs are expected in the future, consider a more explicit ordering strategy.

test/levels_uniqueness_test.dart (1)

20-30: Consider extracting duplicated level ID parsing logic.

The level ID extraction and sorting logic (lines 20-30) is duplicated from levels_provider.dart. If the parsing logic changes in one place, tests might drift out of sync.

Consider extracting this into a shared utility function or directly using levelsProvider in tests.

♻️ Example: Extract shared utility
// In a shared utility file (e.g., lib/utils/level_utils.dart)
List<String> extractAndSortLevelIds(List<String> levelPaths) {
  final levelIds = levelPaths.map((path) {
    return path.split('/').last.replaceAll('level_', '').replaceAll('.json', '');
  }).toList();

  levelIds.sort((a, b) {
    final aNum = double.tryParse(a) ?? 0;
    final bNum = double.tryParse(b) ?? 0;
    return aNum.compareTo(bNum);
  });

  return levelIds;
}

Comment on lines +66 to +108
if (oldVersion < 6) {
// Migrate level_id from INTEGER to TEXT to support fractional levels (0.1, 0.2, etc.)
// Also migrate level 0 to 0.1 for users who already completed it

// Create new tables with TEXT level_id
await db.execute('''
CREATE TABLE completed_levels_new (
level_id TEXT PRIMARY KEY,
completed_at TEXT NOT NULL,
moves_count INTEGER NOT NULL
)
''');

await db.execute('''
CREATE TABLE saved_games_new (
level_id TEXT PRIMARY KEY,
board_state TEXT NOT NULL,
moves_count INTEGER NOT NULL,
saved_at TEXT NOT NULL
)
''');

// Migrate data, converting level 0 to 0.1
await db.execute('''
INSERT INTO completed_levels_new (level_id, completed_at, moves_count)
SELECT CASE WHEN level_id = 0 THEN '0.1' ELSE CAST(level_id AS TEXT) END,
completed_at, moves_count
FROM completed_levels
''');

await db.execute('''
INSERT INTO saved_games_new (level_id, board_state, moves_count, saved_at)
SELECT CASE WHEN level_id = 0 THEN '0.1' ELSE CAST(level_id AS TEXT) END,
board_state, moves_count, saved_at
FROM saved_games
''');

// Drop old tables and rename new ones
await db.execute('DROP TABLE completed_levels');
await db.execute('DROP TABLE saved_games');
await db.execute('ALTER TABLE completed_levels_new RENAME TO completed_levels');
await db.execute('ALTER TABLE saved_games_new RENAME TO saved_games');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Wrap migration in a transaction to prevent data loss on partial failure.

The migration performs multiple DDL operations sequentially. If any statement fails after DROP TABLE (lines 104-105) but before ALTER TABLE ... RENAME (lines 106-107), the database will be left in an inconsistent state with potential data loss.

🛡️ Proposed fix: Wrap in transaction
     if (oldVersion < 6) {
       // Migrate level_id from INTEGER to TEXT to support fractional levels (0.1, 0.2, etc.)
       // Also migrate level 0 to 0.1 for users who already completed it
+      await db.transaction((txn) async {
+        // Create new tables with TEXT level_id
+        await txn.execute('''
+          CREATE TABLE completed_levels_new (
+            level_id TEXT PRIMARY KEY,
+            completed_at TEXT NOT NULL,
+            moves_count INTEGER NOT NULL
+          )
+        ''');
 
-      // Create new tables with TEXT level_id
-      await db.execute('''
-        CREATE TABLE completed_levels_new (
-          level_id TEXT PRIMARY KEY,
-          completed_at TEXT NOT NULL,
-          moves_count INTEGER NOT NULL
-        )
-      ''');
-
-      await db.execute('''
-        CREATE TABLE saved_games_new (
-          level_id TEXT PRIMARY KEY,
-          board_state TEXT NOT NULL,
-          moves_count INTEGER NOT NULL,
-          saved_at TEXT NOT NULL
-        )
-      ''');
-
-      // Migrate data, converting level 0 to 0.1
-      await db.execute('''
-        INSERT INTO completed_levels_new (level_id, completed_at, moves_count)
-        SELECT CASE WHEN level_id = 0 THEN '0.1' ELSE CAST(level_id AS TEXT) END,
-               completed_at, moves_count
-        FROM completed_levels
-      ''');
-
-      await db.execute('''
-        INSERT INTO saved_games_new (level_id, board_state, moves_count, saved_at)
-        SELECT CASE WHEN level_id = 0 THEN '0.1' ELSE CAST(level_id AS TEXT) END,
-               board_state, moves_count, saved_at
-        FROM saved_games
-      ''');
-
-      // Drop old tables and rename new ones
-      await db.execute('DROP TABLE completed_levels');
-      await db.execute('DROP TABLE saved_games');
-      await db.execute('ALTER TABLE completed_levels_new RENAME TO completed_levels');
-      await db.execute('ALTER TABLE saved_games_new RENAME TO saved_games');
+        await txn.execute('''
+          CREATE TABLE saved_games_new (
+            level_id TEXT PRIMARY KEY,
+            board_state TEXT NOT NULL,
+            moves_count INTEGER NOT NULL,
+            saved_at TEXT NOT NULL
+          )
+        ''');
+
+        // Migrate data, converting level 0 to 0.1
+        await txn.execute('''
+          INSERT INTO completed_levels_new (level_id, completed_at, moves_count)
+          SELECT CASE WHEN level_id = 0 THEN '0.1' ELSE CAST(level_id AS TEXT) END,
+                 completed_at, moves_count
+          FROM completed_levels
+        ''');
+
+        await txn.execute('''
+          INSERT INTO saved_games_new (level_id, board_state, moves_count, saved_at)
+          SELECT CASE WHEN level_id = 0 THEN '0.1' ELSE CAST(level_id AS TEXT) END,
+                 board_state, moves_count, saved_at
+          FROM saved_games
+        ''');
+
+        // Drop old tables and rename new ones
+        await txn.execute('DROP TABLE completed_levels');
+        await txn.execute('DROP TABLE saved_games');
+        await txn.execute('ALTER TABLE completed_levels_new RENAME TO completed_levels');
+        await txn.execute('ALTER TABLE saved_games_new RENAME TO saved_games');
+      });
     }
🤖 Prompt for AI Agents
In `@lib/core/database/database_helper.dart` around lines 66 - 108, The migration
block guarded by "if (oldVersion < 6)" must be executed inside a database
transaction to avoid partial failure: replace the current sequence of await
db.execute(...) calls for creating completed_levels_new, saved_games_new, the
INSERT ... SELECT migrations, DROP TABLE completed_levels/saved_games and the
ALTER TABLE ... RENAME operations with a single transactional call (e.g. await
db.transaction((txn) async { ... }) ) and run each DDL/DML against the
transaction handle (txn.execute/txn.rawInsert) so any exception triggers an
automatic rollback and prevents loss of completed_levels and saved_games data.

Comment on lines 41 to +44
static String _getCurrentOtherAppsUrl() {
// Check which URL is currently active
// The Makefile will modify this to switch between markets
return GeneralConsts.otherAppsRustoreLink; // This will be changed by make
return GeneralConsts.otherAppsGooglePlayLink; // This will be changed by make

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify Makefile/mk scripts still target the correct token after this change
echo "=== Searching for Makefile/mk files ==="
fd -e Makefile -e mk | head -20

echo -e "\n=== Searching in Makefiles for market-related tokens ==="
rg -n "market_helper|otherAppsGooglePlayLink|otherAppsRuStoreLink|rustore|play\.google\.com" -g 'Makefile*' -g '*.mk' --max-count=20

echo -e "\n=== Checking market_helper.dart constants ==="
rg -n "otherAppsGooglePlayLink|otherAppsRuStoreLink|RuStore" lib/core/utils/market_helper.dart

Repository: khlebobul/pegma

Length of output: 2602


Update the Makefile sed pattern for Google Play builds to match the new default.

The set-google-play target (line 33) uses:

sed -i '' -E "s|return GeneralConsts\\.otherAppsRustoreLink;|return GeneralConsts.otherAppsGooglePlayLink;|"

This no longer matches the default return statement after your change. While RuStore builds work correctly (line 39), the Google Play build logic is broken—relying on the code already being at the correct default. Update line 33 to replace otherAppsGooglePlayLink when needed, or adjust the default logic to ensure both build paths work reliably.

🤖 Prompt for AI Agents
In `@lib/core/utils/market_helper.dart` around lines 41 - 44, The Makefile's
set-google-play sed pattern no longer matches because the code default was
changed to return GeneralConsts.otherAppsGooglePlayLink; update the sed in the
set-google-play target so it matches either variant and forces the return to
GeneralConsts.otherAppsGooglePlayLink (e.g. use a regex that matches
GeneralConsts.otherAppsRustoreLink|GeneralConsts.otherAppsGooglePlayLink and
replaces with GeneralConsts.otherAppsGooglePlayLink); also ensure the
set-rustore target is updated symmetrically to always replace either symbol with
GeneralConsts.otherAppsRustoreLink so _getCurrentOtherAppsUrl and the
GeneralConsts.otherAppsGooglePlayLink/otherAppsRustoreLink swaps work reliably.

@khlebobul
khlebobul merged commit 4cad478 into main Jan 28, 2026
2 checks passed
@khlebobul
khlebobul deleted the new_classic_level branch January 28, 2026 12:08
@coderabbitai coderabbitai Bot mentioned this pull request Mar 28, 2026
4 tasks
@coderabbitai coderabbitai Bot mentioned this pull request Jul 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature new levels New levels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant