dice with late stat modifier

This commit is contained in:
Chen Asraf
2022-02-18 19:11:56 +02:00
parent 7c1be49dde
commit edc283f3a1
4 changed files with 231 additions and 71 deletions

View File

@@ -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 = <int>[];
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<String?> _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<int> results;
DiceResult({required this.dice, required this.results});
static List<DiceResult> rollMany(List<Dice> dice) {
return dice.map((d) {
var arr = <int>[];
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<DiceRoll> rollMany(List<Dice> 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}");
}
}
}

View File

@@ -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<T> {
final Map<String, Map<String, T>> _cache = {};
String currentLocale;
class Repository {
String _currentLocale;
String get currentLocale => _currentLocale;
final Set<String> _locales = {};
RepositoryItem({
required this.currentLocale,
});
final Map<String, RepositoryItem<AlignmentValue>> _alignments = {};
final Map<String, RepositoryItem<CharacterClass>> _classes = {};
final Map<String, RepositoryItem<Item>> _items = {};
final Map<String, RepositoryItem<Monster>> _monsters = {};
final Map<String, RepositoryItem<Race>> _races = {};
final Map<String, RepositoryItem<Move>> _moves = {};
final Map<String, RepositoryItem<Spell>> _spells = {};
final Map<String, RepositoryItem<Tag>> _tags = {};
List<T> get list {
_ensureLocale(currentLocale);
return _cache[currentLocale]!.values.toList();
}
Repository({
required String currentLocale,
}) : _currentLocale = currentLocale;
Map<String, T> get map {
_ensureLocale(currentLocale);
return _cache[currentLocale]!;
}
RepositoryItem<AlignmentValue> get alignments => _withCurrentLocale(_alignments);
RepositoryItem<CharacterClass> get classes => _withCurrentLocale(_classes);
RepositoryItem<Item> get items => _withCurrentLocale(_items);
RepositoryItem<Monster> get monsters => _withCurrentLocale(_monsters);
RepositoryItem<Race> get races => _withCurrentLocale(_races);
RepositoryItem<Move> get moves => _withCurrentLocale(_moves);
RepositoryItem<Spell> get spells => _withCurrentLocale(_spells);
RepositoryItem<Tag> 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<String, T> items) {
void changeLocale(String locale) {
_ensureLocale(locale);
_cache[locale]!.addAll(items);
_currentLocale = locale;
}
void loadItems<T>(String locale, Map<String, T> items) {
registerLocale(locale);
switch (T) {
case AlignmentValue:
_alignments[locale]!.addItems(items.cast<String, AlignmentValue>());
break;
case CharacterClass:
_classes[locale]!.addItems(items.cast<String, CharacterClass>());
break;
case Item:
_items[locale]!.addItems(items.cast<String, Item>());
break;
case Monster:
_monsters[locale]!.addItems(items.cast<String, Monster>());
break;
case Race:
_races[locale]!.addItems(items.cast<String, Race>());
break;
case Move:
_moves[locale]!.addItems(items.cast<String, Move>());
break;
case Spell:
_spells[locale]!.addItems(items.cast<String, Spell>());
break;
case Tag:
_tags[locale]!.addItems(items.cast<String, Tag>());
break;
default:
throw Exception("Type $T not supported");
}
}
Map<String, Map<String, RepositoryItem<dynamic>>> 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<T>(Map<String, T> iter) {
_ensureLocale(currentLocale);
if (iter.isEmpty || iter[currentLocale] == null) {
throw Exception('Repository Item does not exist for locale $currentLocale');
}
return iter[currentLocale]!;
}
}
class RepositoryItem<T> {
final Map<String, T> _cache = {};
final String locale;
RepositoryItem({required this.locale});
List<T> get list {
return _cache.values.toList();
}
Map<String, T> get map {
return _cache;
}
void addItems(Map<String, T> items) {
_cache!.addAll(items);
}
}

View File

@@ -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<Dice> 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 = <Dice>{};
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(

View File

@@ -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));
});
});