Skip to content

Commit 4cad478

Browse files
authored
Merge pull request #29 from khlebobul/new_classic_level
New classic level
2 parents ef6dc5f + f0d4582 commit 4cad478

File tree

17 files changed

+168
-80
lines changed

17 files changed

+168
-80
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
## [1.8.0]
2+
3+
#### New Features
4+
5+
- Added support for fractional level IDs (0.1, 0.2).
6+
- Renamed classic level 0 to 0.1.
7+
- Added new level 0.2.
8+
9+
#### Improvements
10+
11+
- Database migration to support new level ID format.
12+
- Existing users' progress is preserved automatically.
13+
114
## [1.7.0]
215

316
#### New Features

ios/Runner.xcodeproj/project.pbxproj

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@
495495
buildSettings = {
496496
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
497497
CLANG_ENABLE_MODULES = YES;
498-
CURRENT_PROJECT_VERSION = 29;
498+
CURRENT_PROJECT_VERSION = 31;
499499
DEVELOPMENT_TEAM = G4KB5U6326;
500500
ENABLE_BITCODE = NO;
501501
INFOPLIST_FILE = Runner/Info.plist;
@@ -505,7 +505,7 @@
505505
"$(inherited)",
506506
"@executable_path/Frameworks",
507507
);
508-
MARKETING_VERSION = 1.7.0;
508+
MARKETING_VERSION = 1.8.0;
509509
PRODUCT_BUNDLE_IDENTIFIER = com.khlebobul.pegma;
510510
PRODUCT_NAME = "$(TARGET_NAME)";
511511
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -683,7 +683,7 @@
683683
buildSettings = {
684684
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
685685
CLANG_ENABLE_MODULES = YES;
686-
CURRENT_PROJECT_VERSION = 29;
686+
CURRENT_PROJECT_VERSION = 31;
687687
DEVELOPMENT_TEAM = G4KB5U6326;
688688
ENABLE_BITCODE = NO;
689689
INFOPLIST_FILE = Runner/Info.plist;
@@ -693,7 +693,7 @@
693693
"$(inherited)",
694694
"@executable_path/Frameworks",
695695
);
696-
MARKETING_VERSION = 1.7.0;
696+
MARKETING_VERSION = 1.8.0;
697697
PRODUCT_BUNDLE_IDENTIFIER = com.khlebobul.pegma;
698698
PRODUCT_NAME = "$(TARGET_NAME)";
699699
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -709,7 +709,7 @@
709709
buildSettings = {
710710
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
711711
CLANG_ENABLE_MODULES = YES;
712-
CURRENT_PROJECT_VERSION = 29;
712+
CURRENT_PROJECT_VERSION = 31;
713713
DEVELOPMENT_TEAM = G4KB5U6326;
714714
ENABLE_BITCODE = NO;
715715
INFOPLIST_FILE = Runner/Info.plist;
@@ -719,7 +719,7 @@
719719
"$(inherited)",
720720
"@executable_path/Frameworks",
721721
);
722-
MARKETING_VERSION = 1.7.0;
722+
MARKETING_VERSION = 1.8.0;
723723
PRODUCT_BUNDLE_IDENTIFIER = com.khlebobul.pegma;
724724
PRODUCT_NAME = "$(TARGET_NAME)";
725725
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";

lib/core/database/database_helper.dart

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class DatabaseHelper {
2323

2424
return await openDatabase(
2525
path,
26-
version: 5,
26+
version: 6,
2727
onCreate: _createDB,
2828
onUpgrade: _upgradeDB,
2929
);
@@ -62,13 +62,57 @@ class DatabaseHelper {
6262
whereArgs: [10, 45, 48],
6363
);
6464
}
65+
66+
if (oldVersion < 6) {
67+
// Migrate level_id from INTEGER to TEXT to support fractional levels (0.1, 0.2, etc.)
68+
// Also migrate level 0 to 0.1 for users who already completed it
69+
70+
// Create new tables with TEXT level_id
71+
await db.execute('''
72+
CREATE TABLE completed_levels_new (
73+
level_id TEXT PRIMARY KEY,
74+
completed_at TEXT NOT NULL,
75+
moves_count INTEGER NOT NULL
76+
)
77+
''');
78+
79+
await db.execute('''
80+
CREATE TABLE saved_games_new (
81+
level_id TEXT PRIMARY KEY,
82+
board_state TEXT NOT NULL,
83+
moves_count INTEGER NOT NULL,
84+
saved_at TEXT NOT NULL
85+
)
86+
''');
87+
88+
// Migrate data, converting level 0 to 0.1
89+
await db.execute('''
90+
INSERT INTO completed_levels_new (level_id, completed_at, moves_count)
91+
SELECT CASE WHEN level_id = 0 THEN '0.1' ELSE CAST(level_id AS TEXT) END,
92+
completed_at, moves_count
93+
FROM completed_levels
94+
''');
95+
96+
await db.execute('''
97+
INSERT INTO saved_games_new (level_id, board_state, moves_count, saved_at)
98+
SELECT CASE WHEN level_id = 0 THEN '0.1' ELSE CAST(level_id AS TEXT) END,
99+
board_state, moves_count, saved_at
100+
FROM saved_games
101+
''');
102+
103+
// Drop old tables and rename new ones
104+
await db.execute('DROP TABLE completed_levels');
105+
await db.execute('DROP TABLE saved_games');
106+
await db.execute('ALTER TABLE completed_levels_new RENAME TO completed_levels');
107+
await db.execute('ALTER TABLE saved_games_new RENAME TO saved_games');
108+
}
65109
}
66110

67111
Future<void> _createDB(Database db, int version) async {
68112
// Table for completed levels
69113
await db.execute('''
70114
CREATE TABLE completed_levels (
71-
level_id INTEGER PRIMARY KEY,
115+
level_id TEXT PRIMARY KEY,
72116
completed_at TEXT NOT NULL,
73117
moves_count INTEGER NOT NULL
74118
)
@@ -77,7 +121,7 @@ class DatabaseHelper {
77121
// Table for saved game states
78122
await db.execute('''
79123
CREATE TABLE saved_games (
80-
level_id INTEGER PRIMARY KEY,
124+
level_id TEXT PRIMARY KEY,
81125
board_state TEXT NOT NULL,
82126
moves_count INTEGER NOT NULL,
83127
saved_at TEXT NOT NULL
@@ -87,7 +131,7 @@ class DatabaseHelper {
87131

88132
// Completed Levels
89133
Future<void> markLevelCompleted({
90-
required int levelId,
134+
required String levelId,
91135
required int movesCount,
92136
}) async {
93137
final db = await database;
@@ -98,7 +142,7 @@ class DatabaseHelper {
98142
}, conflictAlgorithm: ConflictAlgorithm.replace);
99143
}
100144

101-
Future<bool> isLevelCompleted(int levelId) async {
145+
Future<bool> isLevelCompleted(String levelId) async {
102146
final db = await database;
103147
final result = await db.query(
104148
'completed_levels',
@@ -108,15 +152,15 @@ class DatabaseHelper {
108152
return result.isNotEmpty;
109153
}
110154

111-
Future<List<int>> getCompletedLevelIds() async {
155+
Future<List<String>> getCompletedLevelIds() async {
112156
final db = await database;
113157
final result = await db.query('completed_levels');
114-
return result.map((row) => row['level_id'] as int).toList();
158+
return result.map((row) => row['level_id'] as String).toList();
115159
}
116160

117161
// Saved Game States
118162
Future<void> saveGameState({
119-
required int levelId,
163+
required String levelId,
120164
required List<List<String>> board,
121165
required int movesCount,
122166
}) async {
@@ -129,7 +173,7 @@ class DatabaseHelper {
129173
}, conflictAlgorithm: ConflictAlgorithm.replace);
130174
}
131175

132-
Future<Map<String, dynamic>?> getSavedGameState(int levelId) async {
176+
Future<Map<String, dynamic>?> getSavedGameState(String levelId) async {
133177
final db = await database;
134178
final result = await db.query(
135179
'saved_games',
@@ -142,14 +186,14 @@ class DatabaseHelper {
142186
try {
143187
final row = result.first;
144188
final boardData = jsonDecode(row['board_state'] as String) as List<dynamic>;
145-
189+
146190
// Validate board structure
147191
if (boardData.isEmpty || boardData.first is! List) {
148192
// Invalid board structure, delete corrupted save
149193
await deleteSavedGameState(levelId);
150194
return null;
151195
}
152-
196+
153197
return {
154198
'board': boardData,
155199
'moves_count': row['moves_count'] as int,
@@ -161,7 +205,7 @@ class DatabaseHelper {
161205
}
162206
}
163207

164-
Future<void> deleteSavedGameState(int levelId) async {
208+
Future<void> deleteSavedGameState(String levelId) async {
165209
final db = await database;
166210
await db.delete('saved_games', where: 'level_id = ?', whereArgs: [levelId]);
167211
}
@@ -172,7 +216,7 @@ class DatabaseHelper {
172216
}
173217

174218
/// Clear saved games for specific levels (useful after level modifications)
175-
Future<void> clearSavedGamesForLevels(List<int> levelIds) async {
219+
Future<void> clearSavedGamesForLevels(List<String> levelIds) async {
176220
if (levelIds.isEmpty) return;
177221
final db = await database;
178222
final placeholders = List.filled(levelIds.length, '?').join(',');

lib/core/router/app_router.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class AppRouter {
8585
path: '$game/:levelId',
8686
name: game,
8787
pageBuilder: (context, state) {
88-
final levelId = int.parse(state.pathParameters['levelId']!);
88+
final levelId = state.pathParameters['levelId']!;
8989
return _fadeTransition(GameScreen(levelId: levelId), state);
9090
},
9191
),

lib/core/utils/market_helper.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,6 @@ class MarketHelper {
4141
static String _getCurrentOtherAppsUrl() {
4242
// Check which URL is currently active
4343
// The Makefile will modify this to switch between markets
44-
return GeneralConsts.otherAppsRustoreLink; // This will be changed by make
44+
return GeneralConsts.otherAppsGooglePlayLink; // This will be changed by make
4545
}
4646
}

lib/data/levels/level_0.2.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"board": [
3+
["x", "x", "1", "1", "1", "x", "x"],
4+
["x", "1", "1", "1", "1", "1", "x"],
5+
["1", "1", "1", "1", "1", "1", "1"],
6+
["1", "1", "1", "0", "1", "1", "1"],
7+
["1", "1", "1", "1", "1", "1", "1"],
8+
["x", "1", "1", "1", "1", "1", "x"],
9+
["x", "x", "1", "1", "1", "x", "x"]
10+
]
11+
}

lib/presentation/providers/completed_levels_provider.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import 'package:flutter_riverpod/flutter_riverpod.dart';
22
import 'package:pegma/core/database/database_helper.dart';
33

4-
final completedLevelsProvider = FutureProvider<List<int>>((ref) async {
4+
final completedLevelsProvider = FutureProvider<List<String>>((ref) async {
55
final db = DatabaseHelper.instance;
66
return await db.getCompletedLevelIds();
77
});
88

9-
final isLevelCompletedProvider = FutureProvider.family<bool, int>((
9+
final isLevelCompletedProvider = FutureProvider.family<bool, String>((
1010
ref,
1111
levelId,
1212
) async {

lib/presentation/providers/game_provider.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class Game extends _$Game {
1717
final DatabaseHelper _db = DatabaseHelper.instance;
1818

1919
@override
20-
GameState build(int levelId) {
20+
GameState build(String levelId) {
2121
ref.keepAlive();
2222
return GameState(board: <List<String>>[], possibleMoves: []);
2323
}
@@ -50,7 +50,7 @@ class Game extends _$Game {
5050
return LevelLoadType.fresh;
5151
}
5252

53-
Future<void> loadLevel(int level, {bool ignoreSaved = false}) async {
53+
Future<void> loadLevel(String level, {bool ignoreSaved = false}) async {
5454
try {
5555
Map<String, dynamic>? savedState;
5656

lib/presentation/providers/game_provider.g.dart

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)