Files
trainhub-flutter/lib/data/services/combo_service.dart
Kazimierz Ciołek 6dd7213eb0
Some checks failed
Build Linux App / build (push) Failing after 1m18s
Przebudowanie aplikacji, usprawnione AI, dodanie combo buildera
2026-07-06 23:44:17 +02:00

205 lines
5.4 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:uuid/uuid.dart';
const _uuid = Uuid();
/// One technique node in a combo graph.
class ComboNode {
const ComboNode({
required this.id,
required this.technique,
this.translation = '',
required this.dx,
required this.dy,
});
final String id;
final String technique;
final String translation;
final double dx;
final double dy;
ComboNode copyWith({double? dx, double? dy}) => ComboNode(
id: id,
technique: technique,
translation: translation,
dx: dx ?? this.dx,
dy: dy ?? this.dy,
);
Map<String, dynamic> toJson() => {
'id': id,
'technique': technique,
'translation': translation,
'dx': dx,
'dy': dy,
};
factory ComboNode.fromJson(Map<String, dynamic> json) => ComboNode(
id: json['id'] as String,
technique: json['technique'] as String,
translation: (json['translation'] ?? '') as String,
dx: (json['dx'] as num).toDouble(),
dy: (json['dy'] as num).toDouble(),
);
}
/// A directed transition between two techniques.
class ComboEdge {
const ComboEdge(this.fromId, this.toId);
final String fromId;
final String toId;
Map<String, dynamic> toJson() => {'from': fromId, 'to': toId};
factory ComboEdge.fromJson(Map<String, dynamic> json) =>
ComboEdge(json['from'] as String, json['to'] as String);
}
/// A named technique graph: nodes are techniques, edges are the transitions
/// that make sense between them.
class Combo {
const Combo({
required this.id,
required this.name,
this.nodes = const [],
this.edges = const [],
});
final String id;
final String name;
final List<ComboNode> nodes;
final List<ComboEdge> edges;
Combo copyWith({
String? name,
List<ComboNode>? nodes,
List<ComboEdge>? edges,
}) => Combo(
id: id,
name: name ?? this.name,
nodes: nodes ?? this.nodes,
edges: edges ?? this.edges,
);
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'nodes': nodes.map((n) => n.toJson()).toList(),
'edges': edges.map((e) => e.toJson()).toList(),
};
factory Combo.fromJson(Map<String, dynamic> json) => Combo(
id: json['id'] as String,
name: json['name'] as String,
nodes: [
for (final n in (json['nodes'] as List? ?? []))
ComboNode.fromJson(n as Map<String, dynamic>),
],
edges: [
for (final e in (json['edges'] as List? ?? []))
ComboEdge.fromJson(e as Map<String, dynamic>),
],
);
/// Random walk over the graph — the generated combination to drill.
/// Follows outgoing edges; if a node is a dead end the walk stops there.
List<ComboNode> generateSequence({int length = 4, Random? random}) {
if (nodes.isEmpty) return [];
final rnd = random ?? Random();
final byId = {for (final n in nodes) n.id: n};
final outgoing = <String, List<String>>{};
for (final e in edges) {
outgoing.putIfAbsent(e.fromId, () => []).add(e.toId);
}
// Prefer starting somewhere that actually leads on.
final starts = nodes.where((n) => outgoing.containsKey(n.id)).toList();
var current = (starts.isEmpty
? nodes
: starts)[rnd.nextInt((starts.isEmpty ? nodes : starts).length)];
final sequence = <ComboNode>[current];
while (sequence.length < length) {
final next = outgoing[current.id];
if (next == null || next.isEmpty) break;
current = byId[next[rnd.nextInt(next.length)]]!;
sequence.add(current);
}
return sequence;
}
}
/// Loads and persists combos as a JSON file in the app documents dir.
class ComboService extends ChangeNotifier {
static const _fileName = 'trainhub_combos.json';
List<Combo> _combos = [];
bool _loaded = false;
List<Combo> get combos => _combos;
Future<File> _file() async {
final dir = await getApplicationDocumentsDirectory();
return File(p.join(dir.path, _fileName));
}
Future<void> ensureLoaded() async {
if (_loaded) return;
_loaded = true;
try {
final file = await _file();
if (!file.existsSync()) return;
final json = jsonDecode(await file.readAsString()) as List<dynamic>;
_combos = [
for (final c in json) Combo.fromJson(c as Map<String, dynamic>),
];
notifyListeners();
} catch (e) {
if (kDebugMode) debugPrint('Failed to load combos: $e');
}
}
Combo create(String name) {
final combo = Combo(id: _uuid.v4(), name: name);
_combos = [..._combos, combo];
notifyListeners();
_persist();
return combo;
}
void delete(String id) {
_combos = _combos.where((c) => c.id != id).toList();
notifyListeners();
_persist();
}
/// Replaces a combo. Set [persist] false for high-frequency updates
/// (node dragging) and call [persistNow] once afterwards.
void update(Combo combo, {bool persist = true}) {
_combos = [for (final c in _combos) c.id == combo.id ? combo : c];
notifyListeners();
if (persist) _persist();
}
void persistNow() => _persist();
Future<void> _persist() async {
try {
final file = await _file();
await file.writeAsString(
jsonEncode(_combos.map((c) => c.toJson()).toList()),
);
} catch (e) {
if (kDebugMode) debugPrint('Failed to save combos: $e');
}
}
}