diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 0a23d16..e919092 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -44,17 +44,17 @@ jobs: run: dart analyze --fatal-infos . - name: "VM Tests" - run: dart test --platform vm + run: cd packages/dart_dice_parser && dart test --platform vm - name: "Chrome Tests" - run: dart test --platform chrome + run: cd packages/dart_dice_parser && dart test --platform chrome - name: "Run example/simple.dart" - run: dart run --enable-asserts example/simple.dart - - name: "Run example/main.dart" - run: "dart run --enable-asserts example/main.dart -n 10 -o pretty '(3d6 + 3d6! + 3d6!!) #cs #cf #s #f'" + run: cd packages/dart_dice_parser && dart run --enable-asserts example/simple.dart + - name: "Run dicecli" + run: "dart run --enable-asserts dicecli -n 10 -o pretty '(3d6 + 3d6! + 3d6!!) #cs #cf #s #f'" - name: "Collect coverage" run: | - dart pub global activate coverage - dart pub global run coverage:test_with_coverage --branch-coverage + dart pub global activate coverage + cd packages/dart_dice_parser && dart pub global run coverage:test_with_coverage --branch-coverage - name: "Upload coverage" uses: codecov/codecov-action@v4 env: diff --git a/.vscode/launch.json b/.vscode/launch.json index 3bc65ea..93baa94 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -26,6 +26,15 @@ "request": "launch", "type": "dart" }, + { + "name": "(3d6!! + 4d6!) -<2", + "program": "example/main.dart", + "args": [ + "(3d6!! + 4d6!) -<2", + ], + "request": "launch", + "type": "dart" + }, { "name": "explode - old syntax", "program": "example/main.dart", diff --git a/.vscode/settings.json b/.vscode/settings.json index 056cf1b..42c961a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -35,4 +35,5 @@ // Force all files to have a trailing newline for consistency and reduced diffs when // adding new lines at the bottom of the file. "files.insertFinalNewline": true, + "dart.sdkPath": "~/.local/share/mise/installs/dart/3", } diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b581fb..68258cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,27 +1,69 @@ +# 8.0.0 + +## ⚠️⚠️ Breaking changes ⚠️⚠️ + +Roll results have changed significantly. Now, the individual die results are modeled +as a RolledDie object, rather than an integer. The RolledDie object incorporates +a lot of the metadata/scoring in pre-8.0.0 results. + +Having a more elaborate result will make it easier for client applications +to render the outcome of a roll. Each RolledDie knows its result, the # of sides on the die, and +whether it was counted as a success/failure. + +## 📈 Enhancements + +- upgrade to dart 3.8.0 +- added dependency on fast_immutable_collections +- allow compounding, exploding, and rerolls for 'odd' die (`dF, D66, and d[vals]`) +- API Changes + - change signature of roll method to be async `Future DiceExpression.roll()` + - on DiceExpression.create(), allow passing a `DiceRoller` implementation that returns `Stream` results + - remove metadata & score from RollResult + - rather than simple list of Integers representing the output of a roll or operation, now each + die rolled is a 'RolledDie' object which has the metadata/score. + - the RollResult's results field changes from List to IList. RolledDie represents not just the + outcome of the roll, but also includes metadata about the type of die, nsides of the die (if polyhedral die), + and metadata about the dice expression's modification of the rolls. + - RollResult.results will include both discarded an not-discarded RolledDie. + - RollSummary.results is just not-discarded die, there's a separate RollSummary.discarded. Alternatively, look at + RollSummary.detailedResults.results to see the whole set of die that accumulated while evaluating the expression. +- new dice expression syntax: + - add 'sort' to dice syntax -- `4d6 s` (ascending) or `4d6 sd` (descending) + - allow commas to separate different dice expressions: `4d4,6d6,8d8` + - implement penetrating dice ala Hackmaster + - `1d6p` -- roll d6, if 6 is rolled explode with d6s subtracting one each time. + - `1d100p20` -- roll a d100, if 100 is rolled penetrate with d20s + - use curly braces to create a 'total' result instead of keeping each individual result. + - `{3d6}` -- if that rolled `2,4,1`, those would be discarded and replaced with a `7` for the total. + # 7.1.1 + ## 🛠️ Bug fixes -- fix typo in readme +- fix typo in readme # 7.1.0 ## 📈 Enhancements -- upgrade to dart 3.6.0 -- add roll notation to support defining your own values. `1d[val1,val2,val3]` - For example, to roll 2 dice of primes under 20, use: `2d[2,3,5,7,11,13,17,19]` +- upgrade to dart 3.6.0 +- add roll notation to support defining your own values. `1d[val1,val2,val3]`. + For example, to roll 2 dice of primes under 20, use: `2d[2,3,5,7,11,13,17,19]` # 7.0.3 ## 🛠️ Bug fixes + - Previously, `4d6 + 3d6 #s #f` would only count success/failures of the 3d6 rolls, since counting had higher priority than the plus operation. You could workaround that with parens `(4d6 + 3d6) #s #f` In 7.0.3 and later, the counting operations now have the lowest priority. # 7.0.2 + - remove demo code accidentally added # 7.0.1 + - remove direct dependency on `meta` # 7.0.0 @@ -33,175 +75,219 @@ a tree of results from evaluating the dice expression. See the README for exampl traverse the graph. ## 📈 Enhancements + - new API for registering roll listeners. see README.md - analysis cleanup and dep upgrades ## 🛠️ Bug fixes -- propagate the roll metadata up to the top of the graph +- propagate the roll metadata up to the top of the graph # 6.0.2 + - more readme fixes - cleanup error handling / exceptions # 6.0.1 + - fix deprecation - minor readme language changes # 6.0.0 + - dart 3.0 requirement - upgrade deps # 5.1.5 + - clean up example documenation in README.md # 5.1.4 + - upgrade deps (lint) # 5.1.3 + - transfer repository to Adventuresmith org # 5.1.2 + - fix error messages with extraneous `'` - make examples in README less abstract - add method for json encoding roll result # 5.1.1 + - add syntax for exploding/compounding/reroll once - - `4d6 !o` - - `4d6 !!o` - - `4d6 ro` + - `4d6 !o` + - `4d6 !!o` + - `4d6 ro` - add syntax for success, failure, as well as crit success & failure. - - `4d6 #s=6` - - plain `#` counts results and transforms the expression from rolls into a count. - - but `#s`, `#f`, `#cs`, `#cf` only add metadata to the roll result, and can be chained together. - - `9d6! -= 3 #s>=5#f1#cs` -- roll 9d6 with exploding, drop any threes, count >=5 as success, 1s as failures, and 6 as critical success + - `4d6 #s=6` + - plain `#` counts results and transforms the expression from rolls into a count. + - but `#s`, `#f`, `#cs`, `#cf` only add metadata to the roll result, and can be chained together. + - `9d6! -= 3 #s>=5#f1#cs` -- roll 9d6 with exploding, drop any threes, count >=5 as success, 1s as failures, and + 6 as critical success # 5.1.0 + - make RollResult aggregate _all_ results for whole AST # 5.0.0 + - add compounding dice: `5d6!!` - add keep high/low: `2d20k` - add reroll: `10d4 r<=2` - allow exploding/compounding dice to have an rhs expression (>=,<=,=,<,>) - remove `!!` as 'limited explosion' (make it compounding, like Roll20 dice notation) -- fix syntax for exploding dice (2d6!, not 2d!6). -- remove exploding method from DiceRoller -- now it's part of the AST. +- fix syntax for exploding dice (2d6!, not 2d!6). +- remove exploding method from DiceRoller -- now it's part of the AST. - dice rolls return RollResult - clean up redundant parser config # 4.1.0 + - cleanup examples - add syntax for `>=` and `<=` for counts, drop, clamp - remove redundancy in parser defintion # 4.0.6 + - allow subtraction # 4.0.5 + - remove unused dev dependency # 4.0.4 + - fix typo in unit test # 4.0.3 + - increase test coverage -- test rollN and stats. # 4.0.1 + - add github actions, remove circleci - add codecov # 4.0.0 + - cleanup petitparser usage -- generate AST so that parsing the dice expression can be separate from rolling dice. # 3.1.0 + - remove subtraction - clean up add/mult -- don't collapse lists to ints - - `[1,4,5] + 2` => `[1,4,5,2]` - - `[1,4,5] * 2` => `[2,8,10]` + - `[1,4,5] + 2` => `[1,4,5,2]` + - `[1,4,5] * 2` => `[2,8,10]` - remove subtraction -- like division, too many corner cases - clean up error handling -- throw less often, less complex if/else statements. # 3.0.3 -- update linter and fix analysis + +- update linter and fix analysis # 3.0.2 + - fix circleci build # 3.0.1 + - minor analysis cleanup (dead code) # 3.0.0 + - upgrade deps & null safety - more error handling # 2.0.1 + - downgrade petitparser # 2.0.0 + - library upgrades including sdk >2.7.0 # 1.4.2 + - use unmodifiable view return types # 1.4.1 + - fix return types of stats objects # 1.4.0 + - replace use of stats library w/ implementation of welford's algorithm. see http://alias-i.com/lingpipe/docs/api/com/aliasi/stats/OnlineNormalEstimator.html - make DiceParser.rollN an async generator returning stream of results # 1.3.6 + - allow lowercase for `-H`, `-L`, `C>`, `C<` # 1.3.5 + - back off petitparser dependency -- back off to 2.2.1, so that we don't have dependency on Dart 2.3.0 (current stable flutter is 2.3.0-dev) # 1.3.4 + - make code more idiomatic # 1.3.3 + - make log fields non-public # 1.3.2 + - upgrade mockito dep # 1.3.0 + - add drop equals/less-than/greater-than - add cap/clamp - add counting operation - add exploding dice # 1.2.2 + - more logging and error handling cleanup # 1.2.1 + - more logging and error handling cleanup # 1.2.0 + - add dice stats to the parser - logging & error handling cleanup # 1.1.2 + - more tests and cleanup examples # 1.1.1 + - fix typo in readme # 1.1.0 + - add drop high/low parsing # 1.0.2 + - fix readme example # 1.0.1 + - fix readme issues # 1.0.0 + - bump min dart SDK to 2.2.2 - cleanup parser code - cleanup analysis problems @@ -210,12 +296,15 @@ traverse the graph. - flesh out API docs # 0.2.2 + - reformat # 0.2.1 + - upgrade some dependencies # 0.2.0 + - upgrade to dart 2 # 0.1.2 @@ -242,4 +331,3 @@ traverse the graph. # 0.0.1 - Initial version, created by Steve Christensen - diff --git a/LICENSE b/LICENSE index 02e8890..56f8f12 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Stephen Christensen +Copyright (c) 2025 Stephen Christensen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 7c154e7..90777e2 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,12 @@ # dart_dice_parser + [![Pub Package](https://img.shields.io/pub/v/dart_dice_parser.svg)](https://pub.dartlang.org/packages/dart_dice_parser) [![Dart](https://github.com/Adventuresmith/dart-dice-parser/actions/workflows/dart.yml/badge.svg)](https://github.com/Adventuresmith/dart-dice-parser/actions/workflows/dart.yml) [![codecov](https://codecov.io/gh/Adventuresmith/dart-dice-parser/branch/main/graph/badge.svg?token=YG5OYN9VY1)](https://codecov.io/gh/Adventuresmith/dart-dice-parser) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md) [![style: lint](https://img.shields.io/badge/style-lint-4BC0F5.svg)](https://pub.dev/packages/lint) - - -A dart library for parsing dice notation (`2d6+4`). Supports advantage/disadvantage, counting success/failures, +A dart library for parsing dice notation (`2d6+4`). Supports advantage/disadvantage, counting success/failures, exploding, compounding, and other variations. # Example @@ -16,17 +15,17 @@ exploding, compounding, and other variations. import 'package:dart_dice_parser/dart_dice_parser.dart'; -void main() { +Future main() async { // Create a roller for D20 advantage (roll 2d20, keep highest). final d20adv = DiceExpression.create('2d20 kh'); - stdout.writeln(d20adv.roll()); + stdout.writeln(await d20adv.roll()); // outputs: - // ((2d20) kh ) ===> RollSummary(total: 16, results: [16], metadata: {rolled: [4, 16], discarded: [4]}) + // ((2d20) kh ) ===> RollSummary(total: 16, results: [16(d20), 4(d20)⛔︎]) - stdout.writeln(d20adv.roll()); + stdout.writeln(await d20adv.roll()); // outputs: - // ((2d20) kh ) ===> RollSummary(total: 19, results: [19], metadata: {rolled: [13, 19], discarded: [13]}) + // ((2d20) kh ) ===> RollSummary(total: 19, results: [19(d20), 13(d20)⛔︎]) } ``` @@ -35,120 +34,153 @@ void main() { ## Examples: * `2d20 #cf #cs` - * roll 2d20, result will include counts of critical successes (20) and failures (1) + * roll 2d20, result will include counts of critical successes (20) and failures (1) * advantage - * `2d20-L` -- drop lowest - * `2d20k`, `2d20kh` -- keep highest + * `2d20-L` -- drop lowest + * `2d20k`, or `2d20kh` -- keep highest * disadvantage * `2d20-H` -- drop highest * `2d20-kl` -- keep lowest * `(2d10+3d20)-L3` -- roll 2d10 and 3d20, combine the two results lists, and drop lowest 3 results * `20d10-<3->8#` -- roll 20 d10, drop any less than 3 or greater than 8 and count the number of remaining dice - ## Supported notation * `2d6` -- roll `2` dice of `6` sides * special dice variations: - * `4dF` -- roll `4` fudge dice (sides: `[-1, -1, 0, 0, 1, 1]`) - * `1d%` -- roll `1` percentile dice (equivalent to `1d100`) - * `1D66` -- roll `1` D66, aka `1d6*10 + 1d6` - * **_NOTE_**: you _must_ use uppercase `D66`, lowercase `d66` will be interpreted as a 66-sided die - * `2d[2,3,5,7]`-- roll 2 dice with values `[2,3,5,7]` - + * `4dF` -- roll `4` fudge dice (sides: `[-1, -1, 0, 0, 1, 1]`) + * `1d%` -- roll `1` percentile dice (equivalent to `1d100` or `1d10 * 10 + 1d10`) + * `1D66` -- roll `1` D66, aka `1d6*10 + 1d6` + * **_NOTE_**: you _must_ use uppercase `D66`, lowercase `d66` will be interpreted as a 66-sided die + * `2d[2,3,5,7]`-- roll 2 dice with values `[2,3,5,7]` + * `4d6p` -- penetrating dice. Similar to exploding, and -1 is added for each subsequent reroll. + * `1d20p6`, `1d100p20` -- HackMaster rules say a d20 penetrates with d6s, and a d100 penetrates with d20s + * exploding dice - * `4d6!` -- roll `4` `6`-sided dice, explode if max (`6`) is rolled (re-roll and include in results) - * `4d6 !=5` or `4d6!5` -- explode a roll if equal to 5 - * `4d6 !>=4` - explode if >= 4 - * `4d6 !<=2` - explode if <=2 - * `4d6 !>5` - explode if > 5 - * `4d6 !<2` - explode if <2 - * To explode only once, use syntax `!o` - * `4d6 !o<5` + * `4d6!` -- roll `4` `6`-sided dice, explode if max (`6`) is rolled (re-roll and include in results) + * `4d6 !=5` or `4d6!5` -- explode a roll if equal to 5 + * `4d6 !>=4` - explode if >= 4 + * `4d6 !<=2` - explode if <=2 + * `4d6 !>5` - explode if > 5 + * `4d6 !<2` - explode if <2 + * To explode only once, use syntax `!o` + * `4d6 !o<5` * compounding dice (Shadowrun, L5R, etc). Similar to exploding, but the additional rolls for each dice are added together as a single "roll". The original roll is replaced by the sum of it and any additional rolls. - * `5d6 !!` -- roll `5` `6`-sided dice, compound - * `5d6 !!=5` or `5d6!5` -- compound a roll if equal to 5 - * `5d6 !!>=4` - compound if >= 4 - * `5d6 !!<=4` - compound if <= 4 - * `5d6 !!>5` - compound if > 5 - * `5d6 !!<3` - compound if < 3 - * To compound only once, use syntax `!!o` - * `5d6 !!o<2` + * `5d6 !!` -- roll `5` `6`-sided dice, compound + * `5d6 !!=5` or `5d6!5` -- compound a roll if equal to 5 + * `5d6 !!>=4` - compound if >= 4 + * `5d6 !!<=4` - compound if <= 4 + * `5d6 !!>5` - compound if > 5 + * `5d6 !!<3` - compound if < 3 + * To compound only once, use syntax `!!o` + * `5d6 !!o<2` * re-rolling dice: - * `4d4 r2` -- roll 4d4, re-roll any result = 2 - * `4d4 r=2` -- roll 4d4, re-roll any result = 2 - * `4d4 r<=2` -- roll 4d4, re-roll any <= 2 - * `4d4 r>=3` -- roll 4d4, re-roll any >= 3 - * `4d4 r<2` -- roll 4d4, re-roll any < 2 - * `4d4 r>3` -- roll 4d4, re-roll any > 3 - * To reroll only once, use syntax `ro` - * `4d4 ro<2` + * `4d4 r2` -- roll 4d4, re-roll any result = 2 + * `4d4 r=2` -- roll 4d4, re-roll any result = 2 + * `4d4 r<=2` -- roll 4d4, re-roll any <= 2 + * `4d4 r>=3` -- roll 4d4, re-roll any >= 3 + * `4d4 r<2` -- roll 4d4, re-roll any < 2 + * `4d4 r>3` -- roll 4d4, re-roll any > 3 + * To reroll only once, use syntax `ro` + * `4d4 ro<2` * keeping dice: - * `3d20 k 2` -- roll 3d20, keep 2 highest - * `3d20 kh 2` -- roll 3d20, keep 2 highest - * `3d20 kl 2` -- roll 3d20, keep 2 lowest + * `3d20 k 2` -- roll 3d20, keep 2 highest + * `3d20 kh 2` -- roll 3d20, keep 2 highest + * `3d20 kl 2` -- roll 3d20, keep 2 lowest * dropping dice: - * `4d6 -H` -- roll 4d6, drop 1 highest - * `4d6 -L` -- roll 4d6, drop 1 lowest - * `4d6 -H2` -- roll 4d6, drop 2 highest - * `4d6 -L2` -- roll 4d6, drop 2 lowest - * `4d6 ->5` -- roll 4d6, drop any results > 5 - * `4d6 -<2` -- roll 4d6, drop any results < 2 - * `4d6 ->=5` -- roll 4d6, drop any results >= 5 - * `4d6 -<=2` -- roll 4d6, drop any results <= 2 - * `4d6 -=1` -- roll 4d6, drop any results equal to 1 - * NOTE: the drop operators have higher precedence than - the arithmetic operators; `4d10-L2+2` is equivalent to `(4d10-L2)+2` - * NOTE: drop is not subtraction. - * `4d6 - 3` -- roll 4d6, subtract 3 - * `4d6 - 2d6` -- roll 4d6, subtract the result of rolling 2d6 + * `4d6 -H` -- roll 4d6, drop 1 highest + * `4d6 -L` -- roll 4d6, drop 1 lowest + * `4d6 -H2` -- roll 4d6, drop 2 highest + * `4d6 -L2` -- roll 4d6, drop 2 lowest + * `4d6 ->5` -- roll 4d6, drop any results > 5 + * `4d6 -<2` -- roll 4d6, drop any results < 2 + * `4d6 ->=5` -- roll 4d6, drop any results >= 5 + * `4d6 -<=2` -- roll 4d6, drop any results <= 2 + * `4d6 -=1` -- roll 4d6, drop any results equal to 1 + * NOTE: the drop operators have higher precedence than + the arithmetic operators; `4d10-L2+2` is equivalent to `(4d10-L2)+2` + * NOTE: drop is not subtraction. + * `4d6 - 3` -- roll 4d6, subtract 3 + * `4d6 - 2d6` -- roll 4d6, subtract the result of rolling 2d6 * cap/clamp: - * `4d20 C<5` -- roll 4d20, change any value < 5 to 5 - * `4d20 C>15` -- roll 4d20, change any value > 15 to 15 + * You can think of these similar to the floor and ceiling mathematical operations. + * `4d20 C<5` -- roll 4d20, change any value < 5 to 5. + * `4d20 C>15` -- roll 4d20, change any value > 15 to 15. +* sorting results + * `4d20 s` -- results sorted in ascending order + * `4d20 sd` -- results sorted in descending order +* totalling results - `{`,`}` + * In general, this library will try to keep each individual die result as its own separate entity + as long as possible. But, that can have unwanted consequences certain operations. + * For example, say you want to roll `2d6+2` but have a maximum result of 10. + * `(2d6+2)C>10` -- rolls 2d6 and adds 2. But, these are all individual results: `[3,6,2]`. + The clamping tests on each die roll individually, and doesn't have our desired effect + -- limiting the total result to no more than 10. + * Instead, use curly braces to aggregate the results into a total: `{2d6+2} C>10`. + If that subexpression resulted in `[3,6,2]` then the curly braces discard those results and replaces + them with `[11]`, which will be clamped to `10` + +* multiple expressions separated by comma + * `(1d8!,1d6!)kh` -- rolls 1d8 and 1d6 exploding, and keeps the highest result + * NOTE: each subexpression is evaluated into a result, and the individual rolls are discarded. + * example: `(1d8!,1d6!)kh` + * `1d6!` rolls a 6, and explodes with a 4. total: 10 + * `1d8!` rolls an 8, and explodes with a 1. total: 9 + * the comma-separated results are each replaced -- the rolls are discarded, and the results now are `10,9` + * the highest is kept. the expression's total is 10. * scoring dice rolls: - * counting: - * `4d6 #` -- how many results? - * For example, you might use this to count # of dice above a target. `(5d10 -<6)#` -- roll 5 d10, drop any less than 6, count results - * `4d6 #>3` -- roll 4d6, count any > 3 - * `4d6 #<3` -- roll 4d6, count any < 3 - * `4d6 #>=5` -- roll 4d6, count any >= 5 - * `4d6 #<=2` -- roll 4d6, count any <= 2 - * `4d6 #=5` -- roll 4d6, count any equal to 5 - * successes and failures - * A normal count operation `#` discards the rolled dice and changes the result to be the count - * For example, `2d6#<=3` rolls `[3,4]` then counts which results are `<=3` , returning `[1]` - * But, sometimes you want to be able to count successes/failures without discarding the dice rolls. - In this case, use modifiers `#s`, `#f`, `#cs`, `#cf` to add metadata to the results. - * `6d6 #f<=2 #s>=5 #cs6 #cf1` -- roll 6d6, count results <= 2 as failures, >= 5 as successes, =6 as critical successes, =1 as critical failures - * The above returns a result like: `RollSummary(total: 22, results: [6, 2, 1, 5, 3, 5], metadata: {rolled: [6, 2, 1, 5, 3, 5], score: {successes: [6, 5, 5], failures: [2, 1], critSuccesses: [6], critFailures: [1]}})` - * NOTE: order matters - * `2d20 kh #cf #cs` -- roll 2d20, keep the highest, count critical successes & failures. If this - rolled `[1,18]`, the `1` is dropped and the result metadata won't record a critical failure. - If that's not the behavior you want, move the counts prior to the drop (`2d20 #cf #cs kh`). - -* arithmetic operations - * parenthesis to force a certain order of operations - * addition is a little special -- could be a sum of ints, or it can be used to aggregate results of multiple dice rolls - * Addition of integers is the usual sum - * `4+5` - * `2d6 + 1` - * Addition of roll results combines the results (use parens to ensure the order of operations is what you desire) - * `(5d6+5d10)-L2` -- roll 5d6 and 5d10, and from aggregate results drop the lowest 2. - * `5d6+5d10-L2` -- roll 5d6 and 5d10, and from only the 5d10 results drop the lowest 2. equivalent to `5d6+(5d10-L2)` - * `*` for multiplication - * `-` for subtraction - * numbers must be integers - * division is not supported. + * counting: + * `4d6 #` -- how many results? + * For example, you might use this to count # of dice above a target. `(5d10 -<6)#` -- roll 5 d10, drop any + less than 6, count results + * `4d6 #>3` -- roll 4d6, count any > 3 + * `4d6 #<3` -- roll 4d6, count any < 3 + * `4d6 #>=5` -- roll 4d6, count any >= 5 + * `4d6 #<=2` -- roll 4d6, count any <= 2 + * `4d6 #=5` -- roll 4d6, count any equal to 5 + * successes and failures + * A simple count operation `#` discards the rolled dice and changes the result to be the count of results matching the condition + * For example, `2d6#<=3` rolls `[3,4]` then counts which results are `<=3` , returning `[1]` + * But, sometimes you want to be able to count successes/failures without discarding the dice rolls. + In this case, use modifiers `#s`, `#f`, `#cs`, `#cf` to add metadata to the results. + * without modifiers, `#s` or `#cs` will count the result as a success if it matches the maximum possible value for the die + * without modifiers, `#f` or `#cf` will count the result as a failure if it matches the minimum possible value for the die + * `6d6 #f<=2 #s>=5 #cs6 #cf1` -- roll 6d6, count results <= 2 as failures, >= 5 as successes, =6 as critical + successes, =1 as critical failures + * The above returns a result like: + `RollSummary(total: 22, results: [6, 2, 1, 5, 3, 5], metadata: {rolled: [6, 2, 1, 5, 3, 5], score: {successes: [6, 5, 5], failures: [2, 1], critSuccesses: [6], critFailures: [1]}})` + * NOTE: order matters + * `2d20 kh #cf #cs` -- roll 2d20, keep the highest, count critical successes & failures. If this + rolled `[1,18]`, the `1` is dropped and the result does not record a critical failure. +* arithmetic operations + * parenthesis to force a certain order of operations + * addition is a little special -- could be a sum of ints, or it can be used to aggregate results of multiple dice + rolls + * Addition of integers is the usual sum + * `4+5` + * `2d6 + 1` + * Addition of roll results combines the results (use parens to ensure the order of operations is what you + desire) + * `(5d6+5d10)-L2` -- roll 5d6 and 5d10, and from aggregate results drop the lowest 2. + * `5d6+5d10-L2` -- roll 5d6 and 5d10, and from only the 5d10 results drop the lowest 2. equivalent to + `5d6+(5d10-L2)` + * `((1d6 + 1d8)-L)!` -- roll 1d6 and 1d8, drop the lowest result, and explode the remaining die + * `*` for multiplication + * `-` for subtraction + * numbers must be integers + * division is not supported. # Random Number Generator By default, Random.secure() is used. You can select other RNGs when creating the -dice expression. Random() will be faster than Random.secure(); if you're doing lots of rolls -for use cases where security doesn't matter, you will want to use Random(). +dice expression. + +Random() is much faster than Random.secure(); if you're doing many rolls +for use cases where security doesn't matter, you probably want to use Random(). For example, you might create a dice-rolling app that both provides rolls _and_ displays statistics (mean, stddev, etc) about the dice expression. To do that, you might create two separate @@ -156,27 +188,29 @@ For example, you might create a dice-rolling app that both provides rolls _and_ clicks a button), and the second to display min/max/mean/stddev/etc ```dart - final diceExpr_SecureRNG = DiceExpression.create('2d6'); - final diceExpr_FastRNG = DiceExpression.create('2d6', Random()); - - //.... - // on button-click, roll the dice - final roll = diceExpr_SecureRNG.roll(); - - //.... - // when dice expr changes, update the stats graph. - final stats = await diceExpr_FastRNG.stats(); - // output of stats: {mean: 6.98, stddev: 2.41, min: 2, max: 12, count: 10000, histogram: {2: 310, 3: 557, 4: 787, 5: 1090, 6: 1450, 7: 1646, 8: 1395, 9: 1147, 10: 825, 11: 526, 12: 267}} + +final diceExpr_SecureRNG = DiceExpression.create('2d6'); +final diceExpr_FastRNG = DiceExpression.create('2d6', RNGRoller(Random())); + +//.... +// on button-click, roll the dice +final roll = await diceExpr_SecureRNG.roll(); + +//.... +// when dice expr changes, update the stats graph. +final stats = await diceExpr_FastRNG.stats(); +// output of stats: {mean: 6.98, stddev: 2.41, min: 2, max: 12, count: 10000, histogram: {2: 310, 3: 557, 4: 787, 5: 1090, 6: 1450, 7: 1646, 8: 1395, 9: 1147, 10: 825, 11: 526, 12: 267}} ``` # CLI Usage -There's no executable in bin, but there's an example CLI at `example/main.dart`. +Within the repository, there's a packages/dicecli that can be used to experiment with different syntax. Usage: + ``` -❯ dart run example/main.dart -h +❯ dart run dicecli -h Usage: -n, --num Number of times to roll the expression (defaults to "1") @@ -201,91 +235,176 @@ Usage: Examples: ```console -❯ dart example/main.dart '3d6' -(3d6) ===> RollSummary(total: 13, results: [3, 6, 4], metadata: {rolled: [3, 6, 4]}) - +❯ dart run dicecli '3d6' +(3d6) ===> RollSummary(total: 9, results: [6(d6), 2(d6), 1(d6)]) # run N number of rolls -❯ dart run example/main.dart -n5 '3d6' -(3d6) ===> RollSummary(total: 10, results: [2, 2, 6], metadata: {rolled: [2, 2, 6]}) -(3d6) ===> RollSummary(total: 12, results: [6, 4, 2], metadata: {rolled: [6, 4, 2]}) -(3d6) ===> RollSummary(total: 5, results: [2, 1, 2], metadata: {rolled: [2, 1, 2]}) -(3d6) ===> RollSummary(total: 10, results: [5, 3, 2], metadata: {rolled: [5, 3, 2]}) -(3d6) ===> RollSummary(total: 13, results: [4, 5, 4], metadata: {rolled: [4, 5, 4]}) +❯ dart run dicecli -n5 '3d6' +(3d6) ===> RollSummary(total: 6, results: [1(d6), 4(d6), 1(d6)]) +(3d6) ===> RollSummary(total: 9, results: [4(d6), 1(d6), 4(d6)]) +(3d6) ===> RollSummary(total: 14, results: [4(d6), 6(d6), 4(d6)]) +(3d6) ===> RollSummary(total: 10, results: [4(d6), 3(d6), 3(d6)]) +(3d6) ===> RollSummary(total: 6, results: [3(d6), 2(d6), 1(d6)]) + # show statistics for a dice expression -❯ dart example/main.dart -s '3d6' +❯ dart run dicecli -s '3d6' {mean: 10.5, stddev: 2.97, min: 3, max: 18, count: 10000, histogram: {3: 49, 4: 121, 5: 273, 6: 461, 7: 727, 8: 961, 9: 1153, 10: 1182, 11: 1272, 12: 1151, 13: 952, 14: 733, 15: 486, 16: 289, 17: 154, 18: 36}} ``` Sometimes it's nice to change the output type so you can see the graph of results: + ```console # show the result graph: -❯ dart run example/main.dart -o pretty '3d6 #cs #cf' -(((3d6) #cs ) #cf ) ===> RollSummary(total: 13, results: [1, 6, 6], metadata: {rolled: [1, 6, 6], score: {critSuccesses: [6, 6], critFailures: [1]}}) - (((3d6) #cs ) #cf ) =count=> RollResult(total: 13, results: [1, 6, 6], metadata: {score: {critFailures: [1]}}) - ((3d6) #cs ) =count=> RollResult(total: 13, results: [1, 6, 6], metadata: {score: {critSuccesses: [6, 6]}}) - (3d6) =rollDice=> RollResult(total: 13, results: [1, 6, 6], metadata: {rolled: [1, 6, 6]}) - - -❯ dart run example/main.dart -o json '3d6 #cs #cf' -{"expression":"(((3d6) #cs ) #cf )","total":9,"results":[2,3,4],"detailedResults":{"expression":"(((3d6) #cs ) #cf )","opType":"count","nsides":6,"ndice":3,"results":[2,3,4],"left":{"expression":"((3d6) #cs )","opType":"count","nsides":6,"ndice":3,"results":[2,3,4],"left":{"expression":"(3d6)","opType":"rollDice","nsides":6,"ndice":3,"results":[2,3,4],"metadata":{"rolled":[2,3,4]}}}},"metadata":{"rolled":[2,3,4]}} - +❯ dart run dicecli -o pretty '3d6 #cs #cf' +(((3d6) #cs ) #cf ) ===> RollSummary(total: 13, results: [6(d6✅), 5(d6), 2(d6)], critSuccessCount: 1) + (((3d6) #cs ) #cf ) =count=> RollResult(total: 13, results: [6(d6✅), 5(d6), 2(d6)]) + ((3d6) #cs ) =count=> RollResult(total: 13, results: [6(d6✅), 5(d6), 2(d6)]) + (3d6) =rollDice=> RollResult(total: 13, results: [6(d6), 5(d6), 2(d6)]) +❯ dart run dicecli -o json '3d6 #cs #cf' | jq +{ + "expression": "(((3d6) #cs ) #cf )", + "total": 12, + "results": [ + { + "result": 4, + "nsides": 6, + "dieType": "polyhedral" + }, + { + "result": 5, + "nsides": 6, + "dieType": "polyhedral" + }, + { + "result": 3, + "nsides": 6, + "dieType": "polyhedral" + } + ], + "detailedResults": { + "expression": "(((3d6) #cs ) #cf )", + "opType": "count", + "results": [ + { + "result": 4, + "nsides": 6, + "dieType": "polyhedral" + }, + { + "result": 5, + "nsides": 6, + "dieType": "polyhedral" + }, + { + "result": 3, + "nsides": 6, + "dieType": "polyhedral" + } + ], + "left": { + "expression": "((3d6) #cs )", + "opType": "count", + "results": [ + { + "result": 4, + "nsides": 6, + "dieType": "polyhedral" + }, + { + "result": 5, + "nsides": 6, + "dieType": "polyhedral" + }, + { + "result": 3, + "nsides": 6, + "dieType": "polyhedral" + } + ], + "left": { + "expression": "(3d6)", + "opType": "rollDice", + "results": [ + { + "result": 4, + "nsides": 6, + "dieType": "polyhedral" + }, + { + "result": 5, + "nsides": 6, + "dieType": "polyhedral" + }, + { + "result": 3, + "nsides": 6, + "dieType": "polyhedral" + } + ], + "total": 12 + }, + "total": 12 + }, + "total": 12 + } +} ``` ## Statistics output +I often wonder, "what range of values should I expect?" or "how likely will this roll explode or compound?" -I often wonder, "what range of values should I expect?" or "how likely will this roll explode or compound?" - -To explore that, you can use stats output: +To explore that, you can use stats output: ```console # roll 4d6 -❯ dart run example/main.dart -s '4d6' +❯ dart run dicecli -s '4d6' {mean: 14.0, stddev: 3.44, min: 4, max: 24, count: 10000, histogram: {4: 5, 5: 28, 6: 72, 7: 163, 8: 280, 9: 468, 10: 573, 11: 816, 12: 948, 13: 1069, 14: 1047, 15: 1118, 16: 1010, 17: 786, 18: 637, 19: 412, 20: 278, 21: 161, 22: 84, 23: 32, 24: 13}} # roll 4d6 explode -❯ dart run example/main.dart -s '4d6!' +❯ dart run dicecli -s '4d6!' {mean: 16.7, stddev: 6.46, min: 4, max: 54, count: 10000, histogram: {4: 6, 5: 32, 6: 87, 7: 166, 8: 264, 9: 403, 10: 558, 11: 655, 12: 690, 13: 734, 14: 695, 15: 651, 16: 621, 17: 607, 18: 498, 19: 507, 20: 425, 21: 350, 22: 353, 23: 260, 24: 229, 25: 194, 26: 167, 27: 150, 28: 143, 29: 118, 30: 99, 31: 69, 32: 53 # roll 4d5 compounding -❯ dart run example/main.dart -s '4d6!!' +❯ dart run dicecli -s '4d6!!' {mean: 16.8, stddev: 6.43, min: 4, max: 58, count: 10000, histogram: {4: 6, 5: 29, 6: 78, 7: 162, 8: 230, 9: 389, 10: 506, 11: 635, 12: 726, 13: 705, 14: 751, 15: 682, 16: 597, 17: 602, 18: 571, 19: 498, 20: 456, 21: 365, 22: 310, 23: 286, 24: 262, 25: 192, 26: 167, 27: 156, 28: 114, 29: 92, 30: 76, 31: 72, 32: 57, 33: 41, 34: 32, 35: 31, 36: 20, 37: 15, 38: 16, 39: 10, 40: 12, 41: 8, 42: 10, 43: 4, 44: 8, 45: 5, 46: 4, 47: 2, 48: 1, 49: 2, 50: 3, 51: 2, 53: 1, 58: 1}} ``` -Or, if I wonder, "in those rolls, how many times might I see a roll >=6?". For that case, I can include a count operation - +Or, if I wonder, "in those rolls, how many times might I see a roll >=6?". For that case, I can include a count +operation ```console -❯ dart run example/main.dart -s '4d6 #>=6' +❯ dart run dicecli -s '4d6 #>=6' {mean: 0.664, stddev: 0.75, min: 0, max: 4, count: 10000, histogram: {0: 4871, 1: 3781, 2: 1192, 3: 144, 4: 12}} -❯ dart run example/main.dart -s '4d6! #>=6' +❯ dart run dicecli -s '4d6! #>=6' {mean: 0.793, stddev: 0.973, min: 0, max: 7, count: 10000, histogram: {0: 4846, 1: 3213, 2: 1328, 3: 449, 4: 116, 5: 40, 6: 4, 7: 4}} -❯ dart run example/main.dart -s '4d6!! #>=6' +❯ dart run dicecli -s '4d6!! #>=6' {mean: 0.665, stddev: 0.747, min: 0, max: 4, count: 10000, histogram: {0: 4840, 1: 3845, 2: 1144, 3: 164, 4: 7}} ``` - # Reacting to dice rolls in your application If you're using this package within an app, you probably want to display dice-rolling events to the user. -There's a couple ways for you to act on roll results. Depending on your use-case, one or more will hopefully fit your needs. +There's a couple ways for you to act on roll results. Depending on your use-case, one or more will hopefully fit your +needs. ## Traversing the result graph -Use the 'left' and 'right' fields of each RollResult node to walk the graph. For an example, see [simple.dart](example/simple.dart) -When a dice expression is parsed, it creates a binary tree to evaluate the roll. +Use the 'left' and 'right' fields of each RollResult node to walk the graph. For an example, +see [simple.dart](packages/dart_dice_parser/example/dart_dice_parser_example.dart) + +When a dice expression is parsed, it creates a binary tree to evaluate the roll. For example, the expression `(3d6 + 3d6!) kh3` means "roll 3d6 and 3d6!, combine results. Keep the 3 highest" and it creates a graph like: @@ -304,7 +423,7 @@ flowchart TD; ROLLA(["3d6"]); ``` -When you roll the dice expression, it traverses the tree from the bottom up and rolls dice or performs the +When you roll the dice expression, it traverses the tree from the bottom up and rolls dice or performs the requested operations. ```mermaid @@ -338,13 +457,15 @@ flowchart TD SUMMARY@{ shape: doc, label: "RollSummary

Total: 15
results:6,5,4

rolled: 2,3,6,4,1,5,2
discarded: 2,3,1,2" }; ``` -## Convert RollResult to JSON +## Convert RollResult to JSON ```dart - Map rollResultAsJson = DiceExpression.create('2d20kh').roll().toJson(); + +Map rollResultAsJson = (await DiceExpression.create('2d20kh').roll()).toJson(); ``` -The returned objects will look roughly like: +The returned objects will look roughly like: + ```json { "expression": "((2d20) kh )", @@ -396,27 +517,28 @@ The returned objects will look roughly like: ``` - ## Listen to RollResult events You can register one or more listeners that will be informed of roll events. There is a default logging listener that logs at FINE level. + ```dart - // if you want to listen to every individual operation within the expression - DiceExpression.registerListener((rollResult) { - stdout.writeln('${rollResult.opType.name} -> $rollResult'); - }); +// if you want to listen to every individual operation within the expression +DiceExpression.registerListener((rollResult) { + stdout.writeln('${rollResult.opType.name} -> $rollResult'); +}); - // if you want to listen for the RollSummary - DiceExpression.registerSummaryListener((rollSummary) { - stdout.writeln('$rollSummary'); - }); +// if you want to listen for the RollSummary +DiceExpression.registerSummaryListener((rollSummary) { + stdout.writeln('$rollSummary');[README.md](README.md) +}); ``` Alternatively, you may not want to know _all_ roll events, and are only interested in the events for your specific roll. In that case, pass an 'onRoll' method to the `roll()` method + ```dart DiceExpression.create('2d20kh').roll( onRoll: (rr) => stdout.writeln('roll - $rr'), @@ -424,7 +546,6 @@ the events for your specific roll. In that case, pass an 'onRoll' method to the ); ``` - # Features and bugs Please file feature requests and bugs at the [issue tracker][tracker]. diff --git a/analysis_options.yaml b/analysis_options.yaml index d45e01d..af5e7f4 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -12,7 +12,7 @@ analyzer: strict-casts: true strict-raw-types: true exclude: - - dice_parser_demo/** + - packages/diceui/** #- '**.freezed.dart' #- '**.g.dart' diff --git a/example/simple.dart b/example/simple.dart deleted file mode 100644 index 5b0107a..0000000 --- a/example/simple.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'dart:io'; -import 'dart:math'; - -import 'package:collection/collection.dart'; -import 'package:dart_dice_parser/dart_dice_parser.dart'; -import 'package:logging/logging.dart'; - -final listEquals = const ListEquality().equals; - -/// NOTE: to run w/ asserts: dart run --enable-asserts example/simple.dart - -Future main() async { - Logger.root.level = Level.INFO; - - Logger.root.onRecord.listen((rec) { - stdout.writeln( - '[${rec.level.name.padLeft(7)}] ${rec.loggerName.padLeft(12)}: ${rec.message}', - ); - }); - - // Create a roller for `4d20 kh2 #cf #cs` (roll 4d20, keep highest 2, and track critical success/failure). - // - // The following example uses a seeded RNG so that results are the same on every run (so that the asserts below won't fail) - // - final d20adv = DiceExpression.create('4d20 kh2 #cf #cs', Random(4321)); - - // repeated rolls of the dice expression generate different results - final result1 = d20adv.roll(); - final result2 = d20adv.roll(); - - stdout.writeln(result1); - stdout.writeln(result2); - // outputs: - // ((((4d20) kh 2) #cf ) #cs ) ===> RollSummary(total: 34, results: [17, 17], metadata: {rolled: [11, 12, 17, 17], discarded: [12, 11]}) - // ((((4d20) kh 2) #cf ) #cs ) ===> RollSummary(total: 39, results: [20, 19], metadata: {rolled: [1, 12, 19, 20], discarded: [12, 1], score: {critSuccesses: [20]}}) - - // demonstrate navigation of the result graph - assert(result2.total == 39); - assert( - listEquals( - result2.results, - [20, 19], - ), - ); - // read the score-related properties - assert(!result2.hasSuccesses); - assert(!result2.hasFailures); - assert(!result2.hasCritFailures); - assert(result2.hasCritSuccesses); - assert(result2.metadata.score.critSuccessCount == 1); - assert( - listEquals( - result2.metadata.score.critSuccesses, - [20], - ), - ); - - // look at the expression tree : - // ((((4d20) kh 2) #cf ) #cs ) ===> RollSummary(total: 39, results: [20, 19], metadata: {rolled: [1, 12, 19, 20], discarded: [12, 1], score: {critSuccesses: [20]}}) - // ((((4d20) kh 2) #cf ) #cs ) =count=> RollResult(total: 39, results: [20, 19], metadata: {score: {critSuccesses: [20]}}) - // (((4d20) kh 2) #cf ) =count=> RollResult(total: 39, results: [20, 19]) - // ((4d20) kh 2) =drop=> RollResult(total: 39, results: [20, 19], metadata: {discarded: [12, 1]}) - // (4d20) =rollDice=> RollResult(total: 52, results: [1, 12, 19, 20], metadata: {rolled: [1, 12, 19, 20]}) - // at the top level, it's a 'count' operation that counted the critical success - final top = result2.detailedResults; - assert(top.opType == OpType.count); - assert( - top.metadata == - const RollMetadata( - score: RollScore( - critSuccesses: [20], - ), - ), - ); - // next level is the count critical failures node of the graph - // NOTE: despite there being a 1 rolled, the criticalFailure expression is _after_ the `1` is discarded by the lower expression - assert(top.left!.opType == OpType.count); - assert(top.left!.metadata.score.hasCritFailures == false); - - assert(top.left!.left!.opType == OpType.drop); - assert( - listEquals( - top.left!.left!.metadata.discarded, - [12, 1], - ), - ); - - assert(top.left!.left!.left!.opType == OpType.rollDice); - - assert( - listEquals( - top.left!.left!.left!.results, - [1, 12, 19, 20], - ), - ); - assert( - listEquals( - top.left!.left!.left!.metadata.rolled, - [1, 12, 19, 20], - ), - ); - - final stats = await DiceExpression.create('2d6', Random(1234)).stats(); - // output: - // {mean: 6.99, stddev: 2.4, min: 2, max: 12, count: 1000, histogram: {2: 27, 3: 56, 4: 90, 5: 98, 6: 138, 7: 180, 8: 141, 9: 109, 10: 80, 11: 51, 12: 30}} - stdout.writeln(stats); -} diff --git a/lib/src/ast.dart b/lib/src/ast.dart deleted file mode 100644 index 75260d9..0000000 --- a/lib/src/ast.dart +++ /dev/null @@ -1,814 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:petitparser/parser.dart'; - -import 'dice_expression.dart'; -import 'dice_roller.dart'; -import 'results.dart'; -import 'utils.dart'; - -/// default limit for rerolls/exploding/compounding to avoid getting stuck in loop -const defaultRerollLimit = 1000; - -/// A value expression. The token we read from input will be a String, -/// it must parse as an int, and an empty string will return empty set. -class SimpleValue extends DiceExpression { - SimpleValue(this.value) - : _results = RollResult( - expression: value, - opType: OpType.value, - results: value.isEmpty ? [] : [int.parse(value)], - ); - - final String value; - final RollResult _results; - - @override - RollResult call() => _results; - - @override - String toString() => value; -} - -/// All our operations will inherit from this class. -/// The `call()` method will be called by the parent node. -/// The `eval()` method is called from the node -abstract class DiceOp extends DiceExpression with LoggingMixin { - // each child class should override this to implement their operation - RollResult eval(); - - // all children can share this call operator -- and it'll let us be consistent w/ regard to logging - @override - RollResult call() { - final result = eval(); - logger.finer(() => '$result'); - return result; - } -} - -/// base class for unary operations -abstract class Unary extends DiceOp { - Unary(this.name, this.left); - - final String name; - final DiceExpression left; - - @override - String toString() => '($left)$name'; -} - -/// base class for binary operations -abstract class Binary extends DiceOp { - Binary(this.name, this.left, this.right); - - final String name; - final DiceExpression left; - final DiceExpression right; - - @override - String toString() => '($left $name $right)'; -} - -/// multiply operation (flattens results) -class MultiplyOp extends Binary { - MultiplyOp(super.name, super.left, super.right); - - @override - RollResult eval() => left() * right(); -} - -/// add operation -class AddOp extends Binary { - AddOp(super.name, super.left, super.right); - - @override - RollResult eval() => left() + right(); -} - -/// subtraction operation -class SubOp extends Binary { - SubOp(super.name, super.left, super.right); - - @override - RollResult eval() => left() - right(); -} - -/// variation on count -- count how many results from lhs are =,<,> rhs. -class CountOp extends Binary { - CountOp( - super.name, - super.left, - super.right, [ - this.countType = CountType.count, - ]) { - if (name.startsWith('#s')) { - countType = CountType.success; - } else if (name.startsWith('#f')) { - countType = CountType.failure; - } else if (name.startsWith('#cs')) { - countType = CountType.critSuccess; - } else if (name.startsWith('#cf')) { - countType = CountType.critFailure; - } else { - countType = CountType.count; - } - } - - CountType countType; - - @override - RollResult eval() { - final lhs = left(); - final rhs = right(); - - var rhsEmptyAndSimpleCount = false; - final target = rhs.totalOrDefault( - () { - // if missing RHS, we can make assumptions depending on operator. - // - switch (name) { - case '#': - // example: '3d6#' should be 3. target is ignored in case statement below. - rhsEmptyAndSimpleCount = true; - return 0; - case '#s' || '#cs': - // example: '3d6#s' -- assume target is nsides (maximum) - return lhs.nsides; - case '#f' || '#cf': - // example: '3d6#f' -- assume target is 1 (minimum) - return 1; - default: - throw FormatException( - 'Invalid count operation. Missing count target', - toString(), - toString().length, - ); - } - }, - ); - bool test(int v) { - switch (name) { - case '#>=' || '#s>=' || '#f>=' || '#cs>=' || '#cf>=': - // how many results on lhs are greater than or equal to rhs? - return v >= target; - case '#<=' || '#s<=' || '#f<=' || '#cs<=' || '#cf<=': - // how many results on lhs are less than or equal to rhs? - return v <= target; - case '#>' || '#s>' || '#f>' || '#cs>' || '#cf>': - // how many results on lhs are greater than rhs? - return v > target; - case '#<' || '#s<' || '#f<' || '#cs<' || '#cf<': - // how many results on lhs are less than rhs? - return v < target; - case '#=' || '#s=' || '#f=' || '#cs=' || '#cf=': - // how many results on lhs are equal to rhs? - return v == target; - case '#' || '#s' || '#f' || '#cs' || '#cf': - if (rhsEmptyAndSimpleCount) { - // if missing rhs, we're just counting results - // that is, '3d6#' should return 3 - return true; - } else { - // if not missing rhs, treat it as equivalent to '#='. - // that is, '3d6#2' should count 2s - return v == target; - } - default: - throw FormatException( - "unknown count operation '$name'", - toString(), - toString().indexOf(name), - ); - } - } - - final filteredResults = lhs.results.where(test); - - if (countType == CountType.count) { - // if counting, the count becomes the new result - - return RollResult( - expression: toString(), - opType: OpType.count, - metadata: RollMetadata( - discarded: lhs.results, - ), - results: [filteredResults.length], - ndice: lhs.ndice, - nsides: lhs.nsides, - left: lhs, - right: rhs, - ); - } else { - // if counting success/failures, the results are unchanged - - return RollResult( - expression: toString(), - results: lhs.results, - opType: OpType.count, - metadata: RollMetadata( - score: RollScore.forCountType(countType, List.of(filteredResults)), - ), - ndice: lhs.ndice, - nsides: lhs.nsides, - left: lhs, - right: rhs, - ); - } - } -} - -/// drop operations -- drop high/low, or drop <,>,= rhs -class DropOp extends Binary { - DropOp(super.name, super.left, super.right); - - @override - RollResult eval() { - final lhs = left(); - final rhs = right(); - - final target = rhs.totalOrDefault(() { - throw FormatException( - 'Invalid drop operation. Missing drop target', - toString(), - toString().length, - ); - }); - - var results = []; - var dropped = []; - switch (name) { - case '-<': // drop < - results = lhs.results.where((v) => v >= target).toList(); - dropped = lhs.results.where((v) => v < target).toList(); - case '-<=': // drop <= - results = lhs.results.where((v) => v > target).toList(); - dropped = lhs.results.where((v) => v <= target).toList(); - case '->': // drop > - results = lhs.results.where((v) => v <= target).toList(); - dropped = lhs.results.where((v) => v > target).toList(); - case '->=': // drop >= - results = lhs.results.where((v) => v < target).toList(); - dropped = lhs.results.where((v) => v >= target).toList(); - case '-=': // drop = - results = lhs.results.where((v) => v != target).toList(); - dropped = lhs.results.where((v) => v == target).toList(); - default: - throw FormatException( - "unknown drop operation '$name'", - toString(), - toString().indexOf(name), - ); - } - - return RollResult( - expression: toString(), - opType: OpType.drop, - ndice: lhs.ndice, - nsides: lhs.nsides, - results: results, - metadata: RollMetadata( - discarded: dropped, - ), - left: lhs, - right: rhs, - ); - } -} - -/// drop operations -- drop high/low, or drop <,>,= rhs -class DropHighLowOp extends Binary { - DropHighLowOp(super.name, super.left, super.right); - - @override - RollResult eval() { - final lhs = left(); - final rhs = right(); - final sorted = lhs.results..sort(); - final numToDrop = rhs.totalOrDefault(() => 1); // if missing, assume '1' - var results = []; - var dropped = []; - switch (name) { - case '-h': // drop high - results = sorted.reversed.skip(numToDrop).toList(); - dropped = sorted.reversed.take(numToDrop).toList(); - case '-l': // drop low - results = sorted.skip(numToDrop).toList(); - dropped = sorted.take(numToDrop).toList(); - case 'kl': - results = sorted.take(numToDrop).toList(); - dropped = sorted.skip(numToDrop).toList(); - case 'kh': - results = sorted.reversed.take(numToDrop).toList(); - dropped = sorted.reversed.skip(numToDrop).toList(); - case 'k': - results = sorted.reversed.take(numToDrop).toList(); - dropped = sorted.reversed.skip(numToDrop).toList(); - default: - throw FormatException( - "unknown drop operation '$name'", - toString(), - toString().indexOf(name), - ); - } - return RollResult( - expression: toString(), - opType: OpType.drop, - ndice: lhs.ndice, - nsides: lhs.nsides, - results: results, - metadata: RollMetadata( - discarded: dropped, - ), - left: lhs, - right: rhs, - ); - } -} - -/// clamp results of lhs to >,< rhs. -class ClampOp extends Binary { - ClampOp(super.name, super.left, super.right); - - @override - RollResult eval() { - final lhs = left(); - final rhs = right(); - final target = rhs.totalOrDefault(() { - throw FormatException( - 'Invalid clamp operation. Missing clamp target', - toString(), - toString().length, - ); - }); - - List results; - final discarded = []; - final added = []; - switch (name) { - case 'c>': // change any value > rhs to rhs - results = lhs.results.map((v) { - if (v > target) { - discarded.add(v); - added.add(target); - return target; - } else { - return v; - } - }).toList(); - case 'c<': // change any value < rhs to rhs - results = lhs.results.map((v) { - if (v < target) { - discarded.add(v); - added.add(target); - return target; - } else { - return v; - } - }).toList(); - default: - throw FormatException( - "unknown clamp operation '$name'", - toString(), - toString().indexOf(name), - ); - } - return RollResult( - expression: toString(), - opType: OpType.clamp, - ndice: lhs.ndice, - nsides: lhs.nsides, - results: results, - metadata: RollMetadata( - discarded: discarded, - rolled: added, - ), - left: lhs, - right: rhs, - ); - } -} - -/// base class for unary dice operations -abstract class UnaryDice extends Unary { - UnaryDice(super.name, super.left, this.roller); - - final DiceRoller roller; - - @override - String toString() => '($left$name)'; -} - -/// base class for binary dice expressions -abstract class BinaryDice extends Binary { - BinaryDice(super.name, super.left, super.right, this.roller); - - final DiceRoller roller; -} - -/// roll fudge dice -class FudgeDice extends UnaryDice { - FudgeDice(super.name, super.left, super.roller); - - @override - RollResult eval() { - final lhs = left(); - final ndice = lhs.totalOrDefault(() => 1); - - // redundant w/ RangeError checks in the DiceRoller. But we can construct better error messages here. - if (ndice < DiceRoller.minDice || ndice > DiceRoller.maxDice) { - throw FormatException( - 'Invalid number of dice ($ndice)', - toString(), - left.toString().length, - ); - } - final roll = roller.rollFudge(ndice); - return RollResult.fromRollResult( - roll, - expression: toString(), - opType: roll.opType, - metadata: RollMetadata( - rolled: roll.results, - ), - left: lhs, - ); - } -} - -class CSVDice extends UnaryDice { - CSVDice(super.op, super.left, super.roller, this.vals); - - final SeparatedList vals; - - @override - String toString() => '(${left}d${vals.elements})'; - - @override - RollResult eval() { - final lhs = left(); - final ndice = lhs.totalOrDefault(() => 1); - - final roll = roller.rollVals(ndice, vals.elements.map(int.parse).toList()); - - return RollResult.fromRollResult( - roll, - expression: toString(), - opType: OpType.rollVals, - left: lhs, - ); - } -} - -/// roll n % dice -class PercentDice extends UnaryDice { - PercentDice(super.name, super.left, super.roller); - - @override - RollResult eval() { - final lhs = left(); - const nsides = 100; - final ndice = lhs.totalOrDefault(() => 1); - final roll = roller.roll(ndice, nsides); - return RollResult.fromRollResult( - roll, - expression: toString(), - opType: OpType.rollPercent, - metadata: RollMetadata( - rolled: roll.results, - ), - left: lhs, - ); - } -} - -/// roll n D66 -class D66Dice extends UnaryDice { - D66Dice(super.name, super.left, super.roller); - - @override - RollResult eval() { - final lhs = left(); - final ndice = lhs.totalOrDefault(() => 1); - final results = [ - for (var i = 0; i < ndice; i++) - roller.roll(1, 6).results.sum * 10 + roller.roll(1, 6).results.sum, - ]; - return RollResult( - expression: toString(), - opType: OpType.rollD66, - ndice: ndice, - results: results, - metadata: RollMetadata( - rolled: results, - ), - left: lhs, - ); - } -} - -/// roll N dice of Y sides. -class StdDice extends BinaryDice { - StdDice(super.name, super.left, super.right, super.roller); - - @override - String toString() => '($left$name$right)'; - - @override - RollResult eval() { - final lhs = left(); - final rhs = right(); - final ndice = lhs.totalOrDefault(() => 1); - final nsides = rhs.totalOrDefault(() => 1); - - // redundant w/ RangeError checks in the DiceRoller. But we can construct better error messages here. - if (ndice < DiceRoller.minDice || ndice > DiceRoller.maxDice) { - throw FormatException( - 'Invalid number of dice ($ndice)', - toString(), - left.toString().length, - ); - } - if (nsides < DiceRoller.minSides || nsides > DiceRoller.maxSides) { - throw FormatException( - 'Invalid number of sides ($nsides)', - toString(), - left.toString().length + name.length + 1, - ); - } - final roll = roller.roll(ndice, nsides); - return RollResult.fromRollResult( - roll, - expression: toString(), - opType: roll.opType, - metadata: RollMetadata( - rolled: roll.results, - ), - left: lhs, - right: rhs, - ); - } -} - -class RerollDice extends BinaryDice { - RerollDice( - super.name, - super.left, - super.right, - super.roller, { - this.limit = defaultRerollLimit, - }) { - if (name.startsWith('ro')) { - limit = 1; - } - } - - int limit; - - @override - RollResult eval() { - final lhs = left(); - final rhs = right(); - - if (lhs.nsides == 0) { - throw FormatException( - "Invalid reroll operation. Cannot determine # sides from '$left'", - toString(), - left.toString().length, - ); - } - final target = rhs.totalOrDefault(() { - throw FormatException( - 'Invalid reroll operation. Missing reroll target', - toString(), - toString().length, - ); - }); - final results = []; - final discarded = []; - final added = []; - - bool test(int val) { - switch (name) { - case 'r' || 'ro' || 'r=' || 'ro=': - return val == target; - case 'r<' || 'ro<': - return val < target; - case 'r>' || 'ro>': - return val > target; - case 'r<=' || 'ro<=': - return val <= target; - case 'r>=' || 'ro>=': - return val >= target; - default: - throw FormatException( - "unknown reroll operation '$name'", - toString(), - toString().indexOf(name), - ); - } - } - - lhs.results.forEachIndexed((i, v) { - if (test(v)) { - int rerolled; - var rerollCount = 0; - do { - rerolled = roller - .roll(1, lhs.nsides, '(reroll ind $i, #$rerollCount)') - .results - .sum; - rerollCount++; - } while (test(rerolled) && rerollCount < limit); - results.add(rerolled); - discarded.add(v); - added.add(rerolled); - } else { - results.add(v); - } - }); - - return RollResult( - expression: toString(), - opType: OpType.reroll, - ndice: lhs.ndice, - nsides: lhs.nsides, - results: results, - metadata: RollMetadata( - rolled: added, - discarded: discarded, - ), - left: lhs, - right: rhs, - ); - } -} - -class CompoundingDice extends BinaryDice { - CompoundingDice( - super.name, - super.left, - super.right, - super.roller, { - this.limit = defaultRerollLimit, - }) { - if (name.startsWith('!!o')) { - limit = 1; - } - } - - int limit; - - @override - RollResult eval() { - final lhs = left(); - final rhs = right(); - - if (lhs.nsides == 0) { - throw FormatException( - "Invalid compounding operation. Cannot determine # sides from '$left'", - toString(), - left.toString().length, - ); - } - final target = rhs.totalOrDefault(() => lhs.nsides); - bool test(int val) { - switch (name) { - case '!!' || '!!=' || '!!o' || '!!o=': - return val == target; - case '!!<' || '!!o<': - return val < target; - case '!!>' || '!!o>': - return val > target; - case '!!<=' || '!!o<=': - return val <= target; - case '!!>=' || '!!o>=': - return val >= target; - default: - throw FormatException( - "unknown compounding operation '$name'", - toString(), - toString().indexOf(name), - ); - } - } - - final results = []; - final discarded = []; - final added = []; - lhs.results.forEachIndexed((i, v) { - if (test(v)) { - var sum = v; - int rerolled; - var numCompounded = 0; - do { - rerolled = roller - .roll(1, lhs.nsides, '(compound ind $i, #$numCompounded)') - .results - .sum; - sum += rerolled; - numCompounded++; - } while (test(rerolled) && numCompounded < limit); - results.add(sum); - discarded.add(v); - added.add(sum); - } else { - results.add(v); - } - }); - - return RollResult( - expression: toString(), - opType: OpType.compound, - ndice: lhs.ndice, - nsides: lhs.nsides, - results: results, - metadata: RollMetadata( - rolled: added, - discarded: discarded, - ), - left: lhs, - right: rhs, - ); - } -} - -class ExplodingDice extends BinaryDice { - ExplodingDice( - super.name, - super.left, - super.right, - super.roller, { - this.limit = defaultRerollLimit, - }) { - if (name.startsWith('!o')) { - limit = 1; - } - } - - int limit; - - @override - RollResult eval() { - final lhs = left(); - final rhs = right(); - - if (lhs.nsides == 0) { - throw FormatException( - "Invalid exploding operation. Cannot determine # sides from '$left'", - toString(), - left.toString().length, - ); - } - final target = rhs.totalOrDefault(() => lhs.nsides); - - final allResults = []; - final newResults = []; - - bool test(int val) { - switch (name) { - case '!' || '!=' || '!o' || '!o=': - return val == target; - case '!<' || '!o<': - return val < target; - case '!>' || '!o>': - return val > target; - case '!<=' || '!o<=': - return val <= target; - case '!>=' || '!o>=': - return val >= target; - default: - throw FormatException( - "unknown explode operation '$name'", - toString(), - toString().indexOf(name), - ); - } - } - - allResults.addAll(lhs.results); - var numToRoll = lhs.results.where(test).length; - var explodeCount = 0; - while (numToRoll > 0 && explodeCount < limit) { - final results = roller.roll( - numToRoll, - lhs.nsides, - '(explode #${explodeCount + 1})', - ); - newResults.addAll(results.results); - numToRoll = results.results.where(test).length; - explodeCount++; - } - allResults.addAll(newResults); - - return RollResult( - expression: toString(), - opType: OpType.explode, - ndice: lhs.ndice, - nsides: lhs.nsides, - results: allResults, - metadata: RollMetadata(rolled: newResults), - left: lhs, - right: rhs, - ); - } -} diff --git a/lib/src/dice_roller.dart b/lib/src/dice_roller.dart deleted file mode 100644 index 3c975c8..0000000 --- a/lib/src/dice_roller.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'dart:math'; - -import 'results.dart'; -import 'utils.dart'; - -/// A dice roller for M dice of N sides (e.g. `2d6`). -/// A roll returns a list of ints. -class DiceRoller with LoggingMixin { - /// Constructs a dice roller - DiceRoller([Random? r]) : _random = r ?? Random.secure(); - - final Random _random; - - /// minimum dice to roll (0) - static const int minDice = 0; - - /// maximum dice to allow to be rolled (1k) - static const int maxDice = 1000; - - /// minimum sides of dice (2) - static const int minSides = 2; - - /// maximum sides of dice (100k) - static const int maxSides = 100000; - - /// default limit to # of times dice rolls can explode (100) - static const int defaultExplodeLimit = 100; - - /// Roll ndice of nsides and return results as list. - RollResult roll(int ndice, int nsides, [String msg = '']) { - RangeError.checkValueInInterval(ndice, minDice, maxDice, 'ndice'); - RangeError.checkValueInInterval(nsides, minSides, maxSides, 'nsides'); - // nextInt is zero-inclusive; add 1 so result will be in range 1-nsides - final results = [ - for (int i = 0; i < ndice; i++) _random.nextInt(nsides) + 1, - ]; - logger.finest(() => 'roll ${ndice}d$nsides => $results $msg'); - return RollResult( - expression: '${ndice}d$nsides', - opType: OpType.rollDice, - metadata: RollMetadata(rolled: results), - ndice: ndice, - nsides: nsides, - results: results, - ); - } - - static const _fudgeVals = [-1, -1, 0, 0, 1, 1]; - - /// select n items from the list of values - List selectN(int n, List vals) => [ - for (var i = 0; i < n; i++) vals[_random.nextInt(vals.length)], - ]; - - /// Roll N fudge dice, return results - RollResult rollFudge(int ndice) { - RangeError.checkValueInInterval(ndice, minDice, maxDice, 'ndice'); - final results = selectN(ndice, _fudgeVals); - - logger.finest(() => 'roll ${ndice}dF => $results'); - - return RollResult( - expression: '${ndice}dF', - opType: OpType.rollFudge, - metadata: RollMetadata(rolled: results), - ndice: ndice, - results: results, - ); - } - - /// Roll N fudge dice, return results - RollResult rollVals(int ndice, List sideVals) { - RangeError.checkValueInInterval(ndice, minDice, maxDice, 'ndice'); - final results = selectN(ndice, sideVals); - - logger.finest(() => 'roll ${ndice}d$sideVals => $results'); - - return RollResult( - expression: '${ndice}d$sideVals', - opType: OpType.rollVals, - metadata: RollMetadata(rolled: results), - ndice: ndice, - results: results, - ); - } -} diff --git a/lib/src/results.dart b/lib/src/results.dart deleted file mode 100644 index 588f94b..0000000 --- a/lib/src/results.dart +++ /dev/null @@ -1,444 +0,0 @@ -import 'dart:math'; - -import 'package:collection/collection.dart'; -import 'package:equatable/equatable.dart'; - -enum OpType { - value, // leaf nodes which are simple integer values - add, - subtract, - multiply, - count, - drop, - clamp, - rollDice, - rollFudge, - rollPercent, - rollD66, - rollVals, - reroll, - compound, - explode, -} - -enum CountType { - count, - success, - failure, - critSuccess, - critFailure; -} - -/// RollScore represents the # of successes and failures. -class RollScore extends Equatable { - const RollScore({ - this.successes = const [], - this.failures = const [], - this.critSuccesses = const [], - this.critFailures = const [], - }); - - factory RollScore.forCountType(CountType countType, List vals) { - switch (countType) { - case CountType.success: - return RollScore( - successes: vals, - ); - case CountType.failure: - return RollScore( - failures: vals, - ); - case CountType.critSuccess: - return RollScore( - critSuccesses: vals, - ); - case CountType.critFailure: - return RollScore( - critFailures: vals, - ); - case CountType.count: - throw UnimplementedError(); - } - } - - final List successes; - final List failures; - final List critSuccesses; - final List critFailures; - - int get successCount => successes.length; - - int get failureCount => failures.length; - - int get critSuccessCount => critSuccesses.length; - - int get critFailureCount => critFailures.length; - - bool get isEmpty => - successes.isEmpty && - failures.isEmpty && - critSuccesses.isEmpty && - critFailures.isEmpty; - - bool get isNotEmpty => !isEmpty; - - bool get hasSuccesses => successes.isNotEmpty; - - bool get hasFailures => failures.isNotEmpty; - - bool get hasCritSuccesses => critSuccesses.isNotEmpty; - - bool get hasCritFailures => critFailures.isNotEmpty; - - @override - List get props => [ - successes, - failures, - critSuccesses, - critFailures, - ]; - - @override - String toString() => '${toJson()}'; - - Map toJson() => { - 'successes': successes, - 'failures': failures, - 'critSuccesses': critSuccesses, - 'critFailures': critFailures, - }..removeWhere((k, v) => (v is List && v.isEmpty)); - - RollScore operator +(RollScore other) => RollScore( - successes: successes + other.successes, - failures: failures + other.failures, - critSuccesses: critSuccesses + other.critSuccesses, - critFailures: critFailures + other.critFailures, - ); -} - -/// RollMetadata represents 'interesting' things that happens during certain operations. -/// This will include the 'score' (if any), a list of which dice were rolled -/// by the operation, and a list of which dice were discarded by the operation. -/// -class RollMetadata extends Equatable { - const RollMetadata({ - this.rolled = const [], - this.discarded = const [], - this.score = const RollScore(), - }); - - final List rolled; - final List discarded; - final RollScore score; - - bool get isEmpty => rolled.isEmpty && discarded.isEmpty && score.isEmpty; - - bool get isNotEmpty => !isEmpty; - - @override - List get props => [ - rolled, - discarded, - score, - ]; - - @override - String toString() => '${toJson()}'; - - Map toJson() => { - 'rolled': rolled, - 'discarded': discarded, - 'score': score.toJson(), - }..removeWhere( - (k, v) => (v is List && v.isEmpty) || (v is Map && v.isEmpty), - ); - - RollMetadata operator +(RollMetadata other) => RollMetadata( - rolled: rolled + other.rolled, - discarded: discarded + other.discarded, - score: score + other.score, - ); -} - -/// [RollSummary] is the final result of rolling a dice expression. -/// It rolls up the metadata of sub-expressions, and includes a `detailResults` -/// if the caller wants to do something interesting to display the result graph. -/// -/// A [RollResult] is modeled as a binary tree. The dice expression -/// is parsed into an AST, and when rolled the results reflect the structure of -/// the AST. -/// -/// In general, users will only care about the root node of the tree. -/// But, depending on the information you want from the evaluated dice rolls, -/// you may need to traverse the tree to inspect all the events. - -class RollSummary extends Equatable { - RollSummary({ - required this.detailedResults, - }) { - total = detailedResults.results.sum; - results = detailedResults.results; - expression = detailedResults.expression; - metadata = rollupMetadata(detailedResults); - } - - final RollResult detailedResults; - - /// sum of [results] - late final int total; - - /// the parsed expression - late final String expression; - - /// the results of the evaluating the expression - late final List results; - late final RollMetadata metadata; - - bool get hasSuccesses => metadata.score.hasSuccesses; - - bool get hasFailures => metadata.score.hasFailures; - - bool get hasCritSuccesses => metadata.score.hasCritSuccesses; - - bool get hasCritFailures => metadata.score.hasCritFailures; - - @override - List get props => [ - total, - expression, - results, - //detailedResults, - metadata, - ]; - - @override - String toString() { - final buffer = StringBuffer( - '$expression ===> RollSummary(total: $total, results: $results', - ); - if (metadata.isNotEmpty) { - buffer.write(', metadata: $metadata'); - } - buffer.write(')'); - return buffer.toString(); - } - - Map toJson() => { - 'expression': expression, - 'total': total, - 'results': results, - 'detailedResults': detailedResults.toJson(), - 'metadata': metadata.toJson(), - }..removeWhere( - (k, v) => - v == null || - (v is Map && v.isEmpty) || - (v is List && v.isEmpty) || - (v is RollScore && v.isEmpty) || - (v is int && v == 0), - ); - - String toStringPretty() { - final buffer = StringBuffer(); - buffer - ..write(toString()) - ..write('\n') - ..write(detailedResults.toStringPretty(indent: ' ')); - - return buffer.toString(); - } -} - -/// [RollResult] represents the result of evaluating a particular node of the AST. -/// -class RollResult extends Equatable { - const RollResult({ - required this.expression, - required this.opType, - this.ndice = 0, - this.nsides = 0, - this.results = const [], - this.metadata = const RollMetadata(), - this.left, - this.right, - }); - - /// factory constructor to merge [other] with the params of this function - /// and produce a new [RollResult]. - factory RollResult.fromRollResult( - RollResult other, { - required String expression, - OpType? opType, - int? ndice, - int? nsides, - List? results, - RollMetadata metadata = const RollMetadata(), - RollResult? left, - RollResult? right, - }) => - RollResult( - expression: expression, - ndice: ndice ?? other.ndice, - nsides: nsides ?? other.nsides, - results: results ?? other.results, - opType: opType ?? other.opType, - metadata: metadata, - left: left ?? other.left, - right: right ?? other.right, - ); - - /// addition operator for [RollResult]. - /// - /// in the returned results, nsides will be max(nsides, other.nsides). - /// this is so we can explode a dice expr like `(2d6 + 1)!`. - /// NOTE: A side-effect of this decision is `(2d6 + 2d10)!` will explode with 10s, not 6s. - RollResult operator +(RollResult other) => RollResult.fromRollResult( - other, - expression: '($expression + ${other.expression})', - results: results + other.results, - nsides: max(nsides, other.nsides), - opType: OpType.add, - left: this, - right: other, - ); - - /// multiplication operator for [RollResult]. - /// - /// Results are collapsed into a single value (the result of multiplication). - /// - RollResult operator *(RollResult other) => RollResult.fromRollResult( - other, - expression: '($expression * ${other.expression})', - results: [results.sum * other.results.sum], - opType: OpType.multiply, - left: this, - right: other, - ); - - /// subtraction operator for [RollResult]. - /// - /// Results create new list lhs.results + (-1)*(other.results). - /// - RollResult operator -(RollResult other) => RollResult.fromRollResult( - other, - expression: '($expression - ${other.expression})', - opType: OpType.subtract, - results: results + other.results.map((v) => v * -1).toList(), - nsides: max(nsides, other.nsides), - left: this, - right: other, - ); - - /// the parsed expression - final String expression; - - /// number of sides. may be zero if complex expression or arithmetic result - final int nsides; - - /// number of dice rolled. may be zero if complex expression or arithmetic result - final int ndice; - - /// the results of the evaluating the expression - final List results; - - final RollMetadata metadata; - - final RollResult? left; - final RollResult? right; - - final OpType opType; - - @override - List get props => [ - expression, - opType, - nsides, - ndice, - results, - metadata, - opType, - //left, - //right, - ]; - - /// Get the total, or if results are empty return result of calling [defaultCb]. - int totalOrDefault(int Function() defaultCb) { - if (results.isEmpty) { - return defaultCb(); - } - return results.sum; - } - - @override - String toString() { - if (opType == OpType.value) { - return '$expression => RollResult(value: ${results.sum})'; - } else { - final buffer = StringBuffer(); - buffer.write( - '$expression =${opType.name}=> RollResult(total: ${results.sum}, results: $results', - ); - if (metadata.isNotEmpty) { - buffer.write(', metadata: $metadata'); - } - buffer.write(')'); - return buffer.toString(); - } - } - - String toStringPretty({String indent = ''}) => pprint(this, indent: indent); - - Map toJson() => { - 'expression': expression, - 'opType': opType.name, - 'nsides': nsides, - 'ndice': ndice, - 'results': results, - 'metadata': metadata.toJson(), - 'left': left != null && left?.opType != OpType.value - ? left?.toJson() - : null, - 'right': right != null && right?.opType != OpType.value - ? right?.toJson() - : null, - }..removeWhere( - (k, v) => - v == null || - (v is Map && v.isEmpty) || - (v is List && v.isEmpty) || - (v is int && v == 0), - ); -} - -String pprint(RollResult? rr, {String indent = ''}) { - if (rr == null) { - return ''; - } - final buffer = StringBuffer(indent); - buffer.write(rr.toString()); - if (rr.left != null && rr.left?.opType != OpType.value) { - buffer - ..write('\n') - ..write(pprint(rr.left, indent: '$indent ')); - } - if (rr.right != null && rr.right?.opType != OpType.value) { - buffer - ..write('\n') - ..write( - pprint( - rr.right, - indent: '$indent ', - ), - ); - } - - return buffer.toString(); -} - -RollMetadata rollupMetadata(RollResult? rr) { - if (rr == null || rr.opType == OpType.value) { - return const RollMetadata(); - } - - return rollupMetadata(rr.left) + rollupMetadata(rr.right) + rr.metadata; -} diff --git a/mise.toml b/mise.toml index 084fb3d..b252394 100644 --- a/mise.toml +++ b/mise.toml @@ -1,2 +1,3 @@ [tools] -dart = "3.7" +dart = "3" +flutter = "3" diff --git a/packages/dart_dice_parser/example/README.md b/packages/dart_dice_parser/example/README.md new file mode 100644 index 0000000..0c10bb4 --- /dev/null +++ b/packages/dart_dice_parser/example/README.md @@ -0,0 +1,20 @@ +# dart_dice_parser examples + +## D20 advantage + +```dart +import 'dart:io'; +import 'package:dart_dice_parser/dart_dice_parser.dart'; + + +Future main() async { + // roll 2 d20s, keep the highest. + // score the results: + // cf == a 1 is a critical failure + // cs == a 20 is a critical success + final d20adv = DiceExpression.create('2d20 kh #cs #cf'); + + final result1 = await d20adv.roll(); + stdout.writeln(result1); +} +``` diff --git a/packages/dart_dice_parser/example/simple.dart b/packages/dart_dice_parser/example/simple.dart new file mode 100644 index 0000000..c039351 --- /dev/null +++ b/packages/dart_dice_parser/example/simple.dart @@ -0,0 +1,89 @@ +import 'dart:io'; +import 'dart:math'; + +import 'package:collection/collection.dart'; +import 'package:dart_dice_parser/dart_dice_parser.dart'; +import 'package:logging/logging.dart'; + +final listEquals = const ListEquality().equals; + +/// NOTE: to run w/ asserts: dart run --enable-asserts example/simple.dart + +Future main() async { + Logger.root.level = Level.INFO; + + Logger.root.onRecord.listen((rec) { + stdout.writeln( + '[${rec.level.name.padLeft(7)}] ${rec.loggerName.padLeft(12)}: ${rec.message}', + ); + }); + + // Create a roller for `4d20 kh2 #cf #cs` (roll 4d20, keep highest 2, and track critical success/failure). + // + // The following example uses a seeded RNG so that results are the same on every run (so that the asserts below won't fail) + // + final d20adv = DiceExpression.create( + '4d20 kh2 #cf #cs', + roller: RNGRoller(Random(4321)), + ); + + // repeated rolls of the dice expression generate different results + final result1 = await d20adv.roll(); + final result2 = await d20adv.roll(); + + stdout.writeln(result1); + stdout.writeln(result2); + // outputs: + //((((4d20) kh 2) #cf ) #cs ) ===> RollSummary(total: 34, results: [17(d20), 17(d20)], discarded: [12(d20⛔︎), 11(d20⛔︎)]) + //((((4d20) kh 2) #cf ) #cs ) ===> RollSummary(total: 39, results: [20(d20✅), 19(d20)], discarded: [12(d20⛔︎), 1(d20⛔︎)], critSuccessCount: 1) + + // demonstrate navigation of the result graph + assert(result2.total == 39); + assert(listEquals(result2.results.map((d) => d.result).toList(), [20, 19])); + // read the score-related properties + assert(result2.successCount == 0); + assert(result2.failureCount == 0); + assert(result2.critFailureCount == 0); + assert(result2.critSuccessCount == 1); + assert(result2.results.where((d) => d.critSuccess).first.result == 20); + + // look at the expression tree : + // at the top level, it's a 'count' operation that counted the critical success + final top = result2.detailedResults; + assert(top.opType == OpType.count); + + // next level is the count critical failures node of the graph + // NOTE: despite there being a 1 rolled, the criticalFailure expression is _after_ the `1` is discarded by the lower expression + final critFailureResult = top.left; + assert(critFailureResult!.opType == OpType.count); + assert(critFailureResult!.critFailureCount == 0); + + final dropResult = critFailureResult!.left; + assert(dropResult!.opType == OpType.drop); + + assert(listEquals(result2.discarded.map((d) => d.result).toList(), [12, 1])); + + assert( + listEquals(dropResult!.discarded.map((d) => d.result).toList(), [12, 1]), + ); + + final rollResult = dropResult!.left; + assert(rollResult!.opType == OpType.rollDice); + + assert( + listEquals(rollResult!.results.map((d) => d.result).toList(), [ + 20, + 19, + 1, + 12, + ]), + ); + + final stats = await DiceExpression.create( + '2d6', + roller: RNGRoller(Random(1234)), + ).stats(); + // output: + // {mean: 6.99, stddev: 2.4, min: 2, max: 12, count: 1000, histogram: {2: 27, 3: 56, 4: 90, 5: 98, 6: 138, 7: 180, 8: 141, 9: 109, 10: 80, 11: 51, 12: 30}} + stdout.writeln(stats); +} diff --git a/lib/dart_dice_parser.dart b/packages/dart_dice_parser/lib/dart_dice_parser.dart similarity index 64% rename from lib/dart_dice_parser.dart rename to packages/dart_dice_parser/lib/dart_dice_parser.dart index 59a16b8..91e669a 100644 --- a/lib/dart_dice_parser.dart +++ b/packages/dart_dice_parser/lib/dart_dice_parser.dart @@ -7,7 +7,7 @@ /// const input = '2d20-L'; // D20 advantage -- roll 2d20, drop lowest /// final diceExpr = DiceExpression.create(input); /// for (var i = 0; i < 2; i++) { -/// final int result = diceExpr.roll(); +/// final int result = await diceExpr.roll(); /// stdout.writeln("$i : $result"); /// } /// ``` @@ -15,4 +15,8 @@ library; export 'src/dice_expression.dart'; export 'src/dice_roller.dart'; -export 'src/results.dart'; +export 'src/enums.dart'; +export 'src/extensions.dart'; +export 'src/roll_result.dart'; +export 'src/roll_summary.dart'; +export 'src/rolled_die.dart'; diff --git a/packages/dart_dice_parser/lib/src/ast_core.dart b/packages/dart_dice_parser/lib/src/ast_core.dart new file mode 100644 index 0000000..61cbce6 --- /dev/null +++ b/packages/dart_dice_parser/lib/src/ast_core.dart @@ -0,0 +1,190 @@ +import 'dice_expression.dart'; +import 'dice_roller.dart'; +import 'enums.dart'; +import 'extensions.dart'; +import 'roll_result.dart'; +import 'rolled_die.dart'; +import 'utils.dart'; + +/// All our operations will inherit from this class. +/// The `call()` method will be called by the parent node. +/// The `eval()` method is called from the node +abstract class DiceOp extends DiceExpression with LoggingMixin { + // each child class should override this to implement their operation + Future eval(); + + // all children can share this call operator -- and it'll let us be consistent w/ regard to logging + @override + Future call() async { + final result = await eval(); + logger.finer(() => '$result'); + return result; + } +} + +/// base class for unary operations +abstract class Unary extends DiceOp { + Unary(this.name, this.left); + + final String name; + final DiceExpression left; + + @override + String toString() => '($left)$name'; +} + +/// base class for binary operations +abstract class Binary extends DiceOp { + Binary(this.name, this.left, this.right); + + final String name; + final DiceExpression left; + final DiceExpression right; + + @override + String toString() => '($left $name $right)'; +} + +class CommaOp extends Binary { + CommaOp(super.name, super.left, super.right); + + @override + Future eval() async { + final lhs = await left(); + final rhs = await right(); + + final results = []; + final discarded = []; + + discarded.addAll(lhs.discarded); + discarded.addAll(rhs.discarded); + + if (lhs.opType == OpType.comma) { + results.addAll(lhs.results); + } else { + results.add( + RolledDie.singleVal( + result: lhs.results.sum, + from: lhs.results, + totaled: true, + ), + ); + discarded.addAll(lhs.results.map(RolledDie.discard)); + } + if (rhs.opType == OpType.comma) { + results.addAll(rhs.results); + } else { + results.add( + RolledDie.singleVal( + result: rhs.results.sum, + from: rhs.results, + totaled: true, + ), + ); + discarded.addAll(rhs.results.map(RolledDie.discard)); + } + + return RollResult( + expression: toString(), + opType: OpType.comma, + results: results, + discarded: discarded, + left: lhs, + right: rhs, + ); + } +} + +/// multiply operation (flattens results) +class MultiplyOp extends Binary { + MultiplyOp(super.name, super.left, super.right); + + @override + Future eval() async => await left() * await right(); +} + +/// add operation +class AddOp extends Binary { + AddOp(super.name, super.left, super.right); + + @override + Future eval() async => await left() + await right(); +} + +/// subtraction operation +class SubOp extends Binary { + SubOp(super.name, super.left, super.right); + + @override + Future eval() async => await left() - await right(); +} + +/// base class for unary dice operations +abstract class UnaryDice extends Unary { + UnaryDice(super.name, super.left, this.roller); + + final DiceResultRoller roller; + + @override + String toString() => '($left$name)'; +} + +/// base class for binary dice expressions +abstract class BinaryDice extends Binary { + BinaryDice(super.name, super.left, super.right, this.roller); + + final DiceResultRoller roller; +} + +/// A value expression. The token we read from input will be a String, +/// it must parse as an int, and an empty string will return empty set. +class SimpleValue extends DiceExpression { + SimpleValue(this.value) + : _results = RollResult( + expression: value, + opType: OpType.value, + results: value.isEmpty + ? [] + : [RolledDie.singleVal(result: int.parse(value))], + ); + + final String value; + final RollResult _results; + + @override + Future call() async => _results; + + @override + String toString() => value; +} + +class AggregateOp extends DiceOp { + AggregateOp(this.subexpression); + + final DiceExpression subexpression; + + @override + String toString() => '{$subexpression}'; + + @override + Future eval() async { + final outcome = await subexpression(); + + return RollResult( + expression: toString(), + opType: OpType.total, + results: [ + RolledDie.singleVal( + result: outcome.results.sum, + from: outcome.results, + totaled: true, + ), + ], + discarded: [ + ...outcome.discarded, + ...outcome.results.map(RolledDie.discard), + ], + left: outcome, + ); + } +} diff --git a/packages/dart_dice_parser/lib/src/ast_dice.dart b/packages/dart_dice_parser/lib/src/ast_dice.dart new file mode 100644 index 0000000..c31e5dc --- /dev/null +++ b/packages/dart_dice_parser/lib/src/ast_dice.dart @@ -0,0 +1,216 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:petitparser/parser.dart'; + +import 'ast_core.dart'; +import 'dice_roller.dart'; +import 'enums.dart'; +import 'roll_result.dart'; +import 'rolled_die.dart'; + +/// roll fudge dice +class FudgeDice extends UnaryDice { + FudgeDice(super.name, super.left, super.roller); + + @override + Future eval() async { + final lhs = await left(); + final ndice = lhs.totalOrDefault(() => 1); + + // redundant w/ RangeError checks in the DiceRoller. But we can construct better error messages here. + if (ndice < DiceRoller.minDice || ndice > DiceRoller.maxDice) { + throw FormatException( + 'Invalid number of dice ($ndice)', + toString(), + left.toString().length, + ); + } + final roll = await roller.rollFudge(ndice); + return RollResult.fromRollResult( + roll, + expression: toString(), + opType: roll.opType, + left: lhs, + ); + } +} + +class CSVDice extends UnaryDice { + CSVDice(super.op, super.left, super.roller, this.vals); + + final SeparatedList vals; + + @override + String toString() => '(${left}d${vals.elements})'; + + @override + Future eval() async { + final lhs = await left(); + final ndice = lhs.totalOrDefault(() => 1); + + final roll = await roller.rollVals( + ndice, + IList(vals.elements.map(int.parse)), + ); + + return RollResult.fromRollResult( + roll, + expression: toString(), + opType: OpType.rollVals, + left: lhs, + ); + } +} + +class PenetratingDice extends UnaryDice { + PenetratingDice( + super.op, + super.left, + super.roller, { + required String nsides, + required String nsidesPenetration, + }) : nsides = int.parse(nsides), + nsidesPenetration = nsidesPenetration.isEmpty + ? int.parse(nsides) + : int.parse(nsidesPenetration); + + final int nsides; + final int nsidesPenetration; + final limit = DiceRoller.defaultRerollLimit; + + @override + String toString() => '(${left}d${nsides}p$nsidesPenetration)'; + + @override + Future eval() async { + final lhs = await left(); + final ndice = lhs.totalOrDefault(() => 1); + + final roll = await roller.roll(ndice, nsides); + + final results = []; + final discarded = []; + for (final (index, rolledDie) in roll.results.indexed) { + if (rolledDie.isMaxResult) { + var sum = rolledDie.result; + RolledDie rerolled; + var numPenetrated = 0; + discarded.add( + RolledDie.copyWith(rolledDie, discarded: true, penetrator: true), + ); + do { + rerolled = (await roller.roll( + 1, + nsidesPenetration, + '(penetration ind[$index] #${numPenetrated + 1})', + )).results.first; + discarded.add( + RolledDie.copyWith(rerolled, discarded: true, penetrator: true), + ); + sum += rerolled.result; + numPenetrated++; + } while (rerolled.isMaxResult && numPenetrated < limit); + discarded.add( + RolledDie.singleVal( + result: -numPenetrated, + discarded: true, + penetrator: true, + ), + ); + results.add( + RolledDie.copyWith( + rolledDie, + result: sum - numPenetrated, + penetrated: true, + from: discarded, + ), + ); + } else { + results.add(rolledDie); + } + } + + return RollResult( + expression: toString(), + opType: OpType.rollPenetration, + results: results, + discarded: lhs.discarded + discarded, + left: lhs, + ); + } +} + +/// roll n % dice +class PercentDice extends UnaryDice { + PercentDice(super.name, super.left, super.roller); + + @override + Future eval() async { + final lhs = await left(); + final ndice = lhs.totalOrDefault(() => 1); + final roll = await roller.roll(ndice, 100); + return RollResult.fromRollResult( + roll, + expression: toString(), + opType: OpType.rollPercent, + left: lhs, + ); + } +} + +/// roll n D66 +class D66Dice extends UnaryDice { + D66Dice(super.name, super.left, super.roller); + + @override + Future eval() async { + final lhs = await left(); + final ndice = lhs.totalOrDefault(() => 1); + final roll = await roller.rollD66(ndice); + return RollResult.fromRollResult( + roll, + expression: toString(), + opType: OpType.rollD66, + left: lhs, + ); + } +} + +/// roll N dice of Y sides. +class StdDice extends BinaryDice { + StdDice(super.name, super.left, super.right, super.roller); + + @override + String toString() => '($left$name$right)'; + + @override + Future eval() async { + final lhs = await left(); + final rhs = await right(); + final ndice = lhs.totalOrDefault(() => 1); + final nsides = rhs.totalOrDefault(() => 1); + + // redundant w/ RangeError checks in the DiceRoller. But we can construct better error messages here. + if (ndice < DiceRoller.minDice || ndice > DiceRoller.maxDice) { + throw FormatException( + 'Invalid number of dice ($ndice)', + toString(), + left.toString().length, + ); + } + if (nsides < DiceRoller.minSides || nsides > DiceRoller.maxSides) { + throw FormatException( + 'Invalid number of sides ($nsides)', + toString(), + left.toString().length + name.length + 1, + ); + } + final roll = await roller.roll(ndice, nsides); + return RollResult.fromRollResult( + roll, + expression: toString(), + opType: roll.opType, + left: lhs, + right: rhs, + ); + } +} diff --git a/packages/dart_dice_parser/lib/src/ast_ops.dart b/packages/dart_dice_parser/lib/src/ast_ops.dart new file mode 100644 index 0000000..14939d9 --- /dev/null +++ b/packages/dart_dice_parser/lib/src/ast_ops.dart @@ -0,0 +1,540 @@ +import 'ast_core.dart'; +import 'dice_roller.dart'; +import 'enums.dart'; +import 'roll_result.dart'; +import 'rolled_die.dart'; + +class SortOp extends Unary { + SortOp(super.name, super.left); + + @override + Future eval() async { + final lhs = await left(); + final reversed = name == 'sd'; + + return RollResult( + results: reversed ? lhs.results.sortReversed() : lhs.results.sort(), + discarded: reversed ? lhs.discarded.sortReversed() : lhs.discarded.sort(), + opType: OpType.sort, + + expression: toString(), + left: lhs, + ); + } +} + +/// variation on count -- count how many results from lhs are =,<,> rhs. +class CountOp extends Binary { + CountOp( + super.name, + super.left, + super.right, [ + this.countType = CountType.count, + ]) { + if (name.startsWith('#s')) { + countType = CountType.success; + } else if (name.startsWith('#f')) { + countType = CountType.failure; + } else if (name.startsWith('#cs')) { + countType = CountType.critSuccess; + } else if (name.startsWith('#cf')) { + countType = CountType.critFailure; + } else { + countType = CountType.count; + } + } + + CountType countType; + + @override + Future eval() async { + final lhs = await left(); + final rhs = await right(); + + bool shouldCount(RolledDie rolledDie) { + var rhsEmptyAndSimpleCount = false; + var calculatedDefault = false; + final target = rhs.totalOrDefault(() { + calculatedDefault = true; + // if missing RHS, we can make assumptions depending on operator and the dietype + switch (name) { + case '#': + // example: '3d6#' should be 3. target is ignored in case statement below. + rhsEmptyAndSimpleCount = true; + return 0; + case '#s' || '#cs': + // example: '3d6#s' should match 6, or '3D66' should match 66 + return rolledDie.maxPotentialValue; + case '#f' || '#cf': + // generally should be 1 or whatever the minimum potential val is + return rolledDie.minPotentialValue; + default: + throw FormatException( + 'Invalid count operation. Missing count target', + toString(), + toString().length, + ); + } + }); + final v = rolledDie.result; + switch (name) { + case '#>=' || '#s>=' || '#f>=' || '#cs>=' || '#cf>=': + // how many results on lhs are greater than or equal to rhs? + return v >= target; + case '#<=' || '#s<=' || '#f<=' || '#cs<=' || '#cf<=': + // how many results on lhs are less than or equal to rhs? + return v <= target; + case '#>' || '#s>' || '#f>' || '#cs>' || '#cf>': + // how many results on lhs are greater than rhs? + return v > target; + case '#<' || '#s<' || '#f<' || '#cs<' || '#cf<': + // how many results on lhs are less than rhs? + return v < target; + case '#=' || '#s=' || '#f=' || '#cs=' || '#cf=': + // how many results on lhs are equal to rhs? + return v == target; + case '#' || '#s' || '#f' || '#cs' || '#cf': + if (rhsEmptyAndSimpleCount) { + // if missing rhs, we're just counting results + // that is, '3d6#' should return 3 + return true; + } else { + // don't allow a singleVal/nvals(with 1 element) be counted as a success just because it's the min or max. + if (calculatedDefault && + rolledDie.dieType.requirePotentialValues && + rolledDie.potentialValues.length == 1) { + return false; + } + // if not missing rhs, treat it as equivalent to '#='. + // that is, '3d6#2' should count 2s + return v == target; + } + default: + throw FormatException( + "unknown count operation '$name'", + toString(), + toString().indexOf(name), + ); + } + } + + final scoredResults = lhs.results.where(shouldCount); + + if (countType == CountType.count) { + // if counting, the count becomes the new result + + return RollResult( + expression: toString(), + opType: OpType.count, + results: [ + RolledDie.singleVal(result: scoredResults.length, from: lhs.results), + ], + discarded: [...lhs.results.map(RolledDie.discard), ...lhs.discarded], + left: lhs, + right: rhs, + ); + } else { + // if counting success/failures, the results are updated w/ scoring + + final nonScoredResults = lhs.results.whereNot(shouldCount); + + return RollResult( + expression: toString(), + opType: OpType.count, + results: [ + ...scoredResults.map( + (v) => RolledDie.scoreForCountType(v, countType: countType), + ), + ...nonScoredResults, + ], + discarded: lhs.discarded, + left: lhs, + right: rhs, + ); + } + } +} + +/// drop operations -- drop high/low, or drop <,>,= rhs +class DropOp extends Binary { + DropOp(super.name, super.left, super.right); + + @override + Future eval() async { + final lhs = await left(); + final rhs = await right(); + + final target = rhs.totalOrDefault(() { + throw FormatException( + 'Invalid drop operation. Missing drop target', + toString(), + toString().length, + ); + }); + + final Iterable results; + final Iterable dropped; + switch (name) { + case '-<': // drop < + results = lhs.results.where((v) => v.result >= target); + dropped = lhs.results.where((v) => v.result < target); + case '-<=': // drop <= + results = lhs.results.where((v) => v.result > target); + dropped = lhs.results.where((v) => v.result <= target); + case '->': // drop > + results = lhs.results.where((v) => v.result <= target); + dropped = lhs.results.where((v) => v.result > target); + case '->=': // drop >= + results = lhs.results.where((v) => v.result < target); + dropped = lhs.results.where((v) => v.result >= target); + case '-=': // drop = + results = lhs.results.where((v) => v.result != target); + dropped = lhs.results.where((v) => v.result == target); + default: + throw FormatException( + "unknown drop operation '$name'", + toString(), + toString().indexOf(name), + ); + } + + return RollResult( + expression: toString(), + opType: OpType.drop, + results: [...results], + discarded: [...dropped.map(RolledDie.discard), ...lhs.discarded], + left: lhs, + right: rhs, + ); + } +} + +/// drop operations -- drop high/low, or drop <,>,= rhs +class DropHighLowOp extends Binary { + DropHighLowOp(super.name, super.left, super.right); + + @override + Future eval() async { + final lhs = await left(); + final rhs = await right(); + final sorted = lhs.results.toList()..sort(); + final numToDrop = rhs.totalOrDefault(() => 1); // if missing, assume '1' + final Iterable results; + final Iterable dropped; + switch (name) { + case '-h': // drop high + results = sorted.reversed.skip(numToDrop); + dropped = sorted.reversed.take(numToDrop); + case '-l': // drop low + results = sorted.skip(numToDrop); + dropped = sorted.take(numToDrop); + case 'kl': + results = sorted.take(numToDrop); + dropped = sorted.skip(numToDrop); + case 'kh': + results = sorted.reversed.take(numToDrop); + dropped = sorted.reversed.skip(numToDrop); + case 'k': + results = sorted.reversed.take(numToDrop); + dropped = sorted.reversed.skip(numToDrop); + default: + throw FormatException( + "unknown drop operation '$name'", + toString(), + toString().indexOf(name), + ); + } + return RollResult( + expression: toString(), + opType: OpType.drop, + results: [...results], + discarded: [...dropped.map(RolledDie.discard), ...lhs.discarded], + left: lhs, + right: rhs, + ); + } +} + +/// clamp results of lhs to >,< rhs. +class ClampOp extends Binary { + ClampOp(super.name, super.left, super.right); + + @override + Future eval() async { + final lhs = await left(); + final rhs = await right(); + final target = rhs.totalOrDefault(() { + throw FormatException( + 'Invalid clamp operation. Missing clamp target', + toString(), + toString().length, + ); + }); + + final newResults = []; + final discarded = []; + for (final d in lhs.results) { + // TODO: add clamped flag? + if (name == 'c>' && d.result > target) { + discarded.add(RolledDie.copyWith(d, discarded: true, clampHigh: true)); + newResults.add(RolledDie.copyWith(d, result: target, clampHigh: true)); + } else if (name == 'c<' && d.result < target) { + discarded.add(RolledDie.copyWith(d, discarded: true, clampLow: true)); + newResults.add(RolledDie.copyWith(d, result: target, clampLow: true)); + } else { + newResults.add(d); + } + } + return RollResult( + expression: toString(), + opType: OpType.clamp, + results: newResults, + discarded: lhs.discarded + discarded, + left: lhs, + right: rhs, + ); + } +} + +class RerollDice extends BinaryDice { + RerollDice( + super.name, + super.left, + super.right, + super.roller, { + this.limit = DiceRoller.defaultRerollLimit, + }) { + if (name.startsWith('ro')) { + limit = 1; + } + } + + int limit; + + @override + Future eval() async { + final lhs = await left(); + final rhs = await right(); + + final target = rhs.totalOrDefault(() { + throw FormatException( + 'Invalid reroll operation. Missing reroll target', + toString(), + toString().length, + ); + }); + + bool shouldReroll(RolledDie rolledDie) { + final val = rolledDie.result; + switch (name) { + case 'r' || 'ro' || 'r=' || 'ro=': + return val == target; + case 'r<' || 'ro<': + return val < target; + case 'r>' || 'ro>': + return val > target; + case 'r<=' || 'ro<=': + return val <= target; + case 'r>=' || 'ro>=': + return val >= target; + default: + throw FormatException( + "unknown reroll operation '$name'", + toString(), + toString().indexOf(name), + ); + } + } + + final results = []; + final discarded = []; + for (final v in lhs.results) { + if (shouldReroll(v)) { + RolledDie rerolled; + var rerollCount = 0; + do { + rerolled = (await roller.reroll( + v, + '(reroll #$rerollCount)', + )).results.first; + rerollCount++; + } while (shouldReroll(rerolled) && rerollCount < limit); + discarded.add(RolledDie.copyWith(v, discarded: true, rerolled: true)); + results.add( + RolledDie.copyWith(v, result: rerolled.result, reroll: true), + ); + } else { + results.add(v); + } + } + + return RollResult( + expression: toString(), + opType: OpType.reroll, + results: results, + discarded: lhs.discarded + discarded, + left: lhs, + right: rhs, + ); + } +} + +class CompoundingDice extends BinaryDice { + CompoundingDice( + super.name, + super.left, + super.right, + super.roller, { + this.limit = DiceRoller.defaultRerollLimit, + }) { + if (name.startsWith('!!o')) { + limit = 1; + } + } + + int limit; + + @override + Future eval() async { + final lhs = await left(); + final rhs = await right(); + + bool shouldCompound(RolledDie rolledDie) { + final val = rolledDie.result; + if (!rolledDie.dieType.explodable) { + logger.finest('$rolledDie cannot compound due to dieType'); + return false; + } + final target = rhs.totalOrDefault(() => rolledDie.maxPotentialValue); + switch (name) { + case '!!' || '!!=' || '!!o' || '!!o=': + return val == target; + case '!!<' || '!!o<': + return val < target; + case '!!>' || '!!o>': + return val > target; + case '!!<=' || '!!o<=': + return val <= target; + case '!!>=' || '!!o>=': + return val >= target; + default: + throw FormatException( + "unknown compounding operation '$name'", + toString(), + toString().indexOf(name), + ); + } + } + + final results = []; + final discarded = []; + for (final (index, rolledDie) in lhs.results.indexed) { + if (shouldCompound(rolledDie)) { + var sum = rolledDie.result; + RolledDie rerolled; + var numCompounded = 0; + discarded.add( + RolledDie.copyWith(rolledDie, discarded: true, compounded: true), + ); + do { + rerolled = (await roller.reroll( + rolledDie, + '(compound ind[$index] #$numCompounded)', + )).results.first; + discarded.add( + RolledDie.copyWith(rerolled, discarded: true, compounded: true), + ); + sum += rerolled.result; + numCompounded++; + } while (shouldCompound(rerolled) && numCompounded < limit); + results.add( + RolledDie.copyWith(rolledDie, result: sum, compoundedFinal: true), + ); + } else { + results.add(rolledDie); + } + } + + return RollResult( + expression: toString(), + opType: OpType.compound, + results: results, + discarded: lhs.discarded + discarded, + left: lhs, + right: rhs, + ); + } +} + +class ExplodingDice extends BinaryDice { + ExplodingDice( + super.name, + super.left, + super.right, + super.roller, { + this.limit = DiceRoller.defaultRerollLimit, + }) { + if (name.startsWith('!o')) { + limit = 1; + } + } + + int limit; + + @override + Future eval() async { + final lhs = await left(); + final rhs = await right(); + + bool shouldExplode(RolledDie rolledDie) { + final val = rolledDie.result; + if (!rolledDie.dieType.explodable) { + logger.finest('$rolledDie cannot compound due to dieType'); + return false; + } + final target = rhs.totalOrDefault(() => rolledDie.maxPotentialValue); + switch (name) { + case '!' || '!=' || '!o' || '!o=': + return val == target; + case '!<' || '!o<': + return val < target; + case '!>' || '!o>': + return val > target; + case '!<=' || '!o<=': + return val <= target; + case '!>=' || '!o>=': + return val >= target; + default: + throw FormatException( + "unknown explode operation '$name'", + toString(), + toString().indexOf(name), + ); + } + } + + final newResults = []; + for (final rolledDie in lhs.results.where(shouldExplode)) { + newResults.add(RolledDie.copyWith(rolledDie, exploded: true)); + var numExplosions = 0; + RolledDie rerolledDie; + do { + rerolledDie = (await roller.reroll( + rolledDie, + '(explode #${numExplosions + 1})', + )).results.first; + numExplosions++; + newResults.add(RolledDie.copyWith(rerolledDie, explosion: true)); + } while (shouldExplode(rerolledDie) && numExplosions < limit); + } + + return RollResult( + expression: toString(), + opType: OpType.explode, + results: [...newResults, ...lhs.results.whereNot(shouldExplode)], + discarded: lhs.discarded, + left: lhs, + right: rhs, + ); + } +} diff --git a/lib/src/dice_expression.dart b/packages/dart_dice_parser/lib/src/dice_expression.dart similarity index 84% rename from lib/src/dice_expression.dart rename to packages/dart_dice_parser/lib/src/dice_expression.dart index b5c4ebc..1871d4c 100644 --- a/lib/src/dice_expression.dart +++ b/packages/dart_dice_parser/lib/src/dice_expression.dart @@ -1,11 +1,11 @@ -import 'dart:math'; - import 'package:logging/logging.dart'; import 'package:petitparser/petitparser.dart'; import 'dice_roller.dart'; +import 'enums.dart'; import 'parser.dart'; -import 'results.dart'; +import 'roll_result.dart'; +import 'roll_summary.dart'; import 'stats.dart'; /// An abstract expression that can be evaluated. @@ -14,9 +14,7 @@ abstract class DiceExpression { static List listeners = [defaultListener]; static List summaryListeners = []; - static void registerListener( - Function(RollResult rollResult) callback, - ) { + static void registerListener(Function(RollResult rollResult) callback) { listeners.add(callback); } @@ -58,11 +56,8 @@ abstract class DiceExpression { /// Parse the given input into a DiceExpression /// /// Throws [FormatException] if invalid - static DiceExpression create( - String input, [ - Random? random, - ]) { - final builder = parserBuilder(DiceRoller(random)); + static DiceExpression create(String input, {DiceRoller? roller}) { + final builder = parserBuilder(DiceResultRoller(roller)); final result = builder.parse(input); if (result is Failure) { throw FormatException( @@ -75,16 +70,16 @@ abstract class DiceExpression { } /// each DiceExpression operation is callable (when we call the parsed string, this is the method that'll be used) - RollResult call(); + Future call(); /// Rolls the dice expression /// /// Throws [FormatException] - RollSummary roll({ + Future roll({ Function(RollResult rollResult) onRoll = noopListener, Function(RollSummary rollSummary) onSummary = noopSummaryListener, - }) { - final rollResult = this(); + }) async { + final rollResult = await this(); callListeners(rollResult, onRoll: onRoll); @@ -101,16 +96,14 @@ abstract class DiceExpression { /// Throws [FormatException] Stream rollN(int num) async* { for (var i = 0; i < num; i++) { - yield roll(); + yield await roll(); } } /// Performs [num] rolls and outputs stats (stddev, mean, min/max, and a histogram) /// /// Throws [FormatException] - Future> stats({ - int num = 1000, - }) async { + Future> stats({int num = 1000}) async { final stats = StatsCollector(); await for (final r in rollN(num)) { diff --git a/packages/dart_dice_parser/lib/src/dice_roller.dart b/packages/dart_dice_parser/lib/src/dice_roller.dart new file mode 100644 index 0000000..e946881 --- /dev/null +++ b/packages/dart_dice_parser/lib/src/dice_roller.dart @@ -0,0 +1,228 @@ +import 'dart:math'; + +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; + +import 'enums.dart'; +import 'roll_result.dart'; +import 'rolled_die.dart'; +import 'utils.dart'; + +abstract class DiceRoller { + /// minimum dice to roll (0) + static const minDice = 0; + + /// maximum dice to allow to be rolled (1k) + static const maxDice = 1000; + + /// minimum sides of dice (2) + static const minSides = 2; + + /// maximum sides of dice (100k) + static const maxSides = 100000; + + /// default limit for rerolls/exploding/compounding to avoid getting stuck in loop + static const defaultRerollLimit = 1000; + + static const defaultFudgeVals = [-1, -1, 0, 0, 1, 1]; + + /// return an Stream of ints. length == ndice, range: [min,nsides] + /// duplicates allowed. + Stream roll({ + required int ndice, + required int nsides, + int min = 1, + DieType dieType = DieType.polyhedral, + }); + + /// return an Stream of results selected from the given vals. length == ndice. + /// results should be selected at random from vals. + /// duplicates are allowed. + Stream rollVals( + int ndice, + List vals, { + DieType dieType = DieType.polyhedral, + }); +} + +/// a dice roller that uses an RNG +class RNGRoller extends DiceRoller { + RNGRoller([Random? random]) : _random = random ?? Random.secure(); + + final Random _random; + + /// select ndice random items from the list of values. duplicates are possible + Iterable selectNFromVals(int ndice, List vals) => [ + for (var i = 0; i < ndice; i++) vals[_random.nextInt(vals.length)], + ]; + + /// return an iterable of ndice random integer values in range [min,nsides] + Iterable selectN({ + required int ndice, + required int nsides, + int min = 1, + }) => [for (int i = 0; i < ndice; i++) _random.nextInt(nsides) + min]; + + @override + Stream roll({ + required int ndice, + required int nsides, + int min = 1, + DieType dieType = DieType.polyhedral, + }) async* { + RangeError.checkValueInInterval( + ndice, + DiceRoller.minDice, + DiceRoller.maxDice, + 'ndice', + ); + RangeError.checkValueInInterval( + nsides, + DiceRoller.minSides, + DiceRoller.maxSides, + 'nsides', + ); + for (final i in selectN(ndice: ndice, nsides: nsides, min: min)) { + yield i; + } + } + + @override + Stream rollVals( + int ndice, + List vals, { + DieType dieType = DieType.polyhedral, + }) async* { + RangeError.checkValueInInterval( + ndice, + DiceRoller.minDice, + DiceRoller.maxDice, + 'ndice', + ); + for (final i in selectNFromVals(ndice, vals)) { + yield i; + } + } +} + +/// A dice roller for standard polyhedral dice, fudge dice, etc. +class DiceResultRoller with LoggingMixin { + /// Constructs a dice roller + DiceResultRoller([DiceRoller? r]) + : _diceRoller = r ?? RNGRoller(Random.secure()); + + final DiceRoller _diceRoller; + + Future reroll(RolledDie rolledDie, [String msg = '']) async { + switch (rolledDie.dieType) { + case DieType.polyhedral: + return roll(1, rolledDie.nsides, msg); + case DieType.fudge: + return rollFudge(1, msg); + case DieType.d66: + return rollD66(1, msg); + case DieType.nvals: + return rollVals(1, rolledDie.potentialValues, msg); + default: + return RollResult( + expression: rolledDie.result.toString(), + opType: OpType.value, + results: [RolledDie.singleVal(result: rolledDie.result)], + ); + } + } + + Future rollD66(int ndice, [String msg = '']) async { + final results = []; + final discarded = []; + for (var i = 0; i < ndice; i++) { + final digits = await _diceRoller + .roll(ndice: 2, nsides: 6, dieType: DieType.d66) + .toList(); + logger.finest(() => 'roll ${ndice}D66 => $digits $msg'); + final tens = digits[0]; + final ones = digits[1]; + final total = tens * 10 + ones; + final rolled = [ + ...digits.map( + (i) => RolledDie.polyhedral(result: i, nsides: 6, discarded: true), + ), + ]; + discarded.addAll(rolled); + results.add(RolledDie.d66(result: total, from: rolled)); + } + logger.finest( + () => 'roll ${ndice}D66 => $results {discarded: $discarded} $msg', + ); + return RollResult( + expression: toString(), + opType: OpType.rollD66, + results: results, + discarded: discarded, + ); + } + + /// Roll ndice of nsides and return results + Future roll(int ndice, int nsides, [String msg = '']) async { + // nextInt is zero-inclusive; add 1 so result will be in range 1-nsides + final results = await _diceRoller + .roll(ndice: ndice, nsides: nsides) + .toList(); + logger.finest(() => 'roll ${ndice}d$nsides => $results $msg'); + return RollResult( + expression: '${ndice}d$nsides', + opType: OpType.rollDice, + results: [ + ...results.map((i) => RolledDie.polyhedral(result: i, nsides: nsides)), + ], + ); + } + + /// Roll N fudge dice, return results + Future rollFudge(int ndice, [String msg = '']) async { + final results = await _diceRoller + .rollVals(ndice, DiceRoller.defaultFudgeVals, dieType: DieType.fudge) + .toList(); + + logger.finest(() => 'roll ${ndice}dF => $results $msg'); + + return RollResult( + expression: '${ndice}dF', + opType: OpType.rollFudge, + results: [...results.map((i) => RolledDie.fudge(result: i))], + ); + } + + /// Roll N fudge dice, return results + Future rollVals( + int ndice, + IList sideVals, [ + String msg = '', + ]) async { + final results = await _diceRoller + .rollVals( + ndice, + sideVals.toList(growable: false), + dieType: DieType.nvals, + ) + .toList(); + + logger.finest( + () => 'roll ${ndice}d${sideVals.toString(false)} => $results $msg', + ); + + return RollResult( + expression: '${ndice}d$sideVals', + opType: OpType.rollVals, + results: [ + ...results.map( + (i) => RolledDie( + result: i, + nsides: sideVals.length, + dieType: DieType.nvals, + potentialValues: sideVals, + ), + ), + ], + ); + } +} diff --git a/packages/dart_dice_parser/lib/src/enums.dart b/packages/dart_dice_parser/lib/src/enums.dart new file mode 100644 index 0000000..690a771 --- /dev/null +++ b/packages/dart_dice_parser/lib/src/enums.dart @@ -0,0 +1,55 @@ +/// types of die. +enum DieType implements Comparable { + // normal polyhedral (1d6, 1d20, etc) + polyhedral(), + // fudge dice + fudge(requirePotentialValues: true), + // 1D66 (equivalent to `1d6*10 + 1d6`). + d66(requireNSides: false), + // 1d[1,3,5,7,9] + nvals(requirePotentialValues: true), + // single value (e.g. a sum or count of dice) + singleVal(explodable: false, requirePotentialValues: true); + + const DieType({ + this.explodable = true, + this.requirePotentialValues = false, + this.requireNSides = true, + }); + + /// can the die be exploded? + final bool explodable; + + /// whether the RolledDie must have non-empty potentialValues + final bool requirePotentialValues; + + /// whether the RolledDie must have non-zero nsides + final bool requireNSides; + + @override + int compareTo(DieType dieType) => index.compareTo(dieType.index); +} + +enum OpType { + value, // leaf nodes which are simple integer values + add, + subtract, + multiply, + count, + drop, + clamp, + rollDice, + rollFudge, + rollPercent, + rollD66, + rollVals, + rollPenetration, + reroll, + compound, + explode, + sort, + comma, + total, +} + +enum CountType { count, success, failure, critSuccess, critFailure } diff --git a/packages/dart_dice_parser/lib/src/extensions.dart b/packages/dart_dice_parser/lib/src/extensions.dart new file mode 100644 index 0000000..e3249f3 --- /dev/null +++ b/packages/dart_dice_parser/lib/src/extensions.dart @@ -0,0 +1,15 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; + +import 'rolled_die.dart'; + +extension RolledDieIListExtensions on IList { + int get sum => map((d) => d.result).fold(0, (sum, i) => sum + i); + + int get successCount => where((d) => d.success).length; + + int get failureCount => where((d) => d.failure).length; + + int get critSuccessCount => where((d) => d.critSuccess).length; + + int get critFailureCount => where((d) => d.critFailure).length; +} diff --git a/lib/src/parser.dart b/packages/dart_dice_parser/lib/src/parser.dart similarity index 57% rename from lib/src/parser.dart rename to packages/dart_dice_parser/lib/src/parser.dart index 1e8b886..076c3e0 100644 --- a/lib/src/parser.dart +++ b/packages/dart_dice_parser/lib/src/parser.dart @@ -1,35 +1,30 @@ import 'package:petitparser/petitparser.dart'; -import 'ast.dart'; +import 'ast_core.dart'; +import 'ast_dice.dart'; +import 'ast_ops.dart'; import 'dice_expression.dart'; import 'dice_roller.dart'; -Parser parserBuilder(DiceRoller roller) { +Parser parserBuilder(DiceResultRoller roller) { final builder = ExpressionBuilder(); // numbers builder.primitive( digit().star().flatten('integer expected').trim().map(SimpleValue.new), ); - // parens - builder.group().wrapper( - char('(').trim(), - char(')').trim(), - (left, value, right) => value, - ); + // parens & curlies + builder.group() + ..wrapper(char('(').trim(), char(')').trim(), (left, value, right) => value) + ..wrapper( + char('{').trim(), + char('}').trim(), + (left, value, right) => AggregateOp(value), + ); // special dice handling need to have higher precedence than 'd' builder.group() - ..postfix( - string('dF').trim(), - (a, op) => FudgeDice(op, a, roller), - ) - ..postfix( - string('D66').trim(), - (a, op) => D66Dice(op, a, roller), - ) - ..postfix( - string('d%').trim(), - (a, op) => PercentDice(op, a, roller), - ) + ..postfix(string('dF').trim(), (a, op) => FudgeDice(op, a, roller)) + ..postfix(string('D66').trim(), (a, op) => D66Dice(op, a, roller)) + ..postfix(string('d%').trim(), (a, op) => PercentDice(op, a, roller)) ..postfix( seq4( char('d').trim(), @@ -41,22 +36,37 @@ Parser parserBuilder(DiceRoller roller) { char(']').trim(), ), (a, op) => CSVDice(op.toString(), a, roller, op.$3), + ) + ..postfix( + seq4( + char('d').trim(), + digit().plus().flatten().trim(), + char('p').trim(), + digit().plus().optional().flatten().trim(), + ), + (a, op) => PenetratingDice( + op.toString(), + a, + roller, + nsides: op.$2, + nsidesPenetration: op.$4, + ), ); builder.group().left( - char('d').trim(), - (a, op, b) => StdDice(op, a, b, roller), - ); + char('d').trim(), + (a, op, b) => StdDice(op, a, b, roller), + ); // compounding dice (has to be in separate group from exploding) builder.group().left( - (string('!!') & - pattern('oO').optional() & - pattern('<>').optional() & - char('=').optional()) - .flatten() - .trim(), - (a, op, b) => CompoundingDice(op.toLowerCase(), a, b, roller), - ); + (string('!!') & + pattern('o').optional() & + pattern('<>').optional() & + char('=').optional()) + .flatten() + .trim(), + (a, op, b) => CompoundingDice(op.toLowerCase(), a, b, roller), + ); builder.group() // reroll & reroll once ..left( @@ -94,12 +104,12 @@ Parser parserBuilder(DiceRoller roller) { ) // drop(-) low, high ..left( - (char('-') & pattern('LlHh')).flatten().trim(), + (char('-') & pattern('lLhH')).flatten().trim(), (a, op, b) => DropHighLowOp(op.toLowerCase(), a, b), ) // keep low/high ..left( - (pattern('Kk') & pattern('LlHh').optional()).flatten().trim(), + (pattern('k') & pattern('lLhH').optional()).flatten().trim(), (a, op, b) => DropHighLowOp(op.toLowerCase(), a, b), ); @@ -109,15 +119,21 @@ Parser parserBuilder(DiceRoller roller) { ..left(char('-').trim(), (a, op, b) => SubOp(op, a, b)); // count >=, <=, <, >, =, // #s, #cs, #f, #cf -- count (critical) successes / failures - builder.group().left( - (char('#') & - char('c').optional() & - pattern('sf').optional() & - pattern('<>').optional() & - char('=').optional()) - .flatten() - .trim(), - (a, op, b) => CountOp(op.toLowerCase(), a, b), - ); + builder.group() + ..left( + (char('#') & + char('c').optional() & + pattern('sf').optional() & + pattern('<>').optional() & + char('=').optional()) + .flatten() + .trim(), + (a, op, b) => CountOp(op.toLowerCase(), a, b), + ) + ..postfix( + (char('s') & char('d').optional()).flatten().trim(), + (a, op) => SortOp(op.toLowerCase(), a), + ) + ..left(char(',').trim(), (a, op, b) => CommaOp(op, a, b)); return builder.build().end(); } diff --git a/packages/dart_dice_parser/lib/src/roll_result.dart b/packages/dart_dice_parser/lib/src/roll_result.dart new file mode 100644 index 0000000..613f0b8 --- /dev/null +++ b/packages/dart_dice_parser/lib/src/roll_result.dart @@ -0,0 +1,201 @@ +import 'package:equatable/equatable.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; + +import 'enums.dart'; +import 'extensions.dart'; +import 'rolled_die.dart'; + +/// [RollResult] represents the result of evaluating a particular node of the AST. +/// +class RollResult extends Equatable { + RollResult({ + required this.expression, + required this.opType, + Iterable results = const IList.empty(), + Iterable discarded = const IList.empty(), + this.left, + this.right, + }) : results = IList(results), + discarded = IList(discarded); + + /// factory constructor to merge [other] with the params of this function + /// and produce a new [RollResult]. + factory RollResult.fromRollResult( + RollResult other, { + required String expression, + OpType? opType, + Iterable? results, + Iterable? discarded, + RollResult? left, + RollResult? right, + }) => RollResult( + expression: expression, + opType: opType ?? other.opType, + results: IList.orNull(results) ?? other.results, + discarded: IList.orNull(discarded) ?? other.discarded, + left: left ?? other.left, + right: right ?? other.right, + ); + + /// addition operator for [RollResult]. + /// + /// in the returned results, nsides will be max(nsides, other.nsides). + /// this is so we can explode a dice expr like `(2d6 + 1)!`. + /// NOTE: A side-effect of this decision is `(2d6 + 2d10)!` will explode with 10s, not 6s. + RollResult operator +(RollResult other) => RollResult.fromRollResult( + other, + expression: '($expression + ${other.expression})', + results: results + other.results, + discarded: discarded + other.discarded, + opType: OpType.add, + left: this, + right: other, + ); + + /// multiplication operator for [RollResult]. + /// + /// Results are collapsed into a single value (the result of multiplication), all other rolled die are discarded. + /// + RollResult operator *(RollResult other) => RollResult.fromRollResult( + other, + expression: '($expression * ${other.expression})', + results: [ + RolledDie.singleVal( + result: results.sum * other.results.sum, + from: results + other.results, + ), + ], + discarded: [ + ...results.map(RolledDie.discard), + ...other.results.map(RolledDie.discard), + ], + opType: OpType.multiply, + left: this, + right: other, + ); + + /// subtraction operator for [RollResult]. + /// + /// Results create new list lhs.results + (-1)*(other.results). + /// other.results are discarded, and a single value result is added + /// + RollResult operator -(RollResult other) => RollResult.fromRollResult( + other, + expression: '($expression - ${other.expression})', + opType: OpType.subtract, + results: [ + ...results, + RolledDie.singleVal(result: -1 * other.results.sum, from: other.results), + ], + discarded: [...other.results.map(RolledDie.discard)], + left: this, + right: other, + ); + + /// the parsed expression + final String expression; + + /// the results of the evaluating the expression + final IList results; + final IList discarded; + + final RollResult? left; + final RollResult? right; + + final OpType opType; + + /// sum of [results] + int get total => totalOrDefault(() => 0); + + int get successCount => results.successCount; + + int get failureCount => results.failureCount; + + int get critSuccessCount => results.critSuccessCount; + + int get critFailureCount => results.critFailureCount; + + @override + List get props => [ + expression, + opType, + results, + discarded, + opType, + //left, + //right, + ]; + + /// Get the total, or if results are empty return result of calling [defaultCb]. + int totalOrDefault(int Function() defaultCb) { + if (results.isEmpty) { + return defaultCb(); + } + return results.sum; + } + + @override + String toString() { + final buffer = StringBuffer(); + buffer.write( + '$expression =${opType.name}=> RollResult(${opType == OpType.value ? 'value' : 'total'}: $total', + ); + if (opType != OpType.value) { + if (results.isNotEmpty) { + buffer.write(', results: ${results.toString(false)}'); + } + if (discarded.isNotEmpty) { + buffer.write(', discarded: ${discarded.toString(false)}'); + } + } + buffer.write(')'); + return buffer.toString(); + } + + String toStringPretty({String indent = ''}) => pprint(this, indent: indent); + + Map toJson() => + { + 'expression': expression, + 'opType': opType.name, + 'results': results.map((e) => e.toJson()).toList(growable: false), + 'discarded': discarded.map((e) => e.toJson()).toList(growable: false), + 'left': left != null && left?.opType != OpType.value + ? left?.toJson() + : null, + 'right': right != null && right?.opType != OpType.value + ? right?.toJson() + : null, + 'total': total, + 'successCount': successCount, + 'failureCount': failureCount, + 'critSuccessCount': critSuccessCount, + 'critFailureCount': critFailureCount, + }..removeWhere( + (k, v) => + v == null || + (v is Map && v.isEmpty) || + (v is Iterable && v.isEmpty) || + (v is int && v == 0), + ); +} + +String pprint(RollResult? rr, {String indent = ''}) { + if (rr == null) { + return ''; + } + final buffer = StringBuffer(indent); + buffer.write(rr.toString()); + if (rr.left != null && rr.left?.opType != OpType.value) { + buffer + ..write('\n') + ..write(pprint(rr.left, indent: '$indent ')); + } + if (rr.right != null && rr.right?.opType != OpType.value) { + buffer + ..write('\n') + ..write(pprint(rr.right, indent: '$indent ')); + } + + return buffer.toString(); +} diff --git a/packages/dart_dice_parser/lib/src/roll_summary.dart b/packages/dart_dice_parser/lib/src/roll_summary.dart new file mode 100644 index 0000000..254dc0b --- /dev/null +++ b/packages/dart_dice_parser/lib/src/roll_summary.dart @@ -0,0 +1,117 @@ +import 'package:equatable/equatable.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; + +import 'extensions.dart'; +import 'roll_result.dart'; +import 'rolled_die.dart'; + +/// [RollSummary] is the final result of rolling a dice expression. +/// It rolls up the metadata of sub-expressions, and includes a `detailResults` +/// if the caller wants to do something interesting to display the result graph. +/// +/// A [RollResult] is modeled as a binary tree. The dice expression +/// is parsed into an AST, and when rolled the results reflect the structure of +/// that AST. +/// +/// In general, users will only care about the root node of the tree. +/// But, depending on the information you want from the evaluated dice rolls, +/// you may need to traverse the tree to inspect all the events. + +class RollSummary extends Equatable { + RollSummary({required this.detailedResults}) + : total = detailedResults.results.sum, + results = IList(detailedResults.results), + discarded = IList(detailedResults.discarded), + expression = detailedResults.expression, + successCount = detailedResults.results.successCount, + failureCount = detailedResults.results.failureCount, + critSuccessCount = detailedResults.results.critSuccessCount, + critFailureCount = detailedResults.results.critFailureCount; + + final RollResult detailedResults; + + /// sum of [results] + late final int total; + late final int successCount; + late final int failureCount; + late final int critSuccessCount; + late final int critFailureCount; + + /// the parsed expression + late final String expression; + + /// the results of the evaluating the expression + late final IList results; + + /// the dice we lost along the way + late final IList discarded; + + @override + List get props => [ + total, + successCount, + failureCount, + critSuccessCount, + critFailureCount, + expression, + results, + discarded, + ]; + + @override + String toString() { + final buffer = StringBuffer( + '$expression ===> RollSummary(total: $total, results: ${results.toString(false)}', + ); + if (discarded.isNotEmpty) { + buffer.write(', discarded: ${discarded.toString(false)}'); + } + final params = { + 'successCount': successCount, + 'failureCount': failureCount, + 'critSuccessCount': critSuccessCount, + 'critFailureCount': critFailureCount, + }..removeWhere((k, v) => v == 0); + + if (params.isNotEmpty) { + buffer.write(', '); + buffer.writeAll( + params.entries.map((entry) => '${entry.key}: ${entry.value}'), + ', ', + ); + } + + buffer.write(')'); + return buffer.toString(); + } + + Map toJson() => + { + 'expression': expression, + 'total': total, + 'successCount': successCount, + 'failureCount': failureCount, + 'critSuccessCount': critSuccessCount, + 'critFailureCount': critFailureCount, + 'results': results.map((e) => e.toJson()).toList(growable: false), + 'discarded': discarded.map((e) => e.toJson()).toList(growable: false), + 'detailedResults': detailedResults.toJson(), + }..removeWhere( + (k, v) => + v == null || + (v is Map && v.isEmpty) || + (v is Iterable && v.isEmpty) || + (v is int && v == 0) || + (v is bool && !v), + ); + + String toStringPretty() { + final buffer = StringBuffer(); + buffer + ..write(toString()) + ..write('\n') + ..write(detailedResults.toStringPretty(indent: ' ')); + + return buffer.toString(); + } +} diff --git a/packages/dart_dice_parser/lib/src/rolled_die.dart b/packages/dart_dice_parser/lib/src/rolled_die.dart new file mode 100644 index 0000000..93da888 --- /dev/null +++ b/packages/dart_dice_parser/lib/src/rolled_die.dart @@ -0,0 +1,384 @@ +import 'package:collection/collection.dart'; +import 'package:equatable/equatable.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; + +import 'dice_roller.dart'; +import 'enums.dart'; + +/// representation of a single dice roll result. +class RolledDie extends Equatable implements Comparable { + RolledDie({ + required this.result, + required this.dieType, + this.nsides = 0, + Iterable potentialValues = const IList.empty(), + this.discarded = false, + this.success = false, + this.failure = false, + this.critSuccess = false, + this.critFailure = false, + this.exploded = false, + this.explosion = false, + this.compoundedFinal = false, + this.compounded = false, + this.penetrated = false, + this.penetrator = false, + this.reroll = false, + this.rerolled = false, + this.clampCeiling = false, + this.clampFloor = false, + this.totaled = false, + this.from = const IList.empty(), + }) : potentialValues = IList(potentialValues) { + if (dieType.requirePotentialValues && potentialValues.isEmpty) { + throw ArgumentError( + 'Invalid die -- ${dieType.name} must have a potentialValues field', + ); + } + if (dieType.requireNSides && nsides == 0) { + throw ArgumentError( + 'Invalid die -- ${dieType.name} must have a nsides != 0', + ); + } + switch (dieType) { + case DieType.polyhedral: + maxPotentialValue = nsides; + minPotentialValue = 1; + case DieType.d66: + maxPotentialValue = 66; + minPotentialValue = 1; + case DieType.singleVal: + maxPotentialValue = minPotentialValue = result; + case DieType.nvals || DieType.fudge: + maxPotentialValue = potentialValues.max; + minPotentialValue = potentialValues.min; + } + } + + factory RolledDie.polyhedral({ + required int result, + required int nsides, + bool discarded = false, + }) => RolledDie( + result: result, + nsides: nsides, + dieType: DieType.polyhedral, + discarded: discarded, + ); + + factory RolledDie.fudge({required int result}) => RolledDie( + result: result, + nsides: DiceRoller.defaultFudgeVals.length, + dieType: DieType.fudge, + potentialValues: DiceRoller.defaultFudgeVals, + ); + + factory RolledDie.singleVal({ + required int result, + bool discarded = false, + bool penetrator = false, + bool totaled = false, + Iterable? from = const IList.empty(), + }) => RolledDie( + result: result, + nsides: 1, + discarded: discarded, + penetrator: penetrator, + dieType: DieType.singleVal, + potentialValues: [result], + totaled: totaled, + from: IList(from), + ); + + factory RolledDie.d66({ + required int result, + Iterable? from = const IList.empty(), + }) => RolledDie(result: result, dieType: DieType.d66, from: IList(from)); + + factory RolledDie.copyWith( + RolledDie other, { + int? result, + bool? discarded, + bool? success, + bool? failure, + bool? critSuccess, + bool? critFailure, + bool? exploded, + bool? explosion, + bool? compounded, + bool? compoundedFinal, + bool? penetrator, + bool? penetrated, + bool? reroll, + bool? rerolled, + bool? clampHigh, + bool? clampLow, + bool? totaled, + Iterable? from, + }) => RolledDie( + potentialValues: other.potentialValues, + nsides: other.nsides, + dieType: other.dieType, + result: result ?? other.result, + discarded: discarded ?? other.discarded, + success: success ?? other.success, + failure: failure ?? other.failure, + penetrated: penetrated ?? other.penetrated, + penetrator: penetrator ?? other.penetrator, + critSuccess: critSuccess ?? other.critSuccess, + critFailure: critFailure ?? other.critFailure, + exploded: exploded ?? other.exploded, + explosion: explosion ?? other.explosion, + compounded: compounded ?? other.compounded, + compoundedFinal: compoundedFinal ?? other.compoundedFinal, + reroll: reroll ?? other.reroll, + rerolled: rerolled ?? other.rerolled, + clampCeiling: clampHigh ?? other.clampCeiling, + clampFloor: clampLow ?? other.clampFloor, + totaled: totaled ?? other.totaled, + from: IList.orNull(from) ?? IList([other]), + ); + + factory RolledDie.discard(RolledDie other) => + RolledDie.copyWith(other, discarded: true); + + factory RolledDie.scoreForCountType( + RolledDie other, { + required CountType countType, + }) => RolledDie.copyWith( + other, + success: other.success || countType == CountType.success, + failure: other.failure || countType == CountType.failure, + critSuccess: other.critSuccess || countType == CountType.critSuccess, + critFailure: other.critFailure || countType == CountType.critFailure, + ); + + /// the rolled result + final int result; + + /// the number of sides on the die. Generally only set if dieType == polyhedral + final int nsides; + + /// the maximum possible result of this die + late final int maxPotentialValue; + + /// the minimum possible result of this die + late final int minPotentialValue; + + /// the die faces (potential values). + /// this will be empty for polyhedral roles -- values of a polyhederal die will be range of [1,nsides] + final IList potentialValues; + + /// true if the result has been discarded + final bool discarded; + + /// whether the die was scored as a 'success' + final bool success; + + /// whether the die was scored as a 'failure' + final bool failure; + + /// whether the die was scored as a 'critical success' + final bool critSuccess; + + /// whether the die was scored as a 'critical failure' + final bool critFailure; + + /// the type of die + final DieType dieType; + + /// the die that were operated on to become this die. + final IList from; + + /// true if the die exploded + final bool exploded; + + /// true if the die is the result of a die exploding + final bool explosion; + + /// true if the die was discarded as a roll during compounding + final bool compounded; + + /// true if the die is the sum a multiple die due to compounding + final bool compoundedFinal; + + /// true if the die was a discarded result during penetration + final bool penetrator; + + /// true if the die was the result of penetration + final bool penetrated; + + /// true if the (discarded) result is from a reroll + final bool rerolled; + + /// true if the result is the rerolled die + final bool reroll; + + /// true if the result has been clamped via `C>` + final bool clampCeiling; + + /// true if the result has been clamped via `C<` + final bool clampFloor; + + /// true if the result is a sum of other die results + final bool totaled; + + bool get isMaxResult => result == maxPotentialValue; + + bool get isCountable => minPotentialValue != maxPotentialValue; + + @override + List get props => [ + result, + nsides, + maxPotentialValue, + minPotentialValue, + potentialValues, + dieType, + discarded, + success, + failure, + critFailure, + critSuccess, + exploded, + explosion, + compounded, + compoundedFinal, + reroll, + rerolled, + clampCeiling, + clampFloor, + penetrated, + penetrator, + ]; + + Map toJson() => + { + 'result': result, + 'nsides': nsides, + 'potentialValues': potentialValues.toList(growable: false), + 'dieType': dieType.name, + 'discarded': discarded, + 'success': success, + 'failure': failure, + 'critSuccess': critSuccess, + 'critFailure': critFailure, + 'exploded': exploded, + 'explosion': explosion, + 'compounded': compounded, + 'compoundedFinal': compoundedFinal, + 'reroll': reroll, + 'rerolled': rerolled, + 'clampHigh': clampCeiling, + 'clampLow': clampFloor, + 'penetrated': penetrated, + 'penetrator': penetrator, + }..removeWhere( + (k, v) => + v == null || + (v is Map && v.isEmpty) || + (v is Iterable && v.isEmpty) || + (v is int && v == 0) || + (v is bool && !v), + ); + + String getDieGlyph() { + switch (dieType) { + case DieType.polyhedral: + return 'd$nsides'; + case DieType.fudge: + return 'dF'; + case DieType.d66: + return 'D66'; + case DieType.singleVal: + return 'val'; + default: + return 'd?'; + } + } + + String getDieStateGlyphs() { + final buffer = StringBuffer(); + + if (discarded) { + buffer.write('⛔︎'); + } + if (rerolled) { + buffer.write('↩'); + } + if (reroll) { + buffer.write('↩'); + } + if (exploded) { + buffer.write('💣'); //'⇪'); + } + if (explosion) { + buffer.write('🔥'); //'⇪'); + } + if (penetrated) { + buffer.write('➶'); + } + if (penetrator) { + buffer.write('⇡'); + } + if (compoundedFinal) { + buffer.write('⇈'); + } + if (compounded) { + buffer.write('↑'); + } + if (clampCeiling) { + buffer.write('⌈⌉'); + } + if (clampFloor) { + buffer.write('⌊⌋'); + } + if (totaled) { + buffer.write('∑'); + } + if (success) { + buffer.write('✓'); + } + if (failure) { + buffer.write('✗'); + } + if (critSuccess) { + buffer.write('✅'); + } + if (critFailure) { + buffer.write('❌'); + } + return buffer.toString(); + } + + @override + String toString() { + final buffer = StringBuffer(); + buffer.write(result); + buffer.write('('); + buffer.write(getDieGlyph()); + buffer.write(getDieStateGlyphs()); + buffer.write(')'); + return buffer.toString(); + } + + @override + int compareTo(RolledDie other) => result + .compareTo(other.result) + .if0(dieType.compareTo(other.dieType)) + .if0(nsides.compareTo(other.nsides)) + .if0(discarded.compareTo(other.discarded)) + .if0(success.compareTo(other.success)) + .if0(failure.compareTo(other.failure)) + .if0(failure.compareTo(other.failure)) + .if0(critSuccess.compareTo(other.critSuccess)) + .if0(critFailure.compareTo(other.critFailure)) + .if0(exploded.compareTo(other.exploded)) + .if0(explosion.compareTo(other.explosion)) + .if0(compoundedFinal.compareTo(other.compoundedFinal)) + .if0(compounded.compareTo(other.compounded)) + .if0(reroll.compareTo(other.reroll)) + .if0(rerolled.compareTo(other.rerolled)) + .if0(clampCeiling.compareTo(other.clampCeiling)) + .if0(clampFloor.compareTo(other.clampFloor)); +} diff --git a/lib/src/stats.dart b/packages/dart_dice_parser/lib/src/stats.dart similarity index 82% rename from lib/src/stats.dart rename to packages/dart_dice_parser/lib/src/stats.dart index 24e429c..cbaaffc 100644 --- a/lib/src/stats.dart +++ b/packages/dart_dice_parser/lib/src/stats.dart @@ -50,11 +50,11 @@ class StatsCollector { /// retrieve stats as map Map toJson({int precision = 3}) => { - 'mean': double.parse(_mean.toStringAsPrecision(precision)), - 'stddev': double.parse(_stddev.toStringAsPrecision(precision)), - 'min': _minVal, - 'max': _maxVal, - 'count': _count, - 'histogram': _histogram, - }; + 'mean': double.parse(_mean.toStringAsPrecision(precision)), + 'stddev': double.parse(_stddev.toStringAsPrecision(precision)), + 'min': _minVal, + 'max': _maxVal, + 'count': _count, + 'histogram': _histogram, + }; } diff --git a/lib/src/utils.dart b/packages/dart_dice_parser/lib/src/utils.dart similarity index 100% rename from lib/src/utils.dart rename to packages/dart_dice_parser/lib/src/utils.dart diff --git a/packages/dart_dice_parser/pubspec.yaml b/packages/dart_dice_parser/pubspec.yaml new file mode 100644 index 0000000..b316306 --- /dev/null +++ b/packages/dart_dice_parser/pubspec.yaml @@ -0,0 +1,29 @@ +name: dart_dice_parser +description: A dart library for parsing dice notation (`2d6+4`). Supports advantage/disadvantage, exploding die, and other variations. +version: 8.0.0 +homepage: https://github.com/Adventuresmith/dart-dice-parser +repository: https://github.com/Adventuresmith/dart-dice-parser + +resolution: workspace + +environment: + sdk: "^3.8.0" + +topics: + - dice + - rpg + - ttrpg + +dependencies: + collection: ^1.0.0 + equatable: ^2.0.0 + fast_immutable_collections: ^11.0.0 + logging: ^1.3.0 + # TODO: upgrade to 7.0.0 once version constraints from stable flutter sdk don't barf + petitparser: ^6.1.0 + +dev_dependencies: + io: ^1.0.0 + lint: ^2.8.0 + mocktail: ^1.0.0 + test: ^1.0.0 diff --git a/packages/dart_dice_parser/test/dart_dice_parser_test.dart b/packages/dart_dice_parser/test/dart_dice_parser_test.dart new file mode 100644 index 0000000..3ee4b08 --- /dev/null +++ b/packages/dart_dice_parser/test/dart_dice_parser_test.dart @@ -0,0 +1,1102 @@ +import 'dart:math'; + +import 'package:dart_dice_parser/dart_dice_parser.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class MockRandom extends Mock implements Random {} + +void main() { + late Random staticMockRandom; + late Random seededRandom; + + setUp(() { + // first 100 seeded rolls for d6 + // [6, 2, 1, 5, 3, 5, 1, 4, 6, 5, 6, 4, 2, 4, 2, 3, 5, 1, 1, 2, 4, 1, 6, 2, 2, 5, 6, 3, 1, 3, 6, 1, 2, 3, 6, 2, 1, 1, 1, 3, 1, 2, 3, 3, 6, 2, 5, 4, 3, 4, 1, 5, 4, 4, 2, 6, 5, 4, 6, 2, 3, 1, 4, 5, 3, 2, 2, 6, 6, 4, 4, 2, 6, 2, 5, 3, 3, 4, 4, 2, 2, 4, 3, 2, 6, 6, 4, 6, 4, 4, 3, 1, 4, 2, 2, 4, 3, 3, 1, 3] + seededRandom = Random(1234); + staticMockRandom = MockRandom(); + // NOTE: this mocks the random number generator to always return '1' + // -- that means the dice-roll is '2' (since rolls are 1-based) + when(() => staticMockRandom.nextInt(any())).thenReturn(1); + }); + void staticRandTest(String name, String input, int expectedTotal) { + test('$name - $input', () async { + expect( + (await DiceExpression.create( + input, + roller: RNGRoller(staticMockRandom), + ).roll()).total, + equals(expectedTotal), + ); + }); + } + + void seededRandTest( + String testName, + String inputExpr, + int? expectedTotal, { + Iterable? expectedResults, + int? successCount, + int? failureCount, + int? critSuccessCount, + int? critFailureCount, + bool? verifyResultOrder, + }) { + test('$testName - $inputExpr', () async { + final rollSummary = await DiceExpression.create( + inputExpr, + roller: RNGRoller(seededRandom), + ).roll(); + if (expectedTotal != null) { + expect( + rollSummary.total, + equals(expectedTotal), + reason: 'mismatching total', + ); + } + if (expectedResults != null) { + final actualResults = rollSummary.results.map((d) => d.result).toList(); + expect( + actualResults, + verifyResultOrder ?? true + ? unorderedEquals(expectedResults) + : equals(expectedResults), + reason: 'mismatching results', + ); + } + if (successCount != null) { + expect( + rollSummary.successCount, + equals(successCount), + reason: 'mismatched success count', + ); + } + if (failureCount != null) { + expect( + rollSummary.failureCount, + equals(failureCount), + reason: 'mismatched success count', + ); + } + if (critSuccessCount != null) { + expect( + rollSummary.critSuccessCount, + equals(critSuccessCount), + reason: 'mismatched success count', + ); + } + if (critFailureCount != null) { + expect( + rollSummary.critFailureCount, + equals(critFailureCount), + reason: 'mismatched success count', + ); + } + }); + } + + group('arithmetic', () { + seededRandTest('addition', '1+20', 21); + seededRandTest('multi', '3*2', 6); + seededRandTest('parens', '(5+6)*2', 22); + seededRandTest('order of operations', '5+6*2', 17); + seededRandTest('subtraction', '5-6', -1); + seededRandTest('subtraction', '5-6', -1); + seededRandTest('subtraction', '1-', 1); + seededRandTest('subtraction', '1-0', 1); + seededRandTest('subtraction', '0-1', -1); + seededRandTest('subtraction', '-1', -1); + seededRandTest('negative number', '-6', -6); // this will be 0-6 + }); + + group('dice and arith', () { + seededRandTest('dice', '4d6', 14, expectedResults: [6, 2, 1, 5]); + seededRandTest('dice+', '4d6+2', 16, expectedResults: [6, 2, 1, 5, 2]); + seededRandTest('dice*', '4d6*2', 28, expectedResults: [28]); + }); + + group('successes and failures', () { + // count s=nsides, f=1 + seededRandTest( + 'defaults are 1 or ndice', + '4d6#s#f#cs#cf', + 14, + expectedResults: const [6, 2, 1, 5], + successCount: 1, + failureCount: 1, + critSuccessCount: 1, + critFailureCount: 1, + ); + seededRandTest( + 'equal sign can be omitted', + '4d6#s6#f1', + 14, + expectedResults: const [6, 2, 1, 5], + successCount: 1, + failureCount: 1, + critSuccessCount: 0, + critFailureCount: 0, + ); + seededRandTest( + 'with equal sign', + '4d6#s=6#f=1', + 14, + expectedResults: [6, 2, 1, 5], + successCount: 1, + failureCount: 1, + critSuccessCount: 0, + critFailureCount: 0, + ); + + seededRandTest( + 'dice', + '4d6#s>4#f<=2#cs>5#cf<2', + 14, + expectedResults: [6, 2, 1, 5], + successCount: 2, + failureCount: 2, + critSuccessCount: 1, + critFailureCount: 1, + ); + + seededRandTest( + 'dice', + '4d6#s>=4#f<2', + 14, + expectedResults: [6, 2, 1, 5], + successCount: 2, + failureCount: 1, + ); + + seededRandTest( + 'success low, failures high', + '4d6#s<2#f>5', + 14, + expectedResults: [6, 2, 1, 5], + successCount: 1, + failureCount: 1, + ); + seededRandTest( + 'critical success is also a success', + '4d6 #s>=5 #cs=6 #f=1', + 14, + expectedResults: [6, 2, 1, 5], + failureCount: 1, + successCount: 2, + critSuccessCount: 1, + ); + + seededRandTest( + 'critical failure is also a failure', + '4d6 #s>=5 #cs=6 #f<=2 #cf', + 14, + expectedResults: [6, 2, 1, 5], + successCount: 2, + failureCount: 2, + critSuccessCount: 1, + critFailureCount: 1, + ); + }); + + group('rollVals', () { + seededRandTest( + '4d6 equivalent', + '4d[1,2,3,4,5,6]', + 14, + expectedResults: [6, 2, 1, 5], + ); + seededRandTest( + '4d6 equivalent - with whitespace', + '4 d \n[1,2 ,3,4,5,6]', + 14, + expectedResults: [6, 2, 1, 5], + ); + + seededRandTest( + '4d6 equivalent -- with negatives', + '4d[-1,2,3,4,5,-6]', + 0, + expectedResults: [-6, 2, -1, 5], + ); + }); + + group('counting operations', () { + // mocked responses should return rolls of 6, 2, 1, 5 + seededRandTest('count >', '4d6#>3', 2); + seededRandTest('count <', '4d6#<6', 3); + seededRandTest('count =', '4d6#=1', 1); + seededRandTest('count <=', '4d6#<=2', 2); + seededRandTest('count >=', '4d6#>=6', 1); + seededRandTest('count > (missing from result)', '4d6#>6', 0); + seededRandTest('count #', '4d6#', 4); + seededRandTest('count # after drop', '4d6-<2#', 3); + seededRandTest('count # missing equals', '4d6#1', 1); + seededRandTest('count # with equals', '4d6#=1', 1); + // this only counts one if you use equals sign. + seededRandTest('count arith result - #1', '(4d6+1)#1', 2); + seededRandTest('count arith result - #=1', '(4d6+1)#=1', 2); + seededRandTest('count arith result - #', '(4d6+1)#', 5); + seededRandTest( + 'count arith result - #=1', + '(4d6+1)#s#f', + 15, + expectedResults: [1, 6, 2, 5, 1], + successCount: 1, + failureCount: 1, // the 1(val) does not count as a failure + ); + + seededRandTest( + 'count arith result - #=1', + '(4d6+1)#s#f=1', + 15, + expectedResults: [1, 6, 2, 5, 1], + successCount: 1, + failureCount: 2, + ); + + // 1234 seed will return [1, -1, -1, 1, 0, 1] + seededRandTest('count fudge', '6dF#', 6); + seededRandTest('count fudge', '6dF#=1', 3); + seededRandTest('count fudge', '6dF#=0', 1); + seededRandTest('count fudge', '6dF#<0', 2); + seededRandTest('count fudge', '6dF#>0', 3); + seededRandTest('count', '4d6#', 4); + seededRandTest('count', '4d6#6', 1); + + final invalids = [ + '4d6#=', + '4d6#<=', + '4d6#>=', + '4d6#>', + '4d6#<', + '4d6-=', + '4d6 C=', + '4d6 r=', + '4d6 ro=', + ]; + for (final v in invalids) { + test('invalid count - $v', () { + expect(() => DiceExpression.create(v).roll(), throwsFormatException); + }); + } + }); + + group('keep high/low', () { + // mocked responses should return rolls of 6, 2, 1, 5 + seededRandTest('keep low missing rhs', '4d6kl', 1); + seededRandTest('keep low', '4d6kl2', 3); + seededRandTest('keep low', '4d6kl3', 8); + seededRandTest('keep high missing rhs', '4d6kh', 6); + seededRandTest('keep high', '4d6kh2', 11); + seededRandTest('keep high', '4d6kh3', 13); + seededRandTest('keep high missing rhs', '4d6k', 6); + seededRandTest('keep high', '4d6k2', 11); + seededRandTest('keep high', '4d6k3', 13); + }); + + group('roll modifiers - drop, clamp, etc', () { + // mocked responses should return rolls of 6, 2, 1, 5 + seededRandTest('drop high', '4d6-H', 8); + seededRandTest('drop high (lowercase)', '4d6-h', 8); + seededRandTest('drop high (1)', '4d6-h1', 8); + seededRandTest('drop high (3)', '4d6-h3', 1); + seededRandTest('drop low', '4d6-L', 13); + seededRandTest('drop add result', '(4d6+1)-L', 14); + seededRandTest('drop add result', '1-L', 0); + seededRandTest('drop low (lower)', '4d6-l', 13); + seededRandTest('drop low - 1', '4d6-l1', 13); + seededRandTest('drop low - 3', '4d6-l3', 6); + seededRandTest('drop low and high', '4d6-L-H', 7); + seededRandTest('can drop more than rolled', '3d6-H4', 0); + seededRandTest('can drop more than rolled', '3d6-l4', 0); + seededRandTest('can drop arith result', '(2d6+3d6)-L1', 16); + seededRandTest( + 'can drop arith result -- diff dice sides', + '(2d6+3d4)-L1', + 14, + ); + seededRandTest('drop', '4d6->3', 3); + seededRandTest('drop', '4d6-<3', 11); + seededRandTest('drop', '4d6->=2', 1); + seededRandTest('drop', '4d6-<=2', 11); + seededRandTest('drop', '4d6-=2', 12); + seededRandTest('drop (not in results)', '4d6-=4', 14); + seededRandTest('clamp', '4d6C>3', 9); + seededRandTest('clamp', '4d6C<3', 17); + seededRandTest('clamp', '4d6c>3', 9); + seededRandTest('clamp', '4d6c<3', 17); + seededRandTest('clamp', '1 C<1', 1); + // rolls [1,-1,-1,1] , -1s turned to 0 + seededRandTest('clamp', '4dF C<0', 2); + + // mocked responses should return rolls of 6, 2, 1, 5, 3 + // [6,2] + [1,5,3] = [6,2,1,5,3]-L3 => [6,5] = 9 + seededRandTest('drop low on aggregated dice', '(2d6+3d6)-L3', 11); + + test('missing clamp target', () { + expect( + () => DiceExpression.create( + '6d6 C<', + roller: RNGRoller(seededRandom), + ).roll(), + throwsFormatException, + ); + }); + }); + + group('listeners', () { + test('basic', () async { + final dice = DiceExpression.create( + '2d6 kh', + roller: RNGRoller(seededRandom), + ); + final results = []; + final summaries = []; + await dice.roll( + onRoll: (rr) { + results.add(rr); + }, + onSummary: (rs) { + summaries.add(rs); + }, + ); + final rrRoll = RollResult( + expression: '(2d6)', + opType: OpType.rollDice, + results: [ + RolledDie.polyhedral(result: 6, nsides: 6), + RolledDie.polyhedral(result: 2, nsides: 6), + ], + ); + final rrDrop = RollResult( + expression: '((2d6) kh )', + opType: OpType.drop, + results: [RolledDie.polyhedral(result: 6, nsides: 6)], + discarded: [ + RolledDie( + result: 2, + nsides: 6, + dieType: DieType.polyhedral, + discarded: true, + ), + ], + left: rrRoll, + ); + final expectedSummary = RollSummary(detailedResults: rrDrop); + expect(results, equals([rrRoll, rrDrop])); + expect(summaries, equals([expectedSummary])); + }); + }); + + group('addition combines', () { + // mocked responses should return rolls of 6, 2, 1, 5 + seededRandTest( + 'addition combines results (drop is higher priority than plus)', + '3d6+1d6-L1', + 9, + ); + seededRandTest('addition combines results - parens', '(2d6+2d6)-L1', 13); + }); + + group('mult variations', () { + // mocked responses should return rolls of 6, 2, 1, 5 + seededRandTest('int mult on rhs', '2d6*2', 16); + seededRandTest('int mult on lhs', '2*2d6', 16); + seededRandTest('int mult on lhs', '(2*2d6)-l', 0); + }); + + group('missing ints', () { + seededRandTest('empty string returns zero', '', 0); + seededRandTest('empty arith returns zero - add', '+', 0); + seededRandTest('empty arith returns zero - mult', '*', 0); + seededRandTest('empty ndice is 1', 'd6', 6); + seededRandTest('whitespace should be swallowed', '2 d6', 8); + seededRandTest('whitespace should be swallowed', '2d 6', 8); + + test('missing nsides', () { + expect( + () => + DiceExpression.create('6d', roller: RNGRoller(seededRandom)).roll(), + throwsFormatException, + ); + }); + }); + group('metadata', () { + seededRandTest( + 'reroll, keep, count success,count fail, add', + '(((10d6 r=3)kh2 #s>5)#f<2)+2', + 14, + expectedResults: [6, 6, 2], + successCount: 2, + ); + + seededRandTest( + 'score is determined from subtrees', + '(4d6#s<=2#f>=5) + 1', + 15, + expectedResults: [6, 2, 1, 5, 1], + successCount: 2, + failureCount: 2, + ); + + seededRandTest( + 'separate keeps', + '(((4d6 kh3) + (4d6 kh2))kh3)', + 16, + expectedResults: [6, 5, 5], + ); + }); + + group('reroll', () { + seededRandTest('reroll', '10d4 r=3', 35); + seededRandTest('reroll', '10d4 r3', 35); + seededRandTest('reroll', '10d4 r<2', 33); + seededRandTest('reroll', '10d4 r>2', 16); + seededRandTest('reroll', '10d4 r<=3', 40); + seededRandTest('reroll', '10d4 r>=2', 10); + + seededRandTest('reroll', '10d4 ro=3', 35); + seededRandTest('reroll', '10d4 ro3', 35); + seededRandTest('reroll', '10d4 ro<2', 33); + seededRandTest('reroll', '10d4 ro>2', 26); + seededRandTest('reroll', '10d4 ro<=3', 34); + seededRandTest('reroll', '10d4 ro>=2', 27); + + seededRandTest('reroll once', '8d6r>3', 15); + seededRandTest('reroll once', '8d6ro>3', 28); + }); + + group('dice', () { + staticRandTest('order of operations, with dice', '5 + 6 * 2d6', 29); + + seededRandTest('simple roll', '1d6', 6); + seededRandTest('simple roll', 'd6', 6); + seededRandTest('percentile', '1d%', 96); + seededRandTest('percentile', 'd%', 96); + seededRandTest('D66', '1D66', 62); + seededRandTest('D66', 'D66', 62); + seededRandTest('d66 -- 66-sided, not D66', '1d66', 30); + seededRandTest('d66 -- 66-sided, not D66', 'd66', 30); + seededRandTest('ndice in parens', '(4+6)d10', 54); + seededRandTest('nsides in parens', '10d(2*3)', 38); + + seededRandTest('zero dice rolled', '0d6', 0); + + staticRandTest('dice expr as sides', '2d(3d6)', 4); + + seededRandTest('fudge', '4dF', 0); + seededRandTest('fudge', 'dF', 1); + seededRandTest('fudge', '1dF', 1); + + // 1st roll: 6, 2, 1, 5, 3, 5, 1, 4, 6, (explodes 2) (total 33) + // 2nd roll: 5,6 (explodes 1) (total 11) + // 3rd roll: 4 (explodes 0) (total 4) + seededRandTest('exploding dice', '9d6!', 48); + seededRandTest('exploding dice', '9d6!6', 48); + seededRandTest('exploding dice', '9d6!=6', 48); + seededRandTest('exploding dice', '9d6!>=6', 48); + seededRandTest('exploding dice', '9d6!>5', 48); + + seededRandTest('exploding dice', '9d6!o', 44); + seededRandTest('exploding dice', '9d6!o6', 44); + seededRandTest('exploding dice', '9d6!o=6', 44); + seededRandTest('exploding dice', '9d6!o>=6', 44); + seededRandTest('exploding dice', '9d6!o>5', 44); + + seededRandTest('exploding dice', '9d6!1', 44); + seededRandTest('exploding dice', '9d6!>=5', 56); + seededRandTest('exploding dice', '9d6!<2', 44); + seededRandTest('exploding dice', '9d6!<=3', 54); + + seededRandTest('exploding dice', '9d6!o1', 44); + seededRandTest('exploding dice', '9d6!o>=5', 50); + seededRandTest('exploding dice', '9d6!o<2', 44); + seededRandTest('exploding dice', '9d6!o<=3', 50); + + // 1st round: 6, 2, 1, 5, 3, 5, 1, 4, 6, (compounds 2) (total 33) + // 2nd round: 5, 6 (compounds 1) (total 11) + // 3rd round: 4 (compounds 0) (total 4) + // result 11, 2, 1, 5, 3, 5, 1, 4, 16 + seededRandTest('compounding dice', '9d6!!', 48); + seededRandTest('compounding dice', '9d6!!6', 48); + seededRandTest('compounding dice', '9d6!!=6', 48); + seededRandTest('compounding dice', '9d6!!>=6', 48); + seededRandTest('compounding dice', '9d6!!>5', 48); + + seededRandTest('compounding dice', '9d6!!o', 44); + seededRandTest('compounding dice', '9d6!!o6', 44); + seededRandTest('compounding dice', '9d6!!o=6', 44); + seededRandTest('compounding dice', '9d6!!o>=6', 44); + seededRandTest('compounding dice', '9d6!!o>5', 44); + + seededRandTest('compounding dice count', '9d6!!#>6', 2); + + seededRandTest('compounding dice', '9d6!!>=5', 56); + seededRandTest('compounding dice', '9d6!!<3', 48); + seededRandTest('compounding dice', '9d6!!<=3', 54); + seededRandTest('compounding dice', '9d6!!1', 44); + + seededRandTest('compounding dice', '9d6!!o>=5', 50); + seededRandTest('compounding dice', '9d6!!o<3', 48); + seededRandTest('compounding dice', '9d6!!o<=3', 50); + seededRandTest('compounding dice', '9d6!!o1', 44); + + seededRandTest('penetrating dice', '9d6p', 45); + seededRandTest('penetrating dice', '9d6p4', 50); + + seededRandTest('explode arith result', '(9d6+3)!', 51); + + // explode, then count 6's + seededRandTest('exploding dice and count', '9d6!#=6', 3); + // explode, then drop less-than-6, then count (should be identical to above) + seededRandTest('exploding dice and count variation', '9d6!-<6#', 3); + + // different dice pools can be combined + seededRandTest('differing nsides addition', '4d4 + 4d6', 25); + // fudge dice can be rolled + seededRandTest('differing nsides addition', '4dF + 6dF', 2); + // fudge dice can be added to [1, -1, -1, 1] + seededRandTest('differing nsides addition', '4dF + 1', 1); + seededRandTest('fudge add to d6', '4d6+4dF', 14); + seededRandTest('fudge add to d6', '4dF+4d6', 13); + + seededRandTest( + 'sorted add', + '(1d4+1d6+1d8+1d10) s', + 20, + expectedResults: [2, 4, 5, 9], + verifyResultOrder: true, + ); + seededRandTest( + 'sorted comma', + '(1d4,1d6,1d8,1d10) s', + 20, + expectedResults: [2, 4, 5, 9], + verifyResultOrder: true, + ); + seededRandTest( + 'unsorted add', + '(1d4+1d6+1d8+1d10)', + 20, + expectedResults: [4, 2, 5, 9], + verifyResultOrder: true, + ); + seededRandTest( + 'unsorted comma', + '(1d4,1d6,1d8,1d10)', + 20, + expectedResults: [4, 2, 5, 9], + verifyResultOrder: true, + ); + + seededRandTest( + 'scored comma', + '(1d4,1d4p,1d4!,1d4!!)#s>=4', + 14, + expectedResults: [4, 4, 3, 3], + successCount: 2, + ); + + test('multiple rolls is multiple results', () async { + final dice = DiceExpression.create( + '2d6', + roller: RNGRoller(seededRandom), + ); + expect((await dice.roll()).total, 8); + expect((await dice.roll()).total, 6); + }); + + test('create dice with real random', () { + final dice = DiceExpression.create('10d100'); + final result1 = dice.roll(); + // result will never be zero -- this test is verifying creating the expr & doing roll + expect(result1, isNot(0)); + }); + + test('string method returns expr', () { + final dice = DiceExpression.create( + '2d6# + 5d6!>=5 + 5D66', + roller: RNGRoller(seededRandom), + ); + expect(dice.toString(), '((2d6) # (( + ((5d6) !>= 5)) + (5D66)))'); + }); + + test('invalid dice str', () { + expect( + () => DiceExpression.create( + '1d5 + x2', + roller: RNGRoller(seededRandom), + ).roll(), + throwsFormatException, + ); + }); + + seededRandTest('no-op explode', '4!', 4); + seededRandTest('no-op compound', '4!!', 4); + + seededRandTest('no-op explode', '4dF!', 0); + seededRandTest('no-op compound', '4dF!!', 0); + seededRandTest('no-op explode', '4d66!', 128); + seededRandTest('no-op compound', '4d66!!', 128); + final invalids = ['4dFr', '4D66 r']; + for (final i in invalids) { + test('invalid - $i', () { + expect( + () => + DiceExpression.create(i, roller: RNGRoller(seededRandom)).roll(), + throwsFormatException, + ); + }); + } + + test('toString', () async { + // mocked responses should return rolls of 6, 2, 1, 5 + final dice = DiceExpression.create( + '(4d(3+3)! + (2+2)d6) #cs #cf #s #f', + roller: RNGRoller(seededRandom), + ); + final out = (await dice.roll()).toString(); + expect( + out, + equalsIgnoringWhitespace( + '(((((((4d(3 + 3)) ! ) + ((2 + 2)d6)) #cs ) #cf ) #s ) #f ) ===> RollSummary(total: 33, results: [1(d6✗❌), 1(d6✗❌), 6(d6💣✓✅), 6(d6✓✅), 3(d6🔥), 2(d6), 5(d6), 5(d6), 4(d6)], successCount: 2, failureCount: 2, critSuccessCount: 2, critFailureCount: 2)', + ), + ); + }); + test('toStringPretty', () async { + // mocked responses should return rolls of 6, 2, 1, 5 + final dice = DiceExpression.create( + '(4d(3+3)! + (2+2)d6) #cs #cf #s #f', + roller: RNGRoller(seededRandom), + ); + final out = (await dice.roll()).toStringPretty(); + expect( + out, + equalsIgnoringWhitespace( + ''' + (((((((4d(3 + 3)) ! ) + ((2 + 2)d6)) #cs ) #cf ) #s ) #f ) ===> RollSummary(total: 33, results: [1(d6✗❌), 1(d6✗❌), 6(d6💣✓✅), 6(d6✓✅), 3(d6🔥), 2(d6), 5(d6), 5(d6), 4(d6)], successCount: 2, failureCount: 2, critSuccessCount: 2, critFailureCount: 2) + (((((((4d(3 + 3)) ! ) + ((2 + 2)d6)) #cs ) #cf ) #s ) #f ) =count=> RollResult(total: 33, results: [1(d6✗❌), 1(d6✗❌), 6(d6💣✓✅), 6(d6✓✅), 3(d6🔥), 2(d6), 5(d6), 5(d6), 4(d6)]) + ((((((4d(3 + 3)) ! ) + ((2 + 2)d6)) #cs ) #cf ) #s ) =count=> RollResult(total: 33, results: [6(d6💣✓✅), 6(d6✓✅), 1(d6❌), 1(d6❌), 3(d6🔥), 2(d6), 5(d6), 5(d6), 4(d6)]) + (((((4d(3 + 3)) ! ) + ((2 + 2)d6)) #cs ) #cf ) =count=> RollResult(total: 33, results: [1(d6❌), 1(d6❌), 6(d6💣✅), 6(d6✅), 3(d6🔥), 2(d6), 5(d6), 5(d6), 4(d6)]) + ((((4d(3 + 3)) ! ) + ((2 + 2)d6)) #cs ) =count=> RollResult(total: 33, results: [6(d6💣✅), 6(d6✅), 3(d6🔥), 2(d6), 1(d6), 5(d6), 5(d6), 1(d6), 4(d6)]) + (((4d(3 + 3)) ! ) + ((2 + 2)d6)) =add=> RollResult(total: 33, results: [6(d6💣), 3(d6🔥), 2(d6), 1(d6), 5(d6), 5(d6), 1(d6), 4(d6), 6(d6)]) + ((4d(3 + 3)) ! ) =explode=> RollResult(total: 17, results: [6(d6💣), 3(d6🔥), 2(d6), 1(d6), 5(d6)]) + (4d(3 + 3)) =rollDice=> RollResult(total: 14, results: [6(d6), 2(d6), 1(d6), 5(d6)]) + (3 + 3) =add=> RollResult(total: 6, results: [3(val), 3(val)]) + ((2 + 2)d6) =rollDice=> RollResult(total: 16, results: [5(d6), 1(d6), 4(d6), 6(d6)]) + (2 + 2) =add=> RollResult(total: 4, results: [2(val), 2(val)]) + ''' + .trim(), + ), + ); + }); + + test('toStringPretty - penetrating', () async { + // mocked responses should return rolls of 6, 2, 1, 5 + final dice = DiceExpression.create( + '9d6p', + roller: RNGRoller(seededRandom), + ); + final out = (await dice.roll()).toStringPretty(); + expect( + out, + equalsIgnoringWhitespace( + ''' +(9d6p6) ===> RollSummary(total: 45, results: [10(d6➶), 2(d6), 1(d6), 5(d6), 3(d6), 5(d6), 1(d6), 4(d6), 14(d6➶)], discarded: [6(d6⛔︎⇡), 5(d6⛔︎⇡), -1(val⛔︎⇡), 6(d6⛔︎⇡), 6(d6⛔︎⇡), 4(d6⛔︎⇡), -2(val⛔︎⇡)]) + (9d6p6) =rollPenetration=> RollResult(total: 45, results: [10(d6➶), 2(d6), 1(d6), 5(d6), 3(d6), 5(d6), 1(d6), 4(d6), 14(d6➶)], discarded: [6(d6⛔︎⇡), 5(d6⛔︎⇡), -1(val⛔︎⇡), 6(d6⛔︎⇡), 6(d6⛔︎⇡), 4(d6⛔︎⇡), -2(val⛔︎⇡)]) + + ''' + .trim(), + ), + ); + }); + test('toJson', () async { + // mocked responses should return rolls of 6, 2, 1, 5 + final dice = DiceExpression.create( + '4d6', + roller: RNGRoller(seededRandom), + ); + final obj = (await dice.roll()).toJson(); + expect( + obj, + equals({ + 'expression': '(4d6)', + 'total': 14, + 'results': [ + {'result': 6, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 2, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 1, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 5, 'nsides': 6, 'dieType': 'polyhedral'}, + ], + 'detailedResults': { + 'expression': '(4d6)', + 'opType': 'rollDice', + 'results': [ + {'result': 6, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 2, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 1, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 5, 'nsides': 6, 'dieType': 'polyhedral'}, + ], + 'total': 14, + }, + }), + ); + }); + + test('toJson - with scoring', () async { + // mocked responses should return rolls of 6, 2, 1, 5 + final dice = DiceExpression.create( + '4d6 #cf #cs', + roller: RNGRoller(seededRandom), + ); + final obj = (await dice.roll()).toJson(); + expect( + obj, + equals({ + 'expression': '(((4d6) #cf ) #cs )', + 'total': 14, + 'critSuccessCount': 1, + 'critFailureCount': 1, + 'results': [ + { + 'result': 6, + 'nsides': 6, + 'dieType': 'polyhedral', + 'critSuccess': true, + }, + { + 'result': 1, + 'nsides': 6, + 'dieType': 'polyhedral', + 'critFailure': true, + }, + {'result': 2, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 5, 'nsides': 6, 'dieType': 'polyhedral'}, + ], + 'detailedResults': { + 'expression': '(((4d6) #cf ) #cs )', + 'opType': 'count', + 'results': [ + { + 'result': 6, + 'nsides': 6, + 'dieType': 'polyhedral', + 'critSuccess': true, + }, + { + 'result': 1, + 'nsides': 6, + 'dieType': 'polyhedral', + 'critFailure': true, + }, + {'result': 2, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 5, 'nsides': 6, 'dieType': 'polyhedral'}, + ], + 'left': { + 'expression': '((4d6) #cf )', + 'opType': 'count', + 'results': [ + { + 'result': 1, + 'nsides': 6, + 'dieType': 'polyhedral', + 'critFailure': true, + }, + {'result': 6, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 2, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 5, 'nsides': 6, 'dieType': 'polyhedral'}, + ], + 'left': { + 'expression': '(4d6)', + 'opType': 'rollDice', + 'results': [ + {'result': 6, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 2, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 1, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 5, 'nsides': 6, 'dieType': 'polyhedral'}, + ], + 'total': 14, + }, + 'total': 14, + 'critFailureCount': 1, + }, + 'total': 14, + 'critSuccessCount': 1, + 'critFailureCount': 1, + }, + }), + ); + }); + + test('toJson - 9d6p4', () async { + final dice = DiceExpression.create( + '9d6p4', + roller: RNGRoller(seededRandom), + ); + final obj = (await dice.roll()).toJson(); + expect( + obj, + equals({ + 'expression': '(9d6p4)', + 'total': 50, + 'results': [ + { + 'result': 8, + 'nsides': 6, + 'dieType': 'polyhedral', + 'penetrated': true, + }, + {'result': 2, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 1, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 5, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 3, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 5, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 1, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 4, 'nsides': 6, 'dieType': 'polyhedral'}, + { + 'result': 21, + 'nsides': 6, + 'dieType': 'polyhedral', + 'penetrated': true, + }, + ], + 'discarded': [ + { + 'result': 6, + 'nsides': 6, + 'dieType': 'polyhedral', + 'discarded': true, + 'penetrator': true, + }, + { + 'result': 3, + 'nsides': 4, + 'dieType': 'polyhedral', + 'discarded': true, + 'penetrator': true, + }, + { + 'result': -1, + 'nsides': 1, + 'potentialValues': [-1], + 'dieType': 'singleVal', + 'discarded': true, + 'penetrator': true, + }, + { + 'result': 6, + 'nsides': 6, + 'dieType': 'polyhedral', + 'discarded': true, + 'penetrator': true, + }, + { + 'result': 4, + 'nsides': 4, + 'dieType': 'polyhedral', + 'discarded': true, + 'penetrator': true, + }, + { + 'result': 4, + 'nsides': 4, + 'dieType': 'polyhedral', + 'discarded': true, + 'penetrator': true, + }, + { + 'result': 4, + 'nsides': 4, + 'dieType': 'polyhedral', + 'discarded': true, + 'penetrator': true, + }, + { + 'result': 4, + 'nsides': 4, + 'dieType': 'polyhedral', + 'discarded': true, + 'penetrator': true, + }, + { + 'result': 4, + 'nsides': 4, + 'dieType': 'polyhedral', + 'discarded': true, + 'penetrator': true, + }, + { + 'result': 1, + 'nsides': 4, + 'dieType': 'polyhedral', + 'discarded': true, + 'penetrator': true, + }, + { + 'result': -6, + 'nsides': 1, + 'potentialValues': [-6], + 'dieType': 'singleVal', + 'discarded': true, + 'penetrator': true, + }, + ], + 'detailedResults': { + 'expression': '(9d6p4)', + 'opType': 'rollPenetration', + 'results': [ + { + 'result': 8, + 'nsides': 6, + 'dieType': 'polyhedral', + 'penetrated': true, + }, + {'result': 2, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 1, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 5, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 3, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 5, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 1, 'nsides': 6, 'dieType': 'polyhedral'}, + {'result': 4, 'nsides': 6, 'dieType': 'polyhedral'}, + { + 'result': 21, + 'nsides': 6, + 'dieType': 'polyhedral', + 'penetrated': true, + }, + ], + 'discarded': [ + { + 'result': 6, + 'nsides': 6, + 'dieType': 'polyhedral', + 'discarded': true, + 'penetrator': true, + }, + { + 'result': 3, + 'nsides': 4, + 'dieType': 'polyhedral', + 'discarded': true, + 'penetrator': true, + }, + { + 'result': -1, + 'nsides': 1, + 'potentialValues': [-1], + 'dieType': 'singleVal', + 'discarded': true, + 'penetrator': true, + }, + { + 'result': 6, + 'nsides': 6, + 'dieType': 'polyhedral', + 'discarded': true, + 'penetrator': true, + }, + { + 'result': 4, + 'nsides': 4, + 'dieType': 'polyhedral', + 'discarded': true, + 'penetrator': true, + }, + { + 'result': 4, + 'nsides': 4, + 'dieType': 'polyhedral', + 'discarded': true, + 'penetrator': true, + }, + { + 'result': 4, + 'nsides': 4, + 'dieType': 'polyhedral', + 'discarded': true, + 'penetrator': true, + }, + { + 'result': 4, + 'nsides': 4, + 'dieType': 'polyhedral', + 'discarded': true, + 'penetrator': true, + }, + { + 'result': 4, + 'nsides': 4, + 'dieType': 'polyhedral', + 'discarded': true, + 'penetrator': true, + }, + { + 'result': 1, + 'nsides': 4, + 'dieType': 'polyhedral', + 'discarded': true, + 'penetrator': true, + }, + { + 'result': -6, + 'nsides': 1, + 'potentialValues': [-6], + 'dieType': 'singleVal', + 'discarded': true, + 'penetrator': true, + }, + ], + 'total': 50, + }, + }), + ); + }); + + test('rollN test', () async { + final dice = DiceExpression.create( + '2d6', + roller: RNGRoller(seededRandom), + ); + + final results = await dice + .rollN(2) + .map((result) => result.total) + .toList(); + // mocked responses should return rolls of 6, 2, 1, 5 + expect(results, equals([8, 6])); + }); + + test('stats test', () async { + final dice = DiceExpression.create( + '2d6', + roller: RNGRoller(seededRandom), + ); + + final stats = await dice.stats(num: 100); + + expect( + stats, + equals({ + 'mean': 6.65, + 'stddev': 2.35, + 'min': 2, + 'max': 12, + 'count': 100, + 'histogram': { + 2: 3, + 3: 6, + 4: 12, + 5: 10, + 6: 20, + 7: 10, + 8: 18, + 9: 9, + 10: 7, + 11: 2, + 12: 3, + }, + }), + ); + }); + }); +} diff --git a/example/main.dart b/packages/dicecli/bin/dicecli.dart similarity index 91% rename from example/main.dart rename to packages/dicecli/bin/dicecli.dart index 46b52fc..43827d8 100644 --- a/example/main.dart +++ b/packages/dicecli/bin/dicecli.dart @@ -26,7 +26,7 @@ void main(List arguments) async { var random = Random.secure(); DiceExpression.registerListener((rollResult) { - //stdout.writeln('${rollResult.opType.name} -> $rollResult'); + //stdout.writeln('cb: ${rollResult.opType.name} -> $rollResult'); }); final argParser = ArgParser() @@ -110,9 +110,11 @@ void main(List arguments) async { try { final collectStats = results['stats'] as bool; if (collectStats) { + // if the user wants to collect stats, always use a non-secure RNG + // (aka fast and non-seeded) random = Random(); } - final diceExpr = DiceExpression.create(input, random); + final diceExpr = DiceExpression.create(input, roller: RNGRoller(random)); exit( await run( @@ -135,8 +137,9 @@ Future run({ required String output, }) async { if (stats) { - final stats = - await expression.stats(num: numRolls == 1 ? defaultStatsNum : numRolls); + final stats = await expression.stats( + num: numRolls == 1 ? defaultStatsNum : numRolls, + ); stdout.writeln(stats); } else { await for (final r in expression.rollN(numRolls)) { diff --git a/packages/dicecli/pubspec.yaml b/packages/dicecli/pubspec.yaml new file mode 100644 index 0000000..c94eb31 --- /dev/null +++ b/packages/dicecli/pubspec.yaml @@ -0,0 +1,24 @@ +name: dicecli +description: A CLI tool +version: 8.0.0 +homepage: https://github.com/Adventuresmith/dart-dice-parser +repository: https://github.com/Adventuresmith/dart-dice-parser + +publish_to: none +resolution: workspace + +environment: + sdk: "^3.8.0" + +executables: + dicecli: + +dependencies: + args: ^2.0.0 + dart_dice_parser: + path: ../dart_dice_parser + io: ^1.0.0 + logging: ^1.3.0 + +dev_dependencies: + lint: ^2.8.0 diff --git a/packages/diceui/.gitignore b/packages/diceui/.gitignore new file mode 100644 index 0000000..79c113f --- /dev/null +++ b/packages/diceui/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/diceui/.metadata b/packages/diceui/.metadata new file mode 100644 index 0000000..fdb4416 --- /dev/null +++ b/packages/diceui/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "fcf2c11572af6f390246c056bc905eca609533a0" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: fcf2c11572af6f390246c056bc905eca609533a0 + base_revision: fcf2c11572af6f390246c056bc905eca609533a0 + - platform: android + create_revision: fcf2c11572af6f390246c056bc905eca609533a0 + base_revision: fcf2c11572af6f390246c056bc905eca609533a0 + - platform: ios + create_revision: fcf2c11572af6f390246c056bc905eca609533a0 + base_revision: fcf2c11572af6f390246c056bc905eca609533a0 + - platform: linux + create_revision: fcf2c11572af6f390246c056bc905eca609533a0 + base_revision: fcf2c11572af6f390246c056bc905eca609533a0 + - platform: macos + create_revision: fcf2c11572af6f390246c056bc905eca609533a0 + base_revision: fcf2c11572af6f390246c056bc905eca609533a0 + - platform: web + create_revision: fcf2c11572af6f390246c056bc905eca609533a0 + base_revision: fcf2c11572af6f390246c056bc905eca609533a0 + - platform: windows + create_revision: fcf2c11572af6f390246c056bc905eca609533a0 + base_revision: fcf2c11572af6f390246c056bc905eca609533a0 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/diceui/README.md b/packages/diceui/README.md new file mode 100644 index 0000000..e813d48 --- /dev/null +++ b/packages/diceui/README.md @@ -0,0 +1,16 @@ +# diceui + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/diceui/analysis_options.yaml b/packages/diceui/analysis_options.yaml new file mode 100644 index 0000000..af5e7f4 --- /dev/null +++ b/packages/diceui/analysis_options.yaml @@ -0,0 +1,57 @@ +# This file configures the analyzer to use the lint rule set from `package:lint` + +#include: package:lint/strict.yaml # For production apps +# include: package:lint/casual.yaml # For code samples, hackathons and other non-production code +include: package:lint/package.yaml # Use this for packages with public API + + +# You might want to exclude auto-generated files from dart analysis +analyzer: + + language: + strict-casts: true + strict-raw-types: true + exclude: + - packages/diceui/** + #- '**.freezed.dart' + #- '**.g.dart' + + errors: + always_use_package_imports: false + + +# You can customize the lint rules set to your own liking. A list of all rules +# can be found at https://dart-lang.github.io/linter/lints/options/options.html +linter: + rules: + - annotate_redeclares + - avoid_dynamic_calls + - avoid_final_parameters + - avoid_print + - avoid_unused_constructor_parameters + - combinators_ordering + - comment_references + - directives_ordering + - invalid_case_patterns + - leading_newlines_in_multiline_strings + - missing_code_block_language_in_doc_comment + - no_self_assignments + - omit_local_variable_types + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + - prefer_expression_function_bodies + - prefer_final_in_for_each + - prefer_final_locals + - prefer_if_elements_to_conditional_expressions + - prefer_relative_imports + - prefer_single_quotes + - unnecessary_await_in_return + - unnecessary_breaks + - unnecessary_lambdas + - unnecessary_null_aware_operator_on_extension_on_nullable + - unnecessary_null_checks + - unnecessary_parenthesis + - unnecessary_statements + - unreachable_from_main diff --git a/packages/diceui/android/.gitignore b/packages/diceui/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/packages/diceui/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/packages/diceui/android/app/build.gradle.kts b/packages/diceui/android/app/build.gradle.kts new file mode 100644 index 0000000..9f50570 --- /dev/null +++ b/packages/diceui/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.diceui" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.diceui" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/packages/diceui/android/app/src/debug/AndroidManifest.xml b/packages/diceui/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/packages/diceui/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/diceui/android/app/src/main/AndroidManifest.xml b/packages/diceui/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0005d06 --- /dev/null +++ b/packages/diceui/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/diceui/android/app/src/main/kotlin/com/example/diceui/MainActivity.kt b/packages/diceui/android/app/src/main/kotlin/com/example/diceui/MainActivity.kt new file mode 100644 index 0000000..589a7bd --- /dev/null +++ b/packages/diceui/android/app/src/main/kotlin/com/example/diceui/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.diceui + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/packages/diceui/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/diceui/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/packages/diceui/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/diceui/android/app/src/main/res/drawable/launch_background.xml b/packages/diceui/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/packages/diceui/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/diceui/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/diceui/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/packages/diceui/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/diceui/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/diceui/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/packages/diceui/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/diceui/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/diceui/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/packages/diceui/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/diceui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/diceui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/packages/diceui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/diceui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/diceui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/packages/diceui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/diceui/android/app/src/main/res/values-night/styles.xml b/packages/diceui/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/packages/diceui/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/diceui/android/app/src/main/res/values/styles.xml b/packages/diceui/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/packages/diceui/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/diceui/android/app/src/profile/AndroidManifest.xml b/packages/diceui/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/packages/diceui/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/diceui/android/build.gradle.kts b/packages/diceui/android/build.gradle.kts new file mode 100644 index 0000000..89176ef --- /dev/null +++ b/packages/diceui/android/build.gradle.kts @@ -0,0 +1,21 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/packages/diceui/android/gradle.properties b/packages/diceui/android/gradle.properties new file mode 100644 index 0000000..f018a61 --- /dev/null +++ b/packages/diceui/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/packages/diceui/android/gradle/wrapper/gradle-wrapper.properties b/packages/diceui/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ac3b479 --- /dev/null +++ b/packages/diceui/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/packages/diceui/android/settings.gradle.kts b/packages/diceui/android/settings.gradle.kts new file mode 100644 index 0000000..ab39a10 --- /dev/null +++ b/packages/diceui/android/settings.gradle.kts @@ -0,0 +1,25 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.7.3" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/packages/diceui/ios/.gitignore b/packages/diceui/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/packages/diceui/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packages/diceui/ios/Flutter/AppFrameworkInfo.plist b/packages/diceui/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/packages/diceui/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/packages/diceui/ios/Flutter/Debug.xcconfig b/packages/diceui/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/packages/diceui/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/packages/diceui/ios/Flutter/Release.xcconfig b/packages/diceui/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/packages/diceui/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/packages/diceui/ios/Runner.xcodeproj/project.pbxproj b/packages/diceui/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..51284f4 --- /dev/null +++ b/packages/diceui/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,616 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.diceui; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.diceui.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.diceui.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.diceui.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.diceui; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.diceui; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/diceui/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/diceui/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/packages/diceui/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/diceui/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/diceui/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/packages/diceui/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/diceui/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/diceui/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/packages/diceui/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/diceui/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/diceui/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/packages/diceui/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/diceui/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/diceui/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/packages/diceui/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/diceui/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/diceui/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/packages/diceui/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/diceui/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/diceui/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/packages/diceui/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/diceui/ios/Runner/AppDelegate.swift b/packages/diceui/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/packages/diceui/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/packages/diceui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/diceui/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/diceui/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/packages/diceui/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/diceui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/diceui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/packages/diceui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/diceui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/diceui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/packages/diceui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/diceui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/diceui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/packages/diceui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/diceui/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/diceui/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/packages/diceui/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/diceui/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/diceui/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/packages/diceui/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/diceui/ios/Runner/Base.lproj/Main.storyboard b/packages/diceui/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/packages/diceui/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/diceui/ios/Runner/Info.plist b/packages/diceui/ios/Runner/Info.plist new file mode 100644 index 0000000..2551497 --- /dev/null +++ b/packages/diceui/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Diceui + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + diceui + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/packages/diceui/ios/Runner/Runner-Bridging-Header.h b/packages/diceui/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/packages/diceui/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/packages/diceui/ios/RunnerTests/RunnerTests.swift b/packages/diceui/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/packages/diceui/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/packages/diceui/lib/main.dart b/packages/diceui/lib/main.dart new file mode 100644 index 0000000..7b7f5b6 --- /dev/null +++ b/packages/diceui/lib/main.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + // This is the theme of your application. + // + // TRY THIS: Try running your application with "flutter run". You'll see + // the application has a purple toolbar. Then, without quitting the app, + // try changing the seedColor in the colorScheme below to Colors.green + // and then invoke "hot reload" (save your changes or press the "hot + // reload" button in a Flutter-supported IDE, or press "r" if you used + // the command line to start the app). + // + // Notice that the counter didn't reset back to zero; the application + // state is not lost during the reload. To reset the state, use hot + // restart instead. + // + // This works for code too, not just values: Most code changes can be + // tested with just a hot reload. + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + ), + home: const MyHomePage(title: 'Flutter Demo Home Page'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, required this.title}); + + // This widget is the home page of your application. It is stateful, meaning + // that it has a State object (defined below) that contains fields that affect + // how it looks. + + // This class is the configuration for the state. It holds the values (in this + // case the title) provided by the parent (in this case the App widget) and + // used by the build method of the State. Fields in a Widget subclass are + // always marked "final". + + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + int _counter = 0; + + void _incrementCounter() { + setState(() { + // This call to setState tells the Flutter framework that something has + // changed in this State, which causes it to rerun the build method below + // so that the display can reflect the updated values. If we changed + // _counter without calling setState(), then the build method would not be + // called again, and so nothing would appear to happen. + _counter++; + }); + } + + @override + Widget build(BuildContext context) { + // This method is rerun every time setState is called, for instance as done + // by the _incrementCounter method above. + // + // The Flutter framework has been optimized to make rerunning build methods + // fast, so that you can just rebuild anything that needs updating rather + // than having to individually change instances of widgets. + return Scaffold( + appBar: AppBar( + // TRY THIS: Try changing the color here to a specific color (to + // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar + // change color while the other colors stay the same. + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + // Here we take the value from the MyHomePage object that was created by + // the App.build method, and use it to set our appbar title. + title: Text(widget.title), + ), + body: Center( + // Center is a layout widget. It takes a single child and positions it + // in the middle of the parent. + child: Column( + // Column is also a layout widget. It takes a list of children and + // arranges them vertically. By default, it sizes itself to fit its + // children horizontally, and tries to be as tall as its parent. + // + // Column has various properties to control how it sizes itself and + // how it positions its children. Here we use mainAxisAlignment to + // center the children vertically; the main axis here is the vertical + // axis because Columns are vertical (the cross axis would be + // horizontal). + // + // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" + // action in the IDE, or press "p" in the console), to see the + // wireframe for each widget. + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('You have pushed the button this many times:'), + Text( + '$_counter', + style: Theme.of(context).textTheme.headlineMedium, + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: const Icon(Icons.add), + ), // This trailing comma makes auto-formatting nicer for build methods. + ); + } +} diff --git a/packages/diceui/linux/.gitignore b/packages/diceui/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/packages/diceui/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/packages/diceui/linux/CMakeLists.txt b/packages/diceui/linux/CMakeLists.txt new file mode 100644 index 0000000..851f789 --- /dev/null +++ b/packages/diceui/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "diceui") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.diceui") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/packages/diceui/linux/flutter/CMakeLists.txt b/packages/diceui/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/packages/diceui/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/packages/diceui/linux/flutter/generated_plugin_registrant.cc b/packages/diceui/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..e71a16d --- /dev/null +++ b/packages/diceui/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/packages/diceui/linux/flutter/generated_plugin_registrant.h b/packages/diceui/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/packages/diceui/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/diceui/linux/flutter/generated_plugins.cmake b/packages/diceui/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..2e1de87 --- /dev/null +++ b/packages/diceui/linux/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/diceui/linux/runner/CMakeLists.txt b/packages/diceui/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/packages/diceui/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/packages/diceui/linux/runner/main.cc b/packages/diceui/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/packages/diceui/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/packages/diceui/linux/runner/my_application.cc b/packages/diceui/linux/runner/my_application.cc new file mode 100644 index 0000000..4bc6103 --- /dev/null +++ b/packages/diceui/linux/runner/my_application.cc @@ -0,0 +1,130 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "diceui"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "diceui"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/packages/diceui/linux/runner/my_application.h b/packages/diceui/linux/runner/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/packages/diceui/linux/runner/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/packages/diceui/macos/.gitignore b/packages/diceui/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/packages/diceui/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/packages/diceui/macos/Flutter/Flutter-Debug.xcconfig b/packages/diceui/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/packages/diceui/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/diceui/macos/Flutter/Flutter-Release.xcconfig b/packages/diceui/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/packages/diceui/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/diceui/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/diceui/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..cccf817 --- /dev/null +++ b/packages/diceui/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,10 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { +} diff --git a/packages/diceui/macos/Runner.xcodeproj/project.pbxproj b/packages/diceui/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..af41526 --- /dev/null +++ b/packages/diceui/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,705 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* diceui.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "diceui.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* diceui.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* diceui.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.diceui.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/diceui.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/diceui"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.diceui.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/diceui.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/diceui"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.diceui.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/diceui.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/diceui"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/packages/diceui/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/diceui/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/packages/diceui/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/diceui/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/diceui/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..fbf7999 --- /dev/null +++ b/packages/diceui/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/diceui/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/diceui/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/packages/diceui/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/diceui/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/diceui/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/packages/diceui/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/diceui/macos/Runner/AppDelegate.swift b/packages/diceui/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/packages/diceui/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/packages/diceui/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/diceui/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/packages/diceui/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/diceui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/diceui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/packages/diceui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/packages/diceui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/diceui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/packages/diceui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/packages/diceui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/diceui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/packages/diceui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/packages/diceui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/diceui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/packages/diceui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/packages/diceui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/diceui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/packages/diceui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/packages/diceui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/diceui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/packages/diceui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/packages/diceui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/diceui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/packages/diceui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/packages/diceui/macos/Runner/Base.lproj/MainMenu.xib b/packages/diceui/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/packages/diceui/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/diceui/macos/Runner/Configs/AppInfo.xcconfig b/packages/diceui/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..b7fb38d --- /dev/null +++ b/packages/diceui/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = diceui + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.diceui + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/packages/diceui/macos/Runner/Configs/Debug.xcconfig b/packages/diceui/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/packages/diceui/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/diceui/macos/Runner/Configs/Release.xcconfig b/packages/diceui/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/packages/diceui/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/diceui/macos/Runner/Configs/Warnings.xcconfig b/packages/diceui/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/packages/diceui/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/diceui/macos/Runner/DebugProfile.entitlements b/packages/diceui/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/packages/diceui/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/packages/diceui/macos/Runner/Info.plist b/packages/diceui/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/packages/diceui/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/packages/diceui/macos/Runner/MainFlutterWindow.swift b/packages/diceui/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/packages/diceui/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/packages/diceui/macos/Runner/Release.entitlements b/packages/diceui/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/packages/diceui/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/packages/diceui/macos/RunnerTests/RunnerTests.swift b/packages/diceui/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/packages/diceui/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/packages/diceui/pubspec.yaml b/packages/diceui/pubspec.yaml new file mode 100644 index 0000000..2145c7f --- /dev/null +++ b/packages/diceui/pubspec.yaml @@ -0,0 +1,88 @@ +name: diceui +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev +#resolution: workspace + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.8.0 + flutter: ">=3.32.0 <4.0.0" + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + dart_dice_parser: + path: ../dart_dice_parser + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + + lint: ^2.8.0 + + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/packages/diceui/test/widget_test.dart b/packages/diceui/test/widget_test.dart new file mode 100644 index 0000000..1653355 --- /dev/null +++ b/packages/diceui/test/widget_test.dart @@ -0,0 +1,29 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:diceui/main.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/packages/diceui/web/favicon.png b/packages/diceui/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/packages/diceui/web/favicon.png differ diff --git a/packages/diceui/web/icons/Icon-192.png b/packages/diceui/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/packages/diceui/web/icons/Icon-192.png differ diff --git a/packages/diceui/web/icons/Icon-512.png b/packages/diceui/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/packages/diceui/web/icons/Icon-512.png differ diff --git a/packages/diceui/web/icons/Icon-maskable-192.png b/packages/diceui/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/packages/diceui/web/icons/Icon-maskable-192.png differ diff --git a/packages/diceui/web/icons/Icon-maskable-512.png b/packages/diceui/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/packages/diceui/web/icons/Icon-maskable-512.png differ diff --git a/packages/diceui/web/index.html b/packages/diceui/web/index.html new file mode 100644 index 0000000..2ff1ff5 --- /dev/null +++ b/packages/diceui/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + diceui + + + + + + diff --git a/packages/diceui/web/manifest.json b/packages/diceui/web/manifest.json new file mode 100644 index 0000000..78da035 --- /dev/null +++ b/packages/diceui/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "diceui", + "short_name": "diceui", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/packages/diceui/windows/.gitignore b/packages/diceui/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/packages/diceui/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/packages/diceui/windows/CMakeLists.txt b/packages/diceui/windows/CMakeLists.txt new file mode 100644 index 0000000..2fb11df --- /dev/null +++ b/packages/diceui/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(diceui LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "diceui") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/packages/diceui/windows/flutter/CMakeLists.txt b/packages/diceui/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/packages/diceui/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/packages/diceui/windows/flutter/generated_plugin_registrant.cc b/packages/diceui/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..8b6d468 --- /dev/null +++ b/packages/diceui/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void RegisterPlugins(flutter::PluginRegistry* registry) { +} diff --git a/packages/diceui/windows/flutter/generated_plugin_registrant.h b/packages/diceui/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/packages/diceui/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/diceui/windows/flutter/generated_plugins.cmake b/packages/diceui/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..b93c4c3 --- /dev/null +++ b/packages/diceui/windows/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/diceui/windows/runner/CMakeLists.txt b/packages/diceui/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/packages/diceui/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/packages/diceui/windows/runner/Runner.rc b/packages/diceui/windows/runner/Runner.rc new file mode 100644 index 0000000..f74a528 --- /dev/null +++ b/packages/diceui/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "diceui" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "diceui" "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "diceui.exe" "\0" + VALUE "ProductName", "diceui" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/packages/diceui/windows/runner/flutter_window.cpp b/packages/diceui/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/packages/diceui/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/packages/diceui/windows/runner/flutter_window.h b/packages/diceui/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/packages/diceui/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/packages/diceui/windows/runner/main.cpp b/packages/diceui/windows/runner/main.cpp new file mode 100644 index 0000000..97a494e --- /dev/null +++ b/packages/diceui/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"diceui", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/packages/diceui/windows/runner/resource.h b/packages/diceui/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/packages/diceui/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/packages/diceui/windows/runner/resources/app_icon.ico b/packages/diceui/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/packages/diceui/windows/runner/resources/app_icon.ico differ diff --git a/packages/diceui/windows/runner/runner.exe.manifest b/packages/diceui/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/packages/diceui/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/packages/diceui/windows/runner/utils.cpp b/packages/diceui/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/packages/diceui/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/packages/diceui/windows/runner/utils.h b/packages/diceui/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/packages/diceui/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/diceui/windows/runner/win32_window.cpp b/packages/diceui/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/packages/diceui/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/packages/diceui/windows/runner/win32_window.h b/packages/diceui/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/packages/diceui/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/pubspec.yaml b/pubspec.yaml index 56a4368..9df8932 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,21 +1,9 @@ -name: dart_dice_parser -description: A dart library for parsing dice notation (`2d6+4`). Supports advantage/disadvantage, exploding die, and other variations. -version: 7.1.1 -homepage: https://github.com/Adventuresmith/dart-dice-parser -repository: https://github.com/Adventuresmith/dart-dice-parser - +name: dart_dice_parser_repo +publish_to: none environment: - sdk: "^3.6.0" - -dependencies: - collection: ^1.0.0 - equatable: ^2.0.0 - logging: ^1.3.0 - petitparser: ^6.0.0 - -dev_dependencies: - args: ^2.6.0 - io: ^1.0.0 - lint: ^2.0.0 - mocktail: ^1.0.0 - test: ^1.25.0 + sdk: "^3.8.0" + #flutter: ">=3.32.0 <4.0.0" +workspace: + - packages/dart_dice_parser + - packages/dicecli + #- packages/diceui diff --git a/test/dart_dice_parser_test.dart b/test/dart_dice_parser_test.dart deleted file mode 100644 index ab65891..0000000 --- a/test/dart_dice_parser_test.dart +++ /dev/null @@ -1,844 +0,0 @@ -import 'dart:math'; - -import 'package:dart_dice_parser/dart_dice_parser.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:test/test.dart'; - -class MockRandom extends Mock implements Random {} - -void main() { - late Random staticMockRandom; - late Random seededRandom; - - setUp(() { - // first 100 seeded rolls for d6 - // [6, 2, 1, 5, 3, 5, 1, 4, 6, 5, 6, 4, 2, 4, 2, 3, 5, 1, 1, 2, 4, 1, 6, 2, 2, 5, 6, 3, 1, 3, 6, 1, 2, 3, 6, 2, 1, 1, 1, 3, 1, 2, 3, 3, 6, 2, 5, 4, 3, 4, 1, 5, 4, 4, 2, 6, 5, 4, 6, 2, 3, 1, 4, 5, 3, 2, 2, 6, 6, 4, 4, 2, 6, 2, 5, 3, 3, 4, 4, 2, 2, 4, 3, 2, 6, 6, 4, 6, 4, 4, 3, 1, 4, 2, 2, 4, 3, 3, 1, 3] - seededRandom = Random(1234); - staticMockRandom = MockRandom(); - // NOTE: this mocks the random number generator to always return '1' - // -- that means the dice-roll is '2' (since rolls are 1-based) - when( - () => staticMockRandom.nextInt(any()), - ).thenReturn(1); - }); - void staticRandTest(String name, String input, int expectedTotal) { - test('$name - $input', () { - expect( - DiceExpression.create(input, staticMockRandom).roll().total, - equals(expectedTotal), - ); - }); - } - - void seededRandTest( - String testName, - String inputExpr, - int? expectedTotal, { - List? expectedResults, - RollMetadata? expectedMetadata, - int? successCount, - int? failureCount, - int? critSuccessCount, - int? critFailureCount, - }) { - test('$testName - $inputExpr', () { - final rollSummary = DiceExpression.create(inputExpr, seededRandom).roll(); - if (expectedTotal != null) { - expect( - rollSummary.total, - equals(expectedTotal), - reason: 'mismatching total', - ); - } - if (expectedResults != null) { - expect( - rollSummary.results, - equals(expectedResults), - reason: 'mismatching results', - ); - } - if (expectedMetadata != null) { - expect( - rollSummary.metadata, - equals(expectedMetadata), - reason: 'mismatching roll metadata', - ); - } - if (successCount != null) { - expect( - rollSummary.hasSuccesses, - equals(successCount > 0), - reason: 'summary missing success', - ); - expect( - rollSummary.metadata.score.successCount, - equals(successCount), - reason: 'mismatched success count', - ); - } - if (failureCount != null) { - expect( - rollSummary.hasFailures, - equals(failureCount > 0), - reason: 'summary missing success', - ); - expect( - rollSummary.metadata.score.failureCount, - equals(failureCount), - reason: 'mismatched success count', - ); - } - if (critSuccessCount != null) { - expect( - rollSummary.hasCritSuccesses, - equals(critSuccessCount > 0), - reason: 'summary missing success', - ); - expect( - rollSummary.metadata.score.critSuccessCount, - equals(critSuccessCount), - reason: 'mismatched success count', - ); - } - if (critFailureCount != null) { - expect( - rollSummary.hasCritFailures, - equals(critFailureCount > 0), - reason: 'summary missing success', - ); - expect( - rollSummary.metadata.score.critFailureCount, - equals(critFailureCount), - reason: 'mismatched success count', - ); - } - }); - } - - group('arithmetic', () { - seededRandTest('addition', '1+20', 21); - seededRandTest('multi', '3*2', 6); - seededRandTest('parens', '(5+6)*2', 22); - seededRandTest('order of operations', '5+6*2', 17); - seededRandTest('subtraction', '5-6', -1); - seededRandTest('subtraction', '5-6', -1); - seededRandTest('subtraction', '1-', 1); - seededRandTest('subtraction', '1-0', 1); - seededRandTest('subtraction', '0-1', -1); - seededRandTest('subtraction', '-1', -1); - seededRandTest('negative number', '-6', -6); // this will be 0-6 - }); - - group('dice and arith', () { - seededRandTest('dice', '4d6', 14, expectedResults: [6, 2, 1, 5]); - seededRandTest('dice+', '4d6+2', 16, expectedResults: [6, 2, 1, 5, 2]); - seededRandTest('dice*', '4d6*2', 28, expectedResults: [28]); - }); - - group('successes and failures', () { - // count s=nsides, f=1 - seededRandTest( - 'defaults are 1 or ndice', - '4d6#s#f#cs#cf', - 14, - expectedResults: const [6, 2, 1, 5], - expectedMetadata: const RollMetadata( - rolled: [6, 2, 1, 5], - score: RollScore( - successes: [6], - failures: [1], - critSuccesses: [6], - critFailures: [1], - ), - ), - successCount: 1, - failureCount: 1, - critSuccessCount: 1, - critFailureCount: 1, - ); - seededRandTest( - 'equal sign can be omitted', - '4d6#s6#f1', - 14, - expectedResults: const [6, 2, 1, 5], - expectedMetadata: const RollMetadata( - rolled: [6, 2, 1, 5], - score: RollScore( - successes: [6], - failures: [1], - ), - ), - successCount: 1, - failureCount: 1, - critSuccessCount: 0, - critFailureCount: 0, - ); - seededRandTest( - 'with equal sign', - '4d6#s=6#f=1', - 14, - expectedResults: [6, 2, 1, 5], - expectedMetadata: const RollMetadata( - rolled: [6, 2, 1, 5], - score: RollScore( - successes: [6], - failures: [1], - ), - ), - successCount: 1, - failureCount: 1, - critSuccessCount: 0, - critFailureCount: 0, - ); - - seededRandTest( - 'dice', - '4d6#s>4#f<=2#cs>5#cf<2', - 14, - expectedResults: [6, 2, 1, 5], - expectedMetadata: const RollMetadata( - rolled: [6, 2, 1, 5], - score: RollScore( - successes: [6, 5], - failures: [2, 1], - critSuccesses: [6], - critFailures: [1], - ), - ), - successCount: 2, - failureCount: 2, - critSuccessCount: 1, - critFailureCount: 1, - ); - - seededRandTest( - 'dice', - '4d6#s>=4#f<2', - 14, - expectedResults: [6, 2, 1, 5], - expectedMetadata: const RollMetadata( - rolled: [6, 2, 1, 5], - score: RollScore( - successes: [6, 5], - failures: [1], - ), - ), - ); - - seededRandTest( - 'success low, failures high', - '4d6#s<2#f>5', - 14, - expectedResults: [6, 2, 1, 5], - expectedMetadata: const RollMetadata( - rolled: [6, 2, 1, 5], - score: RollScore( - successes: [1], - failures: [6], - ), - ), - ); - seededRandTest( - 'critical success is also a success', - '4d6 #s>=5 #cs=6 #f=1', - 14, - expectedResults: [6, 2, 1, 5], - expectedMetadata: const RollMetadata( - rolled: [6, 2, 1, 5], - score: RollScore( - successes: [6, 5], - failures: [1], - critSuccesses: [6], - ), - ), - ); - - seededRandTest( - 'critical failure is also a failure', - '4d6 #s>=5 #cs=6 #f<=2 #cf', - 14, - expectedResults: [6, 2, 1, 5], - expectedMetadata: const RollMetadata( - rolled: [6, 2, 1, 5], - score: RollScore( - successes: [6, 5], - failures: [2, 1], - critSuccesses: [6], - critFailures: [1], - ), - ), - ); - }); - - group('rollVals', () { - seededRandTest( - '4d6 equivalent', - '4d[1,2,3,4,5,6]', - 14, - expectedResults: [6, 2, 1, 5], - ); - seededRandTest( - '4d6 equivalent - with whitespace', - '4 d \n[1,2 ,3,4,5,6]', - 14, - expectedResults: [6, 2, 1, 5], - ); - - seededRandTest( - '4d6 equivalent -- with negatives', - '4d[-1,2,3,4,5,-6]', - 0, - expectedResults: [-6, 2, -1, 5], - ); - }); - - group('counting operations', () { - // mocked responses should return rolls of 6, 2, 1, 5 - seededRandTest('count >', '4d6#>3', 2); - seededRandTest('count <', '4d6#<6', 3); - seededRandTest('count =', '4d6#=1', 1); - seededRandTest('count <=', '4d6#<=2', 2); - seededRandTest('count >=', '4d6#>=6', 1); - seededRandTest('count > (missing from result)', '4d6#>6', 0); - seededRandTest('count #', '4d6#', 4); - seededRandTest('count # after drop', '4d6-<2#', 3); - seededRandTest('count # after drop', '4d6#1', 1); - seededRandTest('count # after drop', '4d6#=1', 1); - seededRandTest('count arith result', '(4d6+1)#1', 2); - - // 1234 seed will return [1, -1, -1, 1, 0, 1] - seededRandTest('count fudge', '6dF#', 6); - seededRandTest('count fudge', '6dF#=1', 3); - seededRandTest('count fudge', '6dF#=0', 1); - seededRandTest('count fudge', '6dF#<0', 2); - seededRandTest('count fudge', '6dF#>0', 3); - seededRandTest('count', '4d6#', 4); - seededRandTest('count', '4d6#6', 1); - - final invalids = [ - '4d6#=', - '4d6#<=', - '4d6#>=', - '4d6#>', - '4d6#<', - '4d6-=', - '4d6 C=', - '4d6 r=', - '4d6 ro=', - ]; - for (final v in invalids) { - test('invalid count - $v', () { - expect( - () => DiceExpression.create(v).roll(), - throwsFormatException, - ); - }); - } - }); - - group('keep high/low', () { - // mocked responses should return rolls of 6, 2, 1, 5 - seededRandTest('keep low missing rhs', '4d6kl', 1); - seededRandTest('keep low', '4d6kl2', 3); - seededRandTest('keep low', '4d6kl3', 8); - seededRandTest('keep high missing rhs', '4d6kh', 6); - seededRandTest('keep high', '4d6kh2', 11); - seededRandTest('keep high', '4d6kh3', 13); - seededRandTest('keep high missing rhs', '4d6k', 6); - seededRandTest('keep high', '4d6k2', 11); - seededRandTest('keep high', '4d6k3', 13); - }); - - group('roll modifiers - drop, clamp, etc', () { - // mocked responses should return rolls of 6, 2, 1, 5 - seededRandTest('drop high', '4d6-H', 8); - seededRandTest('drop high (lowercase)', '4d6-h', 8); - seededRandTest('drop high (1)', '4d6-h1', 8); - seededRandTest('drop high (3)', '4d6-h3', 1); - seededRandTest('drop low', '4d6-L', 13); - seededRandTest('drop add result', '(4d6+1)-L', 14); - seededRandTest('drop add result', '1-L', 0); - seededRandTest('drop low (lower)', '4d6-l', 13); - seededRandTest('drop low - 1', '4d6-l1', 13); - seededRandTest('drop low - 3', '4d6-l3', 6); - seededRandTest('drop low and high', '4d6-L-H', 7); - seededRandTest('can drop more than rolled', '3d6-H4', 0); - seededRandTest('can drop more than rolled', '3d6-l4', 0); - seededRandTest('can drop arith result', '(2d6+3d6)-L1', 16); - seededRandTest( - 'can drop arith result -- diff dice sides', - '(2d6+3d4)-L1', - 14, - ); - seededRandTest('drop', '4d6->3', 3); - seededRandTest('drop', '4d6-<3', 11); - seededRandTest('drop', '4d6->=2', 1); - seededRandTest('drop', '4d6-<=2', 11); - seededRandTest('drop', '4d6-=2', 12); - seededRandTest('drop (not in results)', '4d6-=4', 14); - seededRandTest('clamp', '4d6C>3', 9); - seededRandTest('clamp', '4d6C<3', 17); - seededRandTest('clamp', '4d6c>3', 9); - seededRandTest('clamp', '4d6c<3', 17); - seededRandTest('clamp', '1 C<1', 1); - // rolls [1,-1,-1,1] , -1s turned to 0 - seededRandTest('clamp', '4dF C<0', 2); - - // mocked responses should return rolls of 6, 2, 1, 5, 3 - // [6,2] + [1,5,3] = [6,2,1,5,3]-L3 => [6,5] = 9 - seededRandTest('drop low on aggregated dice', '(2d6+3d6)-L3', 11); - - test('missing clamp target', () { - expect( - () => DiceExpression.create('6d6 C<', seededRandom).roll(), - throwsFormatException, - ); - }); - }); - - group('listeners', () { - test('basic', () { - final dice = DiceExpression.create('2d6 kh', seededRandom); - final results = []; - final summaries = []; - dice.roll( - onRoll: (rr) { - results.add(rr); - }, - onSummary: (rs) { - summaries.add(rs); - }, - ); - const rrRoll = RollResult( - expression: '(2d6)', - opType: OpType.rollDice, - nsides: 6, - ndice: 2, - results: [2, 6], - metadata: RollMetadata( - rolled: [2, 6], - ), - ); - const rrDrop = RollResult( - expression: '((2d6) kh )', - opType: OpType.drop, - nsides: 6, - ndice: 2, - results: [6], - metadata: RollMetadata( - discarded: [2], - ), - left: rrRoll, - ); - final expectedSummary = RollSummary(detailedResults: rrDrop); - expect( - results, - equals([ - rrRoll, - rrDrop, - ]), - ); - expect( - summaries, - equals([ - expectedSummary, - ]), - ); - }); - }); - - group('addition combines', () { - // mocked responses should return rolls of 6, 2, 1, 5 - seededRandTest( - 'addition combines results (drop is higher priority than plus)', - '3d6+1d6-L1', - 9, - ); - seededRandTest('addition combines results - parens', '(2d6+2d6)-L1', 13); - }); - - group('mult variations', () { - // mocked responses should return rolls of 6, 2, 1, 5 - seededRandTest('int mult on rhs', '2d6*2', 16); - seededRandTest('int mult on lhs', '2*2d6', 16); - seededRandTest('int mult on lhs', '(2*2d6)-l', 0); - }); - - group('missing ints', () { - seededRandTest('empty string returns zero', '', 0); - seededRandTest('empty arith returns zero - add', '+', 0); - seededRandTest('empty arith returns zero - mult', '*', 0); - seededRandTest('empty ndice is 1', 'd6', 6); - seededRandTest('whitespace should be swallowed', '2 d6', 8); - seededRandTest('whitespace should be swallowed', '2d 6', 8); - - test('missing nsides', () { - expect( - () => DiceExpression.create('6d', seededRandom).roll(), - throwsFormatException, - ); - }); - }); - group('metadata', () { - seededRandTest( - 'reroll, keep, count success,count fail, add', - '(((10d6 r=3)kh2 #s>5)#f<2)+2', - 14, - expectedResults: [6, 6, 2], - expectedMetadata: const RollMetadata( - rolled: [6, 2, 1, 5, 3, 5, 1, 4, 6, 5, 6], - discarded: [3, 6, 5, 5, 5, 4, 2, 1, 1], - score: RollScore( - successes: [6, 6], - ), - ), - ); - - seededRandTest( - 'score is determined from subtrees', - '(4d6#s<=2#f>=5) + 1', - 15, - expectedResults: [6, 2, 1, 5, 1], - expectedMetadata: const RollMetadata( - rolled: [6, 2, 1, 5], - score: RollScore( - // `+ 1` isn't included in success, since it's after parens - successes: [2, 1], - failures: [6, 5], - ), - ), - ); - - seededRandTest( - 'separate keeps', - '(((4d6 kh3) + (4d6 kh2))kh3)', - 16, - expectedResults: [6, 5, 5], - expectedMetadata: const RollMetadata( - rolled: [1, 2, 5, 6, 1, 3, 4, 5], - discarded: [1, 3, 1, 4, 2], - ), - ); - }); - - group('reroll', () { - seededRandTest('reroll', '10d4 r=3', 35); - seededRandTest('reroll', '10d4 r3', 35); - seededRandTest('reroll', '10d4 r<2', 33); - seededRandTest('reroll', '10d4 r>2', 16); - seededRandTest('reroll', '10d4 r<=3', 40); - seededRandTest('reroll', '10d4 r>=2', 10); - - seededRandTest('reroll', '10d4 ro=3', 35); - seededRandTest('reroll', '10d4 ro3', 35); - seededRandTest('reroll', '10d4 ro<2', 33); - seededRandTest('reroll', '10d4 ro>2', 26); - seededRandTest('reroll', '10d4 ro<=3', 34); - seededRandTest('reroll', '10d4 ro>=2', 27); - - seededRandTest('reroll once', '8d6r>3', 15); - seededRandTest('reroll once', '8d6ro>3', 28); - }); - - group('dice', () { - staticRandTest('order of operations, with dice', '5 + 6 * 2d6', 29); - - seededRandTest('simple roll', '1d6', 6); - seededRandTest('simple roll', 'd6', 6); - seededRandTest('percentile', '1d%', 96); - seededRandTest('percentile', 'd%', 96); - seededRandTest('D66', '1D66', 62); - seededRandTest('D66', 'D66', 62); - seededRandTest('d66 -- 66-sided, not D66', '1d66', 30); - seededRandTest('d66 -- 66-sided, not D66', 'd66', 30); - seededRandTest('ndice in parens', '(4+6)d10', 54); - seededRandTest('nsides in parens', '10d(2*3)', 38); - - seededRandTest('zero dice rolled', '0d6', 0); - - staticRandTest('dice expr as sides', '2d(3d6)', 4); - - seededRandTest('fudge', '4dF', 0); - seededRandTest('fudge', 'dF', 1); - seededRandTest('fudge', '1dF', 1); - - // 1st roll: 6, 2, 1, 5, 3, 5, 1, 4, 6, (explodes 2) (total 33) - // 2nd roll: 5,6 (explodes 1) (total 11) - // 3rd roll: 4 (explodes 0) (total 4) - seededRandTest('exploding dice', '9d6!', 48); - seededRandTest('exploding dice', '9d6!6', 48); - seededRandTest('exploding dice', '9d6!=6', 48); - seededRandTest('exploding dice', '9d6!>=6', 48); - seededRandTest('exploding dice', '9d6!>5', 48); - - seededRandTest('exploding dice', '9d6!o', 44); - seededRandTest('exploding dice', '9d6!o6', 44); - seededRandTest('exploding dice', '9d6!o=6', 44); - seededRandTest('exploding dice', '9d6!o>=6', 44); - seededRandTest('exploding dice', '9d6!o>5', 44); - - seededRandTest('exploding dice', '9d6!1', 44); - seededRandTest('exploding dice', '9d6!>=5', 56); - seededRandTest('exploding dice', '9d6!<2', 44); - seededRandTest('exploding dice', '9d6!<=3', 54); - - seededRandTest('exploding dice', '9d6!o1', 44); - seededRandTest('exploding dice', '9d6!o>=5', 50); - seededRandTest('exploding dice', '9d6!o<2', 44); - seededRandTest('exploding dice', '9d6!o<=3', 50); - - // 1st round: 6, 2, 1, 5, 3, 5, 1, 4, 6, (compounds 2) (total 33) - // 2nd round: 5, 6 (compounds 1) (total 11) - // 3rd round: 4 (compounds 0) (total 4) - // result 11, 2, 1, 5, 3, 5, 1, 4, 16 - seededRandTest('compounding dice', '9d6!!', 48); - seededRandTest('compounding dice', '9d6!!6', 48); - seededRandTest('compounding dice', '9d6!!=6', 48); - seededRandTest('compounding dice', '9d6!!>=6', 48); - seededRandTest('compounding dice', '9d6!!>5', 48); - - seededRandTest('compounding dice', '9d6!!o', 44); - seededRandTest('compounding dice', '9d6!!o6', 44); - seededRandTest('compounding dice', '9d6!!o=6', 44); - seededRandTest('compounding dice', '9d6!!o>=6', 44); - seededRandTest('compounding dice', '9d6!!o>5', 44); - - seededRandTest('compounding dice count', '9d6!!#>6', 2); - - seededRandTest('compounding dice', '9d6!!>=5', 56); - seededRandTest('compounding dice', '9d6!!<3', 48); - seededRandTest('compounding dice', '9d6!!<=3', 54); - seededRandTest('compounding dice', '9d6!!1', 44); - - seededRandTest('compounding dice', '9d6!!o>=5', 50); - seededRandTest('compounding dice', '9d6!!o<3', 48); - seededRandTest('compounding dice', '9d6!!o<=3', 50); - seededRandTest('compounding dice', '9d6!!o1', 44); - - seededRandTest('explode arith result', '(9d6+3)!', 51); - - // explode, then count 6's - seededRandTest('exploding dice and count', '9d6!#=6', 3); - // explode, then drop less-than-6, then count (should be identical to above) - seededRandTest('exploding dice and count variation', '9d6!-<6#', 3); - - // different dice pools can be combined - seededRandTest('differing nsides addition', '4d4 + 4d6', 25); - // fudge dice can be rolled - seededRandTest('differing nsides addition', '4dF + 6dF', 2); - // fudge dice can be added to [1, -1, -1, 1] - seededRandTest('differing nsides addition', '4dF + 1', 1); - seededRandTest( - 'fudge add to d6', - '4d6+4dF', - 14, - ); - seededRandTest('fudge add to d6', '4dF+4d6', 13); - - test('multiple rolls is multiple results', () { - final dice = DiceExpression.create('2d6', seededRandom); - expect(dice.roll().total, 8); - expect(dice.roll().total, 6); - }); - - test('create dice with real random', () { - final dice = DiceExpression.create('10d100'); - final result1 = dice.roll(); - // result will never be zero -- this test is verifying creating the expr & doing roll - expect(result1, isNot(0)); - }); - - test('string method returns expr', () { - final dice = DiceExpression.create('2d6# + 5d6!>=5 + 5D66', seededRandom); - expect(dice.toString(), '((2d6) # (( + ((5d6) !>= 5)) + (5D66)))'); - }); - - test('invalid dice str', () { - expect( - () => DiceExpression.create('1d5 + x2', seededRandom).roll(), - throwsFormatException, - ); - }); - final invalids = [ - '4!', - '4dF!', - '4dF!!', - '4dFr', - '4D66!', - '4D66!!', - '4D66 r', - ]; - for (final i in invalids) { - test('invalid - $i', () { - expect( - () => DiceExpression.create(i, seededRandom).roll(), - throwsFormatException, - ); - }); - } - - test('toString', () { - // mocked responses should return rolls of 6, 2, 1, 5 - final dice = DiceExpression.create( - '(4d(3+3)! + (2+2)d6) #cs #cf #s #f', - seededRandom, - ); - final out = dice.roll().toString(); - expect( - out, - equals( - '(((((((4d(3 + 3)) ! ) + ((2 + 2)d6)) #cs ) #cf ) #s ) #f ) ===> RollSummary(total: 33, results: [6, 2, 1, 5, 3, 5, 1, 4, 6], metadata: {rolled: [6, 2, 1, 5, 3, 5, 1, 4, 6], score: {successes: [6, 6], failures: [1, 1], critSuccesses: [6, 6], critFailures: [1, 1]}})', - ), - ); - }); - test('toStringPretty', () { - // mocked responses should return rolls of 6, 2, 1, 5 - final dice = DiceExpression.create( - '(4d(3+3)! + (2+2)d6) #cs #cf #s #f', - seededRandom, - ); - final out = dice.roll().toStringPretty(); - expect( - out, - equals( - ''' -(((((((4d(3 + 3)) ! ) + ((2 + 2)d6)) #cs ) #cf ) #s ) #f ) ===> RollSummary(total: 33, results: [6, 2, 1, 5, 3, 5, 1, 4, 6], metadata: {rolled: [6, 2, 1, 5, 3, 5, 1, 4, 6], score: {successes: [6, 6], failures: [1, 1], critSuccesses: [6, 6], critFailures: [1, 1]}}) - (((((((4d(3 + 3)) ! ) + ((2 + 2)d6)) #cs ) #cf ) #s ) #f ) =count=> RollResult(total: 33, results: [6, 2, 1, 5, 3, 5, 1, 4, 6], metadata: {score: {failures: [1, 1]}}) - ((((((4d(3 + 3)) ! ) + ((2 + 2)d6)) #cs ) #cf ) #s ) =count=> RollResult(total: 33, results: [6, 2, 1, 5, 3, 5, 1, 4, 6], metadata: {score: {successes: [6, 6]}}) - (((((4d(3 + 3)) ! ) + ((2 + 2)d6)) #cs ) #cf ) =count=> RollResult(total: 33, results: [6, 2, 1, 5, 3, 5, 1, 4, 6], metadata: {score: {critFailures: [1, 1]}}) - ((((4d(3 + 3)) ! ) + ((2 + 2)d6)) #cs ) =count=> RollResult(total: 33, results: [6, 2, 1, 5, 3, 5, 1, 4, 6], metadata: {score: {critSuccesses: [6, 6]}}) - (((4d(3 + 3)) ! ) + ((2 + 2)d6)) =add=> RollResult(total: 33, results: [6, 2, 1, 5, 3, 5, 1, 4, 6]) - ((4d(3 + 3)) ! ) =explode=> RollResult(total: 17, results: [6, 2, 1, 5, 3], metadata: {rolled: [3]}) - (4d(3 + 3)) =rollDice=> RollResult(total: 14, results: [6, 2, 1, 5], metadata: {rolled: [6, 2, 1, 5]}) - (3 + 3) =add=> RollResult(total: 6, results: [3, 3]) - ((2 + 2)d6) =rollDice=> RollResult(total: 16, results: [5, 1, 4, 6], metadata: {rolled: [5, 1, 4, 6]}) - (2 + 2) =add=> RollResult(total: 4, results: [2, 2]) - ''' - .trim(), - ), - ); - }); - test('toJson', () { - // mocked responses should return rolls of 6, 2, 1, 5 - final dice = DiceExpression.create('4d6', seededRandom); - final obj = dice.roll().toJson(); - expect( - obj, - equals({ - 'expression': '(4d6)', - 'total': 14, - 'results': [6, 2, 1, 5], - 'detailedResults': { - 'expression': '(4d6)', - 'opType': 'rollDice', - 'nsides': 6, - 'ndice': 4, - 'results': [6, 2, 1, 5], - 'metadata': { - 'rolled': [6, 2, 1, 5], - }, - }, - 'metadata': { - 'rolled': [6, 2, 1, 5], - }, - }), - ); - }); - - test('toJson - metadata', () { - // mocked responses should return rolls of 6, 2, 1, 5 - final dice = DiceExpression.create('4d6 #cf #cs', seededRandom); - final obj = dice.roll().toJson(); - expect( - obj, - equals( - { - 'expression': '(((4d6) #cf ) #cs )', - 'total': 14, - 'results': [6, 2, 1, 5], - 'detailedResults': { - 'expression': '(((4d6) #cf ) #cs )', - 'opType': 'count', - 'nsides': 6, - 'ndice': 4, - 'results': [6, 2, 1, 5], - 'metadata': { - 'score': { - 'critSuccesses': [6], - }, - }, - 'left': { - 'expression': '((4d6) #cf )', - 'opType': 'count', - 'nsides': 6, - 'ndice': 4, - 'results': [6, 2, 1, 5], - 'metadata': { - 'score': { - 'critFailures': [1], - }, - }, - 'left': { - 'expression': '(4d6)', - 'opType': 'rollDice', - 'nsides': 6, - 'ndice': 4, - 'results': [6, 2, 1, 5], - 'metadata': { - 'rolled': [6, 2, 1, 5], - }, - }, - }, - }, - 'metadata': { - 'rolled': [6, 2, 1, 5], - 'score': { - 'critSuccesses': [6], - 'critFailures': [1], - }, - }, - }, - ), - ); - }); - - test('rollN test', () async { - final dice = DiceExpression.create('2d6', seededRandom); - - final results = - await dice.rollN(2).map((result) => result.total).toList(); - // mocked responses should return rolls of 6, 2, 1, 5 - expect(results, equals([8, 6])); - }); - - test('stats test', () async { - final dice = DiceExpression.create('2d6', seededRandom); - - final stats = await dice.stats(num: 100); - - expect( - stats, - equals({ - 'mean': 6.65, - 'stddev': 2.35, - 'min': 2, - 'max': 12, - 'count': 100, - 'histogram': { - 2: 3, - 3: 6, - 4: 12, - 5: 10, - 6: 20, - 7: 10, - 8: 18, - 9: 9, - 10: 7, - 11: 2, - 12: 3, - }, - }), - ); - }); - }); -}