Last active
September 26, 2023 05:19
-
-
Save apples/cae938769cd31ac9a9979c1c0bdac5eb to your computer and use it in GitHub Desktop.
GDScript Dice Notation Parser and Calculator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class_name DiceRoll | |
extends RefCounted | |
## Usage | |
## [codeblock] | |
## var dice := DiceRoll.new("1d4 + 2 * X") | |
## if not dice.is_valid(): | |
## print("Invalid dice.") | |
## print("Range: %s ~ %s" % [dice.min(), dice.max()]) | |
## print("Sample roll: %s" % [dice.roll({ X = 2 })]) | |
## [/codeblock] | |
# notation := term ( ("+" / "-") term )* | |
# term := factor ( ("*" / "/") factor )* | |
# factor := roll / value | |
# roll := value "d" value | |
# value := int / var / "(" notation ")" | |
# int := [0-9]+ | |
# var := [A-Z] | |
var notation: String | |
var bytecode: PackedByteArray | |
enum { | |
BC_INT, | |
BC_VAR, | |
BC_ROLL, | |
BC_ADD, | |
BC_SUB, | |
BC_MUL, | |
BC_DIV, | |
} | |
func _init(p_notation: String): | |
notation = p_notation | |
bytecode = Parser.new(notation).parse() | |
func is_valid() -> bool: | |
return bytecode.size() > 0 | |
func min(vars: Dictionary = {}) -> int: | |
if not is_valid(): | |
return 0 | |
return _execute(vars, FakeRng.new(FakeRng.MODE_MIN)) | |
func max(vars: Dictionary = {}) -> int: | |
if not is_valid(): | |
return 0 | |
return _execute(vars, FakeRng.new(FakeRng.MODE_MAX)) | |
func roll(vars: Dictionary = {}, rng: RandomNumberGenerator = null) -> int: | |
if not is_valid(): | |
return 0 | |
if rng == null: | |
rng = RandomNumberGenerator.new() | |
rng.randomize() | |
return _execute(vars, rng) | |
func _execute(vars: Dictionary, rng): | |
var stack: Array[int] = [] | |
var i: int = 0 | |
var ei: int = bytecode.size() | |
while i < ei: | |
match bytecode[i]: | |
BC_INT: | |
i += 1 | |
assert(i < ei) | |
stack.push_back(bytecode[i]) | |
BC_VAR: | |
i += 1 | |
assert(i < ei) | |
var k := String.chr(bytecode[i]) | |
if k in vars: | |
stack.push_back(vars[k]) | |
else: | |
stack.push_back(0) | |
BC_ROLL: | |
assert(stack.size() >= 2) | |
var s: int = stack.pop_back() | |
var c: int = stack.pop_back() | |
var x := 0 | |
for j in range(c): | |
x += rng.randi_range(1, s) | |
stack.push_back(x) | |
BC_ADD: | |
assert(stack.size() >= 2) | |
var r: int = stack.pop_back() | |
var l: int = stack.pop_back() | |
stack.push_back(l + r) | |
BC_SUB: | |
assert(stack.size() >= 2) | |
var r: int = stack.pop_back() | |
var l: int = stack.pop_back() | |
stack.push_back(l - r) | |
BC_MUL: | |
assert(stack.size() >= 2) | |
var r: int = stack.pop_back() | |
var l: int = stack.pop_back() | |
stack.push_back(l * r) | |
BC_DIV: | |
assert(stack.size() >= 2) | |
var r: int = stack.pop_back() | |
var l: int = stack.pop_back() | |
stack.push_back(l / r) | |
i += 1 | |
assert(stack.size() == 1, "Bytecode left stack in invalid state") | |
return stack[-1] | |
class FakeRng: | |
var mode: int | |
enum { | |
MODE_MIN, | |
MODE_MAX, | |
} | |
func _init(p_mode: int): | |
mode = p_mode | |
func randi_range(a: int, b: int) -> int: | |
match mode: | |
MODE_MIN: | |
return a | |
MODE_MAX: | |
return b | |
_: | |
push_error("Invalid mode") | |
return a | |
class Parser: | |
var notation: String | |
var bytecode: PackedByteArray | |
var _pos: int = -1 | |
var _end: int | |
var _ascii: PackedByteArray | |
var _errors: Array[String] | |
func _init(p_notation: String): | |
notation = p_notation | |
_end = notation.length() | |
_ascii = notation.to_ascii_buffer() | |
func _advance(): | |
_pos += 1 | |
while _pos < _end and notation[_pos] == " ": | |
_pos += 1 | |
func _accept(char: String) -> bool: | |
if _pos < _end and notation[_pos] == char: | |
_advance() | |
return true | |
return false | |
func _error_unexpected_token(expected: String) -> void: | |
var tk := notation[_pos] if _pos < _end else "EOF" | |
_errors.append("Unexpected token (%s): found '%s', expected '%s'." % [_pos, tk, expected]) | |
func parse() -> PackedByteArray: | |
_advance() | |
if not _parse_notation(): | |
_errors.append("Failed to parse root notation.") | |
if _pos < _end: | |
_error_unexpected_token("EOF") | |
if _errors.size() > 0: | |
push_error("Invalid dice notation '%s':\n%s" % [notation, "\n".join(_errors)]) | |
return PackedByteArray() | |
return bytecode | |
func _parse_notation() -> bool: | |
if not _parse_term(): | |
return false | |
while true: | |
if _accept("+"): | |
if not _parse_term(): | |
return false | |
bytecode.append(BC_ADD) | |
elif _accept("-"): | |
if not _parse_term(): | |
return false | |
bytecode.append(BC_SUB) | |
else: | |
break | |
return true | |
func _parse_term() -> bool: | |
if not _parse_factor(): | |
return false | |
while true: | |
if _accept("*"): | |
if not _parse_factor(): | |
return false | |
bytecode.append(BC_MUL) | |
elif _accept("/"): | |
if not _parse_factor(): | |
return false | |
bytecode.append(BC_DIV) | |
else: | |
break | |
return true | |
func _parse_factor() -> bool: | |
if not _parse_value(): | |
return false | |
if _accept("d"): | |
if not _parse_value(): | |
return false | |
bytecode.append(BC_ROLL) | |
return true | |
func _parse_value() -> bool: | |
if _accept("("): | |
if not _parse_notation(): | |
_error_unexpected_token("notation") | |
return false | |
if not _accept(")"): | |
_error_unexpected_token(")") | |
return false | |
return true | |
var ci := _ascii[_pos] | |
if ci >= 48 and ci <= 57: # '0' - '9' | |
var iv := ci - 48 | |
while _pos + 1 < _end: | |
ci = _ascii[_pos + 1] | |
if ci < 48 or ci > 57: | |
break | |
iv = iv * 10 + (ci - 48) | |
_pos += 1 | |
bytecode.append_array([BC_INT, iv]) | |
_advance() | |
return true | |
if ci >= 65 and ci <= 90: # 'A' - 'Z' | |
bytecode.append_array([BC_VAR, ci]) | |
_advance() | |
return true | |
_error_unexpected_token("atomic") | |
return false | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment