From edc283f3a1f82e536d342846873d02553d9dd35d Mon Sep 17 00:00:00 2001 From: Chen Asraf Date: Fri, 18 Feb 2022 19:11:56 +0200 Subject: [PATCH] dice with late stat modifier --- lib/dice.dart | 120 ++++++++++++++++++++---------- lib/models/repository.dart | 136 +++++++++++++++++++++++++++++----- scripts/migrate/v2_to_v3.dart | 13 ++-- test/dice_test.dart | 33 +++++++-- 4 files changed, 231 insertions(+), 71 deletions(-) diff --git a/lib/dice.dart b/lib/dice.dart index 2a21805..367bd43 100644 --- a/lib/dice.dart +++ b/lib/dice.dart @@ -5,22 +5,35 @@ class Dice { Dice({ required this.amount, required this.sides, - this.modifier, - }); + this.modifierValue, + this.modifierStat, + String? modifierSign, + }) : modifierSign = modifierSign ?? + (modifierValue != null + ? modifierValue >= 0 + ? "+" + : "-" + : "+"); final int amount; final int sides; - final int? modifier; + final int? modifierValue; + final String? modifierStat; + final String modifierSign; Dice copyWith({ int? amount, int? sides, - int? modifier, + int? modifierValue, + String? modifierSign, + String? modifierStat, }) => Dice( amount: amount ?? this.amount, sides: sides ?? this.sides, - modifier: modifier ?? this.modifier, + modifierSign: modifierSign ?? this.modifierSign, + modifierValue: modifierValue ?? this.modifierValue, + modifierStat: modifierStat ?? this.modifierStat, ); factory Dice.fromRawJson(String str) => Dice.fromJson(json.decode(str)); @@ -33,21 +46,18 @@ class Dice { static Dice d20 = Dice(amount: 1, sides: 20); static Dice d60 = Dice(amount: 1, sides: 60); static Dice d100 = Dice(amount: 1, sides: 100); + static final _dicePattern = RegExp(r'(\d+)d([0-9]+)(([+-])([0-9a-z]+))?', caseSensitive: false); String toRawJson() => toJson(); factory Dice.fromJson(String json) { - var parts = json.split("d"); - var amount = int.tryParse(parts[0]); - int? sides; - int? modifier; - if (parts[1].contains(RegExp(r'[-+]'))) { - var idx = parts[1].indexOf(RegExp(r'[^0-9]')); - sides = int.tryParse(parts[1].substring(0, idx)); - modifier = int.tryParse(parts[1].substring(idx)); - } else { - sides = int.tryParse(parts[1]); - } + var matches = _diceMatches(json); + var amount = int.tryParse(matches[0]!); + var sides = int.tryParse(matches[1]!); + var modifierSign = matches[2]; + var modifierValue = matches[3] != null ? int.tryParse(matches[3]!) : null; + var modifierStat = + matches[3] != null && modifierValue == null ? matches[3]!.toUpperCase() : null; if (sides == null || amount == null) { throw Exception("Dice parsing failed"); @@ -56,50 +66,84 @@ class Dice { return Dice( amount: amount, sides: sides, - modifier: modifier, + modifierValue: modifierSign == '-' ? -(modifierValue ?? 0) : modifierValue, + modifierSign: modifierSign, + modifierStat: modifierStat, ); } + Dice copyWithModifierValue(int statValue) => + copyWith(amount: amount, sides: sides, modifierValue: statValue); + @override String toString() => "${amount}d$sides$modifierWithSign"; String toJson() => toString(); - String get modifierWithSign => modifier == null - ? "" - : modifier! > 0 - ? "+$modifier" - : "$modifier"; + String get modifierWithSign => + hasModifier ? "$modifierSign${modifierValue?.abs() ?? modifierStat}" : ""; - DiceResult roll() => DiceResult.roll(this); + bool get hasModifier => (modifierValue != null || modifierStat != null); + + String get modifier => hasModifier ? modifierStat ?? modifierValue!.toString() : ""; + + DiceRoll roll() { + if (needsModifier) { + throw Exception("Dice is being rolled without an actual modifier." + "Use `copyWithModifierValue`.\n" + "Expected modifier: $modifierWithSign"); + } + var arr = []; + for (var i = 0; i < amount; i++) { + arr.add(Random().nextInt(sides)); + } + return DiceRoll(dice: this, results: arr); + } + + bool get needsModifier => modifierStat != null && modifierValue == null; operator *(int amount) => copyWith(amount: this.amount * amount); operator /(int amount) => copyWith(amount: this.amount ~/ amount); + + static List _diceMatches(String json) { + _assertDicePattern(json); + var m = _dicePattern.firstMatch(json)!; + return m.groups([1, 2, 4, 5]); + } + + static void _assertDicePattern(String dice) { + if (!_dicePattern.hasMatch(dice)) { + throw Exception("Dice format is invalid, must be {amount}d{sides}([+-]{modifier})" + "(e.g. 1d20, 2d6+DEX, 1d8-3)\n" + "Received: $dice"); + } + } } -class DiceResult { +class DiceRoll { final Dice dice; final List results; - DiceResult({required this.dice, required this.results}); - - static List rollMany(List dice) { - return dice.map((d) { - var arr = []; - for (var i = 0; i < d.amount; i++) { - arr.add(Random().nextInt(d.sides)); - } - return DiceResult(dice: d, results: arr); - }).toList(); + DiceRoll({required this.dice, required this.results}) { + assertDiceModifier(); } - int get total => - results.reduce((all, cur) => all + cur) + (dice.modifier ?? 0); + static List rollMany(List dice) => dice.map((d) => roll(d)).toList(); + + int get total => results.reduce((all, cur) => all + cur) + (dice.modifierValue ?? 0); bool get didHitNaturalMax => indexOfNaturalMax >= 0; int get indexOfNaturalMax => results.indexOf(dice.sides); - static DiceResult roll(Dice dice) { - return rollMany([dice])[0]; + static DiceRoll roll(Dice dice) { + return dice.roll(); + } + + void assertDiceModifier() { + if (dice.needsModifier) { + throw Exception("Dice is being rolled without an actual modifier." + "Use `dice.copyWithModifierValue(int modifierValue)`.\n" + "Expected modifier: ${dice.modifierWithSign}"); + } } } diff --git a/lib/models/repository.dart b/lib/models/repository.dart index 5a2e59e..e6685db 100644 --- a/lib/models/repository.dart +++ b/lib/models/repository.dart @@ -1,38 +1,134 @@ -class Repository {} +import '../character_class.dart'; +import '../alignment.dart'; +import '../item.dart'; +import '../monster.dart'; +import '../move.dart'; +import '../race.dart'; +import '../spell.dart'; +import '../tag.dart'; -class RepositoryItem { - final Map> _cache = {}; - String currentLocale; +class Repository { + String _currentLocale; + String get currentLocale => _currentLocale; + final Set _locales = {}; - RepositoryItem({ - required this.currentLocale, - }); + final Map> _alignments = {}; + final Map> _classes = {}; + final Map> _items = {}; + final Map> _monsters = {}; + final Map> _races = {}; + final Map> _moves = {}; + final Map> _spells = {}; + final Map> _tags = {}; - List get list { - _ensureLocale(currentLocale); - return _cache[currentLocale]!.values.toList(); - } + Repository({ + required String currentLocale, + }) : _currentLocale = currentLocale; - Map get map { - _ensureLocale(currentLocale); - return _cache[currentLocale]!; - } + RepositoryItem get alignments => _withCurrentLocale(_alignments); + RepositoryItem get classes => _withCurrentLocale(_classes); + RepositoryItem get items => _withCurrentLocale(_items); + RepositoryItem get monsters => _withCurrentLocale(_monsters); + RepositoryItem get races => _withCurrentLocale(_races); + RepositoryItem get moves => _withCurrentLocale(_moves); + RepositoryItem get spells => _withCurrentLocale(_spells); + RepositoryItem get tags => _withCurrentLocale(_tags); void registerLocale(String locale) { - if (_cache[locale] != null) { + if (localeExists(locale)) { return; } - _cache[locale] = {}; + + for (var entry in _allRepositories.entries) { + if (entry.value[locale] != null) { + continue; + } + entry.value[locale] = RepositoryItem(locale: locale); + } + + _locales.add(locale); } - void addItems(String locale, Map items) { + void changeLocale(String locale) { _ensureLocale(locale); - _cache[locale]!.addAll(items); + _currentLocale = locale; } + void loadItems(String locale, Map items) { + registerLocale(locale); + switch (T) { + case AlignmentValue: + _alignments[locale]!.addItems(items.cast()); + break; + case CharacterClass: + _classes[locale]!.addItems(items.cast()); + break; + case Item: + _items[locale]!.addItems(items.cast()); + break; + case Monster: + _monsters[locale]!.addItems(items.cast()); + break; + case Race: + _races[locale]!.addItems(items.cast()); + break; + case Move: + _moves[locale]!.addItems(items.cast()); + break; + case Spell: + _spells[locale]!.addItems(items.cast()); + break; + case Tag: + _tags[locale]!.addItems(items.cast()); + break; + default: + throw Exception("Type $T not supported"); + } + } + + Map>> get _allRepositories => { + 'alignments': _alignments, + 'classes': _classes, + 'items': _items, + 'monsters': _monsters, + 'races': _races, + 'moves': _moves, + 'spells': _spells, + 'tags': _tags, + }; + void _ensureLocale(String locale) { - if (_cache[locale] == null) { + if (!localeExists(locale)) { throw Exception('Locale $locale does not exist.'); } } + + bool localeExists(String locale) => _locales.contains(locale); + + T _withCurrentLocale(Map iter) { + _ensureLocale(currentLocale); + if (iter.isEmpty || iter[currentLocale] == null) { + throw Exception('Repository Item does not exist for locale $currentLocale'); + } + return iter[currentLocale]!; + } +} + +class RepositoryItem { + final Map _cache = {}; + final String locale; + + RepositoryItem({required this.locale}); + + List get list { + return _cache.values.toList(); + } + + Map get map { + return _cache; + } + + void addItems(Map items) { + _cache!.addAll(items); + } } diff --git a/scripts/migrate/v2_to_v3.dart b/scripts/migrate/v2_to_v3.dart index 2e59809..bdc3a8c 100644 --- a/scripts/migrate/v2_to_v3.dart +++ b/scripts/migrate/v2_to_v3.dart @@ -66,8 +66,7 @@ main() async { // Races print("Adding ${cls.raceMoves.length} races"); for (var race in cls.raceMoves) { - json['races']! - .add(raceMapper(race, cls.key ?? makeKey(cls.name)).toJson()); + json['races']!.add(raceMapper(race, cls.key ?? makeKey(cls.name)).toJson()); } // Classes @@ -121,12 +120,12 @@ String makeKey(String str) { } Set guessDice(String str) { - var basicRollPattern = RegExp(r'\broll\+[a-z]{3}\b', caseSensitive: false); + var basicRollPattern = RegExp(r'\broll([+-][a-z]+)\b', caseSensitive: false); var dicePattern = RegExp(r'\b\dd\d\b', caseSensitive: false); var found = {}; var basicRollMatches = basicRollPattern.allMatches(str); for (var match in basicRollMatches) { - found.add(Dice.d6 * 2); + found.add(Dice.fromJson('2d6' + match.group(1)!.toUpperCase())); } var diceMatches = dicePattern.allMatches(str); for (var match in diceMatches) { @@ -212,10 +211,8 @@ CharacterClass classMapper(old.PlayerClass cls) => CharacterClass( items: [ GearOption( amount: o.name.contains(RegExp(r'[0-9]+')) - ? double.tryParse(RegExp(r'[0-9]+') - .firstMatch(o.name) - ?.group(0) ?? - "1.0") ?? + ? double.tryParse( + RegExp(r'[0-9]+').firstMatch(o.name)?.group(0) ?? "1.0") ?? 1.0 : 1.0, item: Item( diff --git a/test/dice_test.dart b/test/dice_test.dart index 3494f14..15a2cea 100644 --- a/test/dice_test.dart +++ b/test/dice_test.dart @@ -9,21 +9,39 @@ void main() { var dice = Dice.fromJson(str); expect(dice.amount, equals(1)); expect(dice.sides, equals(6)); - expect(dice.modifier, equals(null)); + expect(dice.modifierValue, equals(null)); + expect(dice.modifierStat, equals(null)); + expect(dice.modifierSign, equals('+')); }); test("With positive modifier", () { var str = "3d8+3"; var dice = Dice.fromJson(str); expect(dice.amount, equals(3)); expect(dice.sides, equals(8)); - expect(dice.modifier, equals(3)); + expect(dice.modifierValue, equals(3)); + expect(dice.modifierStat, equals(null)); + expect(dice.modifierSign, equals('+')); }); test("With negative modifier", () { var str = "2d20-4"; var dice = Dice.fromJson(str); expect(dice.amount, equals(2)); expect(dice.sides, equals(20)); - expect(dice.modifier, equals(-4)); + expect(dice.modifierValue, equals(-4)); + expect(dice.modifierStat, equals(null)); + expect(dice.modifierSign, equals('-')); + }); + + test("With stat modifier", () { + var str = "1d6+DEX"; + var dice = Dice.fromJson(str); + expect(dice.amount, equals(1)); + expect(dice.sides, equals(6)); + expect(dice.modifierValue, equals(null)); + expect(dice.modifierStat, equals("DEX")); + expect(dice.modifierSign, equals('+')); + expect(dice.needsModifier, equals(true)); + expect(() => dice.roll(), throwsException); }); }); @@ -35,12 +53,17 @@ void main() { }); test("With positive modifier", () { var str = "3d8+3"; - var dice = Dice(amount: 3, sides: 8, modifier: 3); + var dice = Dice(amount: 3, sides: 8, modifierValue: 3); expect(dice.toJson(), equals(str)); }); test("With negative modifier", () { var str = "2d20-4"; - var dice = Dice(amount: 2, sides: 20, modifier: -4); + var dice = Dice(amount: 2, sides: 20, modifierValue: -4); + expect(dice.toJson(), equals(str)); + }); + test("With stat modifier", () { + var str = "2d20-DEX"; + var dice = Dice(amount: 2, sides: 20, modifierStat: "DEX", modifierSign: "-"); expect(dice.toJson(), equals(str)); }); });