Refactor the Instruction parser into an Expression one.
Now support more arithmeric operator (mainly *, /) and recurcive bracket
This commit is contained in:
parent
409acf748f
commit
ce51a3be0a
19 changed files with 824 additions and 448 deletions
|
|
@ -4,8 +4,7 @@ import com.pixelized.desktop.lwa.business.CharacterSheetUseCase
|
||||||
import com.pixelized.desktop.lwa.business.RollUseCase
|
import com.pixelized.desktop.lwa.business.RollUseCase
|
||||||
import com.pixelized.desktop.lwa.business.SettingsUseCase
|
import com.pixelized.desktop.lwa.business.SettingsUseCase
|
||||||
import com.pixelized.desktop.lwa.business.SkillStepUseCase
|
import com.pixelized.desktop.lwa.business.SkillStepUseCase
|
||||||
import com.pixelized.desktop.lwa.business.SkillValueComputationUseCase
|
import com.pixelized.desktop.lwa.business.ExpressionUseCase
|
||||||
import com.pixelized.desktop.lwa.parser.arithmetic.ArithmeticParser
|
|
||||||
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetJsonFactory
|
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetJsonFactory
|
||||||
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
|
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
|
||||||
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetStore
|
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetStore
|
||||||
|
|
@ -25,6 +24,9 @@ import com.pixelized.desktop.lwa.screen.network.NetworkFactory
|
||||||
import com.pixelized.desktop.lwa.screen.network.NetworkViewModel
|
import com.pixelized.desktop.lwa.screen.network.NetworkViewModel
|
||||||
import com.pixelized.desktop.lwa.screen.roll.RollViewModel
|
import com.pixelized.desktop.lwa.screen.roll.RollViewModel
|
||||||
import com.pixelized.desktop.lwa.screen.rollhistory.RollHistoryViewModel
|
import com.pixelized.desktop.lwa.screen.rollhistory.RollHistoryViewModel
|
||||||
|
import com.pixelized.desktop.lwa.parser.dice.DiceParser
|
||||||
|
import com.pixelized.desktop.lwa.parser.word.WordParser
|
||||||
|
import com.pixelized.desktop.lwa.parser.expression.ExpressionParser
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import org.koin.core.module.dsl.factoryOf
|
import org.koin.core.module.dsl.factoryOf
|
||||||
import org.koin.core.module.dsl.singleOf
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
|
@ -84,14 +86,16 @@ val viewModelDependencies
|
||||||
|
|
||||||
val parserDependencies
|
val parserDependencies
|
||||||
get() = module {
|
get() = module {
|
||||||
factoryOf(::ArithmeticParser)
|
factoryOf(::WordParser)
|
||||||
|
factoryOf(::DiceParser)
|
||||||
|
factoryOf(::ExpressionParser)
|
||||||
}
|
}
|
||||||
|
|
||||||
val useCaseDependencies
|
val useCaseDependencies
|
||||||
get() = module {
|
get() = module {
|
||||||
factoryOf(::SkillStepUseCase)
|
factoryOf(::SkillStepUseCase)
|
||||||
factoryOf(::RollUseCase)
|
factoryOf(::RollUseCase)
|
||||||
factoryOf(::SkillValueComputationUseCase)
|
factoryOf(::ExpressionUseCase)
|
||||||
factoryOf(::CharacterSheetUseCase)
|
factoryOf(::CharacterSheetUseCase)
|
||||||
factoryOf(::SettingsUseCase)
|
factoryOf(::SettingsUseCase)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
package com.pixelized.desktop.lwa.business
|
||||||
|
|
||||||
|
|
||||||
|
import com.pixelized.desktop.lwa.parser.expression.Expression
|
||||||
|
import com.pixelized.desktop.lwa.parser.expression.ExpressionParser
|
||||||
|
import com.pixelized.desktop.lwa.parser.word.Word
|
||||||
|
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class ExpressionUseCase(
|
||||||
|
private val expressionParser: ExpressionParser,
|
||||||
|
private val rollUseCase: RollUseCase,
|
||||||
|
) {
|
||||||
|
fun computeSkillValue(
|
||||||
|
sheet: CharacterSheet,
|
||||||
|
skill: CharacterSheet.Skill,
|
||||||
|
diminished: Int,
|
||||||
|
): Int {
|
||||||
|
val context = Context(
|
||||||
|
sheet = sheet,
|
||||||
|
skill = skill,
|
||||||
|
)
|
||||||
|
val base: Int = context.evaluate(
|
||||||
|
expression = skill.base.let(expressionParser::parse),
|
||||||
|
).let {
|
||||||
|
when (skill.occupation) {
|
||||||
|
true -> max(MIN_OCCUPATION_VALUE, it)
|
||||||
|
else -> it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val bonus = context.evaluate(
|
||||||
|
expression = skill.bonus?.let(expressionParser::parse),
|
||||||
|
)
|
||||||
|
val level = context.evaluate(
|
||||||
|
expression = skill.level?.let(expressionParser::parse),
|
||||||
|
)
|
||||||
|
|
||||||
|
return max(base + bonus + level - diminished, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun computeRoll(
|
||||||
|
sheet: CharacterSheet,
|
||||||
|
expression: String,
|
||||||
|
): Int {
|
||||||
|
val context = Context(
|
||||||
|
sheet = sheet,
|
||||||
|
skill = null,
|
||||||
|
)
|
||||||
|
print("Evaluate:\"$expression\"")
|
||||||
|
return context.evaluate(
|
||||||
|
expression = expressionParser.parse(input = expression),
|
||||||
|
).also { println(" > $it") }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Context.evaluate(expression: Expression?): Int {
|
||||||
|
|
||||||
|
return when (expression) {
|
||||||
|
is Expression.Add -> {
|
||||||
|
evaluate(expression.first) + evaluate(expression.second)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Expression.Minus -> {
|
||||||
|
evaluate(expression.first) - evaluate(expression.second)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Expression.Div -> {
|
||||||
|
evaluate(expression.first) / evaluate(expression.second)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Expression.Prod -> {
|
||||||
|
evaluate(expression.first) * evaluate(expression.second)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Expression.Inversion -> {
|
||||||
|
-evaluate(expression)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Expression.Maximum -> {
|
||||||
|
min(evaluate(expression.first), evaluate(expression.second))
|
||||||
|
}
|
||||||
|
|
||||||
|
is Expression.Minimum -> {
|
||||||
|
max(evaluate(expression.first), evaluate(expression.second))
|
||||||
|
}
|
||||||
|
|
||||||
|
is Expression.Flat -> {
|
||||||
|
expression.value
|
||||||
|
}
|
||||||
|
|
||||||
|
is Expression.DiceExpression -> {
|
||||||
|
rollUseCase.roll(expression.dice)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Expression.WordExpression -> when (expression.word.type) {
|
||||||
|
Word.Type.BDC -> evaluate(expressionParser.parse(sheet.damageBonus))
|
||||||
|
Word.Type.BDD -> evaluate(expressionParser.parse(sheet.damageBonus))
|
||||||
|
Word.Type.STR -> sheet.strength
|
||||||
|
Word.Type.DEX -> sheet.dexterity
|
||||||
|
Word.Type.CON -> sheet.constitution
|
||||||
|
Word.Type.HEI -> sheet.height
|
||||||
|
Word.Type.INT -> sheet.intelligence
|
||||||
|
Word.Type.POW -> sheet.power
|
||||||
|
Word.Type.CHA -> sheet.charisma
|
||||||
|
}
|
||||||
|
|
||||||
|
null -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Context(
|
||||||
|
val sheet: CharacterSheet,
|
||||||
|
val skill: CharacterSheet.Skill?,
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MIN_OCCUPATION_VALUE = 40
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
package com.pixelized.desktop.lwa.business
|
package com.pixelized.desktop.lwa.business
|
||||||
|
|
||||||
import com.pixelized.desktop.lwa.parser.arithmetic.Instruction
|
|
||||||
|
import com.pixelized.desktop.lwa.parser.dice.Dice
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
class RollUseCase {
|
class RollUseCase {
|
||||||
|
|
||||||
fun roll(dice: Instruction.Dice): Int {
|
fun roll(dice: Dice): Int {
|
||||||
return roll(
|
return roll(
|
||||||
modifier = dice.modifier,
|
modifier = dice.modifier,
|
||||||
quantity = dice.quantity,
|
quantity = dice.quantity,
|
||||||
|
|
@ -16,28 +17,28 @@ class RollUseCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun roll(
|
fun roll(
|
||||||
modifier: Instruction.Dice.Modifier? = null,
|
modifier: Dice.Modifier? = null,
|
||||||
quantity: Int = 1,
|
quantity: Int = 1,
|
||||||
faces: Int,
|
faces: Int,
|
||||||
): Int {
|
): Int {
|
||||||
print("{")
|
print("{")
|
||||||
return sum(count = quantity) { left ->
|
return sum(count = quantity) { left ->
|
||||||
when (modifier) {
|
when (modifier) {
|
||||||
Instruction.Dice.Modifier.ADVANTAGE -> {
|
Dice.Modifier.ADVANTAGE -> {
|
||||||
val roll1 = roll(faces = faces)
|
val roll1 = roll(faces = faces)
|
||||||
val roll2 = roll(faces = faces)
|
val roll2 = roll(faces = faces)
|
||||||
print("[$roll1,$roll2]")
|
print("[$roll1,$roll2]")
|
||||||
max(roll1, roll2)
|
max(roll1, roll2)
|
||||||
}
|
}
|
||||||
|
|
||||||
Instruction.Dice.Modifier.DISADVANTAGE -> {
|
Dice.Modifier.DISADVANTAGE -> {
|
||||||
val roll1 = roll(faces = faces)
|
val roll1 = roll(faces = faces)
|
||||||
val roll2 = roll(faces = faces)
|
val roll2 = roll(faces = faces)
|
||||||
print("[$roll1,$roll2]")
|
print("[$roll1,$roll2]")
|
||||||
min(roll1, roll2)
|
min(roll1, roll2)
|
||||||
}
|
}
|
||||||
|
|
||||||
Instruction.Dice.Modifier.EMPHASIS -> {
|
Dice.Modifier.EMPHASIS -> {
|
||||||
val roll1 = roll(faces = faces)
|
val roll1 = roll(faces = faces)
|
||||||
val roll2 = roll(faces = faces)
|
val roll2 = roll(faces = faces)
|
||||||
print("[$roll1,$roll2]")
|
print("[$roll1,$roll2]")
|
||||||
|
|
@ -58,7 +59,7 @@ class RollUseCase {
|
||||||
if (quantity > 1 && left != 1) print(",")
|
if (quantity > 1 && left != 1) print(",")
|
||||||
}
|
}
|
||||||
}.also {
|
}.also {
|
||||||
print("}:")
|
print("}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
package com.pixelized.desktop.lwa.business
|
|
||||||
|
|
||||||
import com.pixelized.desktop.lwa.parser.arithmetic.ArithmeticParser
|
|
||||||
import com.pixelized.desktop.lwa.parser.arithmetic.Instruction
|
|
||||||
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
class SkillValueComputationUseCase(
|
|
||||||
private val arithmeticParser: ArithmeticParser,
|
|
||||||
private val rollUseCase: RollUseCase,
|
|
||||||
) {
|
|
||||||
fun computeSkillValue(
|
|
||||||
sheet: CharacterSheet,
|
|
||||||
skill: CharacterSheet.Skill,
|
|
||||||
diminished: Int,
|
|
||||||
): Int {
|
|
||||||
val base: Int = arithmeticParser.parse(skill.base).compute(sheet = sheet).let {
|
|
||||||
when (skill.occupation) {
|
|
||||||
true -> max(MIN_OCCUPATION_VALUE, it)
|
|
||||||
else -> it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val bonus = skill.bonus?.let(arithmeticParser::parse)?.compute(sheet = sheet) ?: 0
|
|
||||||
val level = skill.level?.let(arithmeticParser::parse)?.compute(sheet = sheet) ?: 0
|
|
||||||
|
|
||||||
return max(base + bonus + level - diminished, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun computeRoll(
|
|
||||||
sheet: CharacterSheet,
|
|
||||||
roll: String,
|
|
||||||
): Int {
|
|
||||||
return arithmeticParser.parse(value = roll).compute(sheet = sheet)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO return RollDetail instance instead of an simple Int.
|
|
||||||
private fun List<Instruction>.compute(
|
|
||||||
sheet: CharacterSheet,
|
|
||||||
): Int {
|
|
||||||
print("Roll ->")
|
|
||||||
return sumOf { instruction ->
|
|
||||||
print(" ($instruction):")
|
|
||||||
val value = when (instruction) {
|
|
||||||
is Instruction.Dice -> rollUseCase.roll(dice = instruction)
|
|
||||||
|
|
||||||
is Instruction.Flat -> instruction.value
|
|
||||||
|
|
||||||
is Instruction.Word -> {
|
|
||||||
when (instruction.type) {
|
|
||||||
Instruction.Word.Type.BDC -> {
|
|
||||||
val damageBonusInstructions = arithmeticParser
|
|
||||||
.parse(sheet.damageBonus)
|
|
||||||
.firstOrNull()
|
|
||||||
if (damageBonusInstructions is Instruction.Dice) {
|
|
||||||
rollUseCase.roll(dice = damageBonusInstructions)
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Instruction.Word.Type.BDD -> {
|
|
||||||
val damageBonusInstructions = arithmeticParser
|
|
||||||
.parse(sheet.damageBonus)
|
|
||||||
.firstOrNull()
|
|
||||||
if (damageBonusInstructions is Instruction.Dice) {
|
|
||||||
rollUseCase.roll(
|
|
||||||
modifier = damageBonusInstructions.modifier,
|
|
||||||
quantity = damageBonusInstructions.quantity,
|
|
||||||
faces = damageBonusInstructions.faces / 2,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Instruction.Word.Type.STR -> sheet.strength
|
|
||||||
Instruction.Word.Type.DEX -> sheet.dexterity
|
|
||||||
Instruction.Word.Type.CON -> sheet.constitution
|
|
||||||
Instruction.Word.Type.HEI -> sheet.height
|
|
||||||
Instruction.Word.Type.INT -> sheet.intelligence
|
|
||||||
Instruction.Word.Type.POW -> sheet.power
|
|
||||||
Instruction.Word.Type.CHA -> sheet.charisma
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(value * instruction.sign).also { print("$it") }
|
|
||||||
}.also {
|
|
||||||
println()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val MIN_OCCUPATION_VALUE = 40
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
package com.pixelized.desktop.lwa.parser.arithmetic
|
|
||||||
|
|
||||||
import com.pixelized.desktop.lwa.parser.arithmetic.Instruction.Dice.Modifier
|
|
||||||
import com.pixelized.desktop.lwa.parser.arithmetic.Instruction.Word
|
|
||||||
|
|
||||||
// https://medium.com/@gustavoinzunza/how-to-solve-balanced-parenthesis-problem-with-regex-in-ruby-113eff30a79a
|
|
||||||
// https://stackoverflow.com/questions/546433/regular-expression-to-match-balanced-parentheses
|
|
||||||
class ArithmeticParser {
|
|
||||||
|
|
||||||
private val operatorParser = Regex(
|
|
||||||
"""\s*(?<operator>[-+])?\s*(?<payload>[^-+]*)"""
|
|
||||||
)
|
|
||||||
private val diceParser = Regex(
|
|
||||||
"""^(?<modifier>[ade])?(?<quantity>\d+)[dD](?<faces>\d+)"""
|
|
||||||
)
|
|
||||||
private val wordParser = Regex(
|
|
||||||
"""^(?<word>${words.joinToString(separator = "|") { it }})"""
|
|
||||||
)
|
|
||||||
private val flatParser = Regex(
|
|
||||||
"""(?<flat>\d+)"""
|
|
||||||
)
|
|
||||||
|
|
||||||
@Throws(Instruction.UnknownInstruction::class)
|
|
||||||
fun parse(value: String): List<Instruction> {
|
|
||||||
return operatorParser.findAll(value).mapNotNull {
|
|
||||||
val (operator, instruction) = it.destructured
|
|
||||||
if (instruction.isNotBlank()) {
|
|
||||||
parseInstruction(
|
|
||||||
sign = parseOperator(operator),
|
|
||||||
instruction = instruction,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(Instruction.UnknownInstruction::class)
|
|
||||||
fun parseInstruction(
|
|
||||||
sign: Int,
|
|
||||||
instruction: String
|
|
||||||
): Instruction {
|
|
||||||
diceParser.find(instruction)?.let {
|
|
||||||
val (modifier, quantity, faces) = it.destructured
|
|
||||||
Instruction.Dice(
|
|
||||||
sign = sign,
|
|
||||||
modifier = parseModifier(value = modifier),
|
|
||||||
quantity = quantity.toInt(),
|
|
||||||
faces = faces.toInt(),
|
|
||||||
)
|
|
||||||
}?.let { return it }
|
|
||||||
|
|
||||||
wordParser.find(instruction)?.let {
|
|
||||||
parseWord(
|
|
||||||
sign = sign,
|
|
||||||
value = it.value
|
|
||||||
)
|
|
||||||
}?.let { return it }
|
|
||||||
|
|
||||||
flatParser.find(instruction)?.let {
|
|
||||||
Instruction.Flat(
|
|
||||||
sign = sign,
|
|
||||||
value = it.value.toInt(),
|
|
||||||
)
|
|
||||||
}?.let { return it }
|
|
||||||
|
|
||||||
throw Instruction.UnknownInstruction(payload = instruction)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseOperator(operator: String): Int {
|
|
||||||
return when (operator) {
|
|
||||||
"-" -> -1
|
|
||||||
"+" -> 1
|
|
||||||
else -> 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseModifier(value: String): Modifier? {
|
|
||||||
return when (value) {
|
|
||||||
"a" -> Modifier.ADVANTAGE
|
|
||||||
"d" -> Modifier.DISADVANTAGE
|
|
||||||
"e" -> Modifier.EMPHASIS
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseWord(
|
|
||||||
sign: Int,
|
|
||||||
value: String,
|
|
||||||
): Word? {
|
|
||||||
return try {
|
|
||||||
Word(
|
|
||||||
sign = sign,
|
|
||||||
type = Word.Type.valueOf(value),
|
|
||||||
)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun convertInstructionToString(
|
|
||||||
instructions: List<Instruction>,
|
|
||||||
): String {
|
|
||||||
return instructions.map {
|
|
||||||
val sign = if (it.sign > 0) "+" else "-"
|
|
||||||
val value = when (it) {
|
|
||||||
is Instruction.Dice -> "${it.modifier ?: ""}${it.quantity}d${it.faces}"
|
|
||||||
is Instruction.Flat -> "${it.value}"
|
|
||||||
is Word -> when (it.type) {
|
|
||||||
Word.Type.BDC -> Word.Type.BDC.name
|
|
||||||
Word.Type.BDD -> Word.Type.BDD.name
|
|
||||||
Word.Type.STR -> Word.Type.STR.name
|
|
||||||
Word.Type.DEX -> Word.Type.DEX.name
|
|
||||||
Word.Type.CON -> Word.Type.CON.name
|
|
||||||
Word.Type.HEI -> Word.Type.HEI.name
|
|
||||||
Word.Type.INT -> Word.Type.INT.name
|
|
||||||
Word.Type.POW -> Word.Type.POW.name
|
|
||||||
Word.Type.CHA -> Word.Type.CHA.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"$sign$value"
|
|
||||||
}.joinToString(separator = " ") {
|
|
||||||
it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val words: List<String> = Word.Type.entries.map { it.name }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
package com.pixelized.desktop.lwa.parser.arithmetic
|
|
||||||
|
|
||||||
sealed class Instruction {
|
|
||||||
abstract val sign: Int
|
|
||||||
|
|
||||||
data class Dice(
|
|
||||||
override val sign: Int,
|
|
||||||
val modifier: Modifier?,
|
|
||||||
val quantity: Int,
|
|
||||||
val faces: Int,
|
|
||||||
) : Instruction() {
|
|
||||||
|
|
||||||
enum class Modifier {
|
|
||||||
ADVANTAGE,
|
|
||||||
DISADVANTAGE,
|
|
||||||
EMPHASIS,
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return "${sign.sign}${
|
|
||||||
when (modifier) {
|
|
||||||
Modifier.ADVANTAGE -> "a"
|
|
||||||
Modifier.DISADVANTAGE -> "d"
|
|
||||||
Modifier.EMPHASIS -> "e"
|
|
||||||
null -> ""
|
|
||||||
}
|
|
||||||
}${quantity}d${faces}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Flat(
|
|
||||||
override val sign: Int,
|
|
||||||
val value: Int,
|
|
||||||
) : Instruction() {
|
|
||||||
override fun toString(): String {
|
|
||||||
return "${sign.sign}${value}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Word(
|
|
||||||
override val sign: Int,
|
|
||||||
val type: Type,
|
|
||||||
) : Instruction() {
|
|
||||||
|
|
||||||
enum class Type {
|
|
||||||
BDC, // Damages bonus for melee
|
|
||||||
BDD, // Damages bonus for range
|
|
||||||
STR, // Strength
|
|
||||||
DEX, // Dexterity
|
|
||||||
CON, // Constitution
|
|
||||||
HEI, // Height
|
|
||||||
INT, // Intelligence
|
|
||||||
POW, // Power
|
|
||||||
CHA, // Charisma
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return "${sign.sign}${type}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UnknownInstruction(payload: String) : RuntimeException(
|
|
||||||
"Unknown instruction exception. Unable to parse the following payload:\"$payload\" into an instruction"
|
|
||||||
)
|
|
||||||
|
|
||||||
val Int.sign: String get() = if (this > 0) "+" else "-"
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package com.pixelized.desktop.lwa.parser.dice
|
||||||
|
|
||||||
|
data class Dice(
|
||||||
|
val modifier: Modifier?,
|
||||||
|
val quantity: Int,
|
||||||
|
val faces: Int,
|
||||||
|
) {
|
||||||
|
enum class Modifier {
|
||||||
|
ADVANTAGE,
|
||||||
|
DISADVANTAGE,
|
||||||
|
EMPHASIS,
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
val modifier = when (modifier) {
|
||||||
|
Modifier.ADVANTAGE -> "a"
|
||||||
|
Modifier.DISADVANTAGE -> "d"
|
||||||
|
Modifier.EMPHASIS -> "e"
|
||||||
|
null -> ""
|
||||||
|
}
|
||||||
|
return "${modifier}${quantity}d${faces}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package com.pixelized.desktop.lwa.parser.dice
|
||||||
|
|
||||||
|
class DiceParser {
|
||||||
|
private val diceParser = Regex(
|
||||||
|
"""^(?<modifier>[ade])?(?<quantity>\d+)[dD](?<faces>\d+)"""
|
||||||
|
)
|
||||||
|
|
||||||
|
fun parse(expression: String): Dice? {
|
||||||
|
return diceParser.find(expression)?.let {
|
||||||
|
val (modifier, quantity, faces) = it.destructured
|
||||||
|
Dice(
|
||||||
|
modifier = parseModifier(value = modifier),
|
||||||
|
quantity = quantity.toInt(),
|
||||||
|
faces = faces.toInt(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseModifier(value: String): Dice.Modifier? {
|
||||||
|
return when (value) {
|
||||||
|
"a" -> Dice.Modifier.ADVANTAGE
|
||||||
|
"d" -> Dice.Modifier.DISADVANTAGE
|
||||||
|
"e" -> Dice.Modifier.EMPHASIS
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
package com.pixelized.desktop.lwa.parser.expression
|
||||||
|
|
||||||
|
import com.pixelized.desktop.lwa.parser.dice.Dice
|
||||||
|
import com.pixelized.desktop.lwa.parser.word.Word
|
||||||
|
|
||||||
|
sealed interface Expression {
|
||||||
|
|
||||||
|
data class Add(
|
||||||
|
val first: Expression?,
|
||||||
|
val second: Expression?,
|
||||||
|
) : Expression {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "$first+$second"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Minus(
|
||||||
|
val first: Expression?,
|
||||||
|
val second: Expression?,
|
||||||
|
) : Expression {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "$first-$second"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Div(
|
||||||
|
val first: Expression?,
|
||||||
|
val second: Expression?,
|
||||||
|
) : Expression {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "$first/$second"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Prod(
|
||||||
|
val first: Expression?,
|
||||||
|
val second: Expression?,
|
||||||
|
) : Expression {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "$first*$second"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Minimum(
|
||||||
|
val first: Expression?,
|
||||||
|
val second: Expression?,
|
||||||
|
) : Expression {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "minimum($first,$second)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Maximum(
|
||||||
|
val first: Expression?,
|
||||||
|
val second: Expression?,
|
||||||
|
) : Expression {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "maximum($first,$second)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Inversion(
|
||||||
|
val expression: Expression,
|
||||||
|
) : Expression {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "-($expression)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DiceExpression(
|
||||||
|
val dice: Dice,
|
||||||
|
) : Expression {
|
||||||
|
override fun toString(): String {
|
||||||
|
return dice.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class WordExpression(
|
||||||
|
val word: Word,
|
||||||
|
) : Expression {
|
||||||
|
override fun toString(): String {
|
||||||
|
return word.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Flat(
|
||||||
|
val value: Int,
|
||||||
|
) : Expression {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "$value"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,291 @@
|
||||||
|
package com.pixelized.desktop.lwa.parser.expression
|
||||||
|
|
||||||
|
import com.pixelized.desktop.lwa.parser.dice.DiceParser
|
||||||
|
import com.pixelized.desktop.lwa.parser.word.WordParser
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highly inspired by the following javascript implementation:
|
||||||
|
* https://tiarkrompf.github.io/notes/?/just-write-the-parser/
|
||||||
|
*/
|
||||||
|
class ExpressionParser(
|
||||||
|
private val diceParser: DiceParser,
|
||||||
|
private val wordParser: WordParser,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private val tokenBreak = arrayOf(
|
||||||
|
'+', '-', '/', '*', '(', ')', ','
|
||||||
|
)
|
||||||
|
private val operators = mapOf(
|
||||||
|
'+' to Operator(
|
||||||
|
evaluations = { first, second -> Expression.Add(first, second) },
|
||||||
|
priority = 100,
|
||||||
|
association = 1,
|
||||||
|
),
|
||||||
|
'-' to Operator(
|
||||||
|
evaluations = { first, second -> Expression.Minus(first, second) },
|
||||||
|
priority = 100,
|
||||||
|
association = 1,
|
||||||
|
),
|
||||||
|
'*' to Operator(
|
||||||
|
evaluations = { first, second -> Expression.Prod(first, second) },
|
||||||
|
priority = 200,
|
||||||
|
association = 1,
|
||||||
|
),
|
||||||
|
'/' to Operator(
|
||||||
|
evaluations = { first, second -> Expression.Div(first, second) },
|
||||||
|
priority = 200,
|
||||||
|
association = 1,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private inline fun guard(condition: Boolean, error: () -> Error) {
|
||||||
|
return if (condition.not()) throw error() else Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val stack = Stack()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to determined when a token should end.
|
||||||
|
* Every characters that are not un the [Companion.tokenBreak] list can be part of a token.
|
||||||
|
* @see Companion.tokenBreak
|
||||||
|
*/
|
||||||
|
private fun isToken(): Boolean = stack.peek().let {
|
||||||
|
it != null && !tokenBreak.contains(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to pull from the stack until we have a complete token.
|
||||||
|
* Use the isToken() method to determined when a token should end.
|
||||||
|
* @see isToken
|
||||||
|
*/
|
||||||
|
private fun token(): String {
|
||||||
|
guard(isToken()) {
|
||||||
|
Error.ExpectedTokenCharacter(
|
||||||
|
actual = stack.peek(),
|
||||||
|
expression = stack.input,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val token = StringBuilder()
|
||||||
|
do {
|
||||||
|
stack.pull().let(token::append)
|
||||||
|
} while (isToken())
|
||||||
|
return token.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to construct an expression from a token.
|
||||||
|
* This method handle bracket recursion, functions [min(), max(), etc.]
|
||||||
|
* and token expression creation (Word, Dice, Flat, etc.).
|
||||||
|
* @see token
|
||||||
|
*/
|
||||||
|
private fun factor(): Expression? {
|
||||||
|
when (stack.peek()) {
|
||||||
|
'(' -> {
|
||||||
|
stack.moveCursor()
|
||||||
|
val result = evaluate()
|
||||||
|
guard(stack.peek() == ')') {
|
||||||
|
Error.ExpectedOperator(
|
||||||
|
expected = ')',
|
||||||
|
actual = stack.peek(),
|
||||||
|
expression = stack.input
|
||||||
|
)
|
||||||
|
}
|
||||||
|
stack.moveCursor()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
'-' -> { // this is considered as a sign function for the following expression.
|
||||||
|
stack.moveCursor()
|
||||||
|
val result = evaluate()
|
||||||
|
return result?.let(Expression::Inversion)
|
||||||
|
}
|
||||||
|
|
||||||
|
'+' -> { // this is considered as a sign function for the following expression.
|
||||||
|
stack.moveCursor()
|
||||||
|
val result = evaluate()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> when (val token = token()) {
|
||||||
|
"min" -> {
|
||||||
|
// consume the '(' character
|
||||||
|
stack.moveCursor()
|
||||||
|
// evaluate the content of the first parameter.
|
||||||
|
val first = evaluate()
|
||||||
|
// check that the expression is well formed, need a ,.
|
||||||
|
guard(stack.peek() == ',') {
|
||||||
|
Error.ExpectedOperator(
|
||||||
|
expected = ',',
|
||||||
|
actual = stack.peek(),
|
||||||
|
expression = stack.input
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// consume the ',' character of the second parameter.
|
||||||
|
stack.moveCursor()
|
||||||
|
// evaluate the content of the second parameter.
|
||||||
|
val second = evaluate()
|
||||||
|
// check that the expression is well formed, need a ).
|
||||||
|
guard(stack.peek() == ')') {
|
||||||
|
Error.ExpectedOperator(
|
||||||
|
expected = ')',
|
||||||
|
actual = stack.peek(),
|
||||||
|
expression = stack.input
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// consume the ')' character
|
||||||
|
stack.moveCursor()
|
||||||
|
// build the final function expression
|
||||||
|
return Expression.Minimum(first, second)
|
||||||
|
}
|
||||||
|
|
||||||
|
"max" -> {
|
||||||
|
// consume the '(' character
|
||||||
|
stack.moveCursor()
|
||||||
|
// evaluate the content of the first parameter.
|
||||||
|
val first = evaluate()
|
||||||
|
// check that the expression is well formed, need a ,.
|
||||||
|
guard(stack.peek() == ',') {
|
||||||
|
Error.ExpectedOperator(
|
||||||
|
expected = ',',
|
||||||
|
actual = stack.peek(),
|
||||||
|
expression = stack.input
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// consume the ',' character of the second parameter.
|
||||||
|
stack.moveCursor()
|
||||||
|
// evaluate the content of the second parameter.
|
||||||
|
val second = evaluate()
|
||||||
|
// check that the expression is well formed, need a ).
|
||||||
|
guard(stack.peek() == ')') {
|
||||||
|
Error.ExpectedOperator(
|
||||||
|
expected = ')',
|
||||||
|
actual = stack.peek(),
|
||||||
|
expression = stack.input
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// consume the ')' character
|
||||||
|
stack.moveCursor()
|
||||||
|
// build the final function expression
|
||||||
|
return Expression.Maximum(first, second)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
val value = token.toIntOrNull()
|
||||||
|
if (value != null) {
|
||||||
|
return Expression.Flat(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
val word = wordParser.parse(token)
|
||||||
|
if (word != null) {
|
||||||
|
return Expression.WordExpression(word)
|
||||||
|
}
|
||||||
|
|
||||||
|
val dice = diceParser.parse(token)
|
||||||
|
if (dice != null) {
|
||||||
|
return Expression.DiceExpression(dice)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Error.UnRecognizedToken(actual = token, expression = stack.input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to handle operator priority and associativity.
|
||||||
|
* build with the example https://tiarkrompf.github.io/notes/?/just-write-the-parser/ in mind.
|
||||||
|
* @param minPriority the minimum priority to check.
|
||||||
|
* @return a nullable [Expression]
|
||||||
|
*/
|
||||||
|
private fun evaluate(minPriority: Int): Expression? {
|
||||||
|
var res = factor()
|
||||||
|
while (
|
||||||
|
operators.contains(stack.peek()) &&
|
||||||
|
(operators[stack.peek()]?.priority ?: 0) >= minPriority
|
||||||
|
) {
|
||||||
|
val nextMin = operators[stack.peek()]?.let { it.priority + it.association } ?: 0
|
||||||
|
res = operators[stack.pull()]?.evaluations?.invoke(
|
||||||
|
res,
|
||||||
|
evaluate(nextMin)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to evaluate a expression that have been initialized into the stack.
|
||||||
|
* This is typically call just after the stack have been initialized but also when a recursion is needed.
|
||||||
|
* @return a nullable [Expression]
|
||||||
|
*/
|
||||||
|
private fun evaluate(): Expression? {
|
||||||
|
return evaluate(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public parsing method for an expression.
|
||||||
|
* @param input the input expression [String]
|
||||||
|
* @return a nullable [Expression]
|
||||||
|
*/
|
||||||
|
fun parse(input: String?): Expression? {
|
||||||
|
return input.takeIf { it?.isNotBlank() == true }?.let {
|
||||||
|
stack.init(expression = it)
|
||||||
|
evaluate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Error(
|
||||||
|
message: String,
|
||||||
|
expression: String,
|
||||||
|
) : Exception("Expession:$expression - $message") {
|
||||||
|
|
||||||
|
class ExpectedTokenCharacter(
|
||||||
|
val actual: Char?,
|
||||||
|
val expression: String,
|
||||||
|
) : Error(
|
||||||
|
"Expected a token character (either a letter or a digit), but was: $actual", expression
|
||||||
|
)
|
||||||
|
|
||||||
|
class ExpectedOperator(
|
||||||
|
val expected: Char,
|
||||||
|
val actual: Char?,
|
||||||
|
val expression: String,
|
||||||
|
) : Error("Expected operator: $expected, but was $actual", expression)
|
||||||
|
|
||||||
|
class UnRecognizedToken(
|
||||||
|
val actual: String,
|
||||||
|
val expression: String,
|
||||||
|
) : Error(
|
||||||
|
"Expected a specific token 'word', 'digit' or 'function', but was: $actual",
|
||||||
|
expression
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class Operator(
|
||||||
|
val evaluations: (Expression?, Expression?) -> Expression?,
|
||||||
|
val priority: Int, // operator priority, higher is priority
|
||||||
|
val association: Int, // 0 : left associativity, 1: right
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class Stack(
|
||||||
|
var input: String = "",
|
||||||
|
private var cursor: Int = 0,
|
||||||
|
private var peek: Char? = null,
|
||||||
|
) {
|
||||||
|
fun init(expression: String) {
|
||||||
|
input = expression
|
||||||
|
cursor = 0
|
||||||
|
peek = null
|
||||||
|
moveCursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun peek(): Char? = peek
|
||||||
|
|
||||||
|
fun pull(): Char? = peek().also { moveCursor() }
|
||||||
|
|
||||||
|
fun moveCursor() {
|
||||||
|
peek = input.getOrNull(cursor)
|
||||||
|
cursor = (cursor + 1).takeIf { it < input.length } ?: -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package com.pixelized.desktop.lwa.parser.word
|
||||||
|
|
||||||
|
data class Word(
|
||||||
|
val type: Type,
|
||||||
|
) {
|
||||||
|
enum class Type {
|
||||||
|
BDC, // Damages bonus for melee
|
||||||
|
BDD, // Damages bonus for range
|
||||||
|
STR, // Strength
|
||||||
|
DEX, // Dexterity
|
||||||
|
CON, // Constitution
|
||||||
|
HEI, // Height
|
||||||
|
INT, // Intelligence
|
||||||
|
POW, // Power
|
||||||
|
CHA, // Charisma
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "$type"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.pixelized.desktop.lwa.parser.word
|
||||||
|
|
||||||
|
class WordParser {
|
||||||
|
|
||||||
|
fun parse(
|
||||||
|
value: String,
|
||||||
|
): Word? {
|
||||||
|
return try {
|
||||||
|
Word(type = Word.Type.valueOf(value))
|
||||||
|
} catch (_: Exception) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package com.pixelized.desktop.lwa.screen.characterSheet.detail
|
package com.pixelized.desktop.lwa.screen.characterSheet.detail
|
||||||
|
|
||||||
import com.pixelized.desktop.lwa.business.SkillValueComputationUseCase
|
import com.pixelized.desktop.lwa.business.ExpressionUseCase
|
||||||
import com.pixelized.desktop.lwa.composable.tooltip.TooltipUio
|
import com.pixelized.desktop.lwa.composable.tooltip.TooltipUio
|
||||||
import com.pixelized.desktop.lwa.repository.characterSheet.SkillDescriptionFactory
|
import com.pixelized.desktop.lwa.repository.characterSheet.SkillDescriptionFactory
|
||||||
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet
|
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet
|
||||||
|
|
@ -39,7 +39,7 @@ import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteris
|
||||||
import org.jetbrains.compose.resources.getString
|
import org.jetbrains.compose.resources.getString
|
||||||
|
|
||||||
class CharacterSheetFactory(
|
class CharacterSheetFactory(
|
||||||
private val skillUseCase: SkillValueComputationUseCase,
|
private val skillUseCase: ExpressionUseCase,
|
||||||
private val skillDescriptionFactory: SkillDescriptionFactory,
|
private val skillDescriptionFactory: SkillDescriptionFactory,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.pixelized.desktop.lwa.business.SkillStepUseCase
|
import com.pixelized.desktop.lwa.business.SkillStepUseCase
|
||||||
import com.pixelized.desktop.lwa.business.SkillValueComputationUseCase
|
import com.pixelized.desktop.lwa.business.ExpressionUseCase
|
||||||
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
|
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
|
||||||
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet
|
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet
|
||||||
import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
|
import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
|
||||||
|
|
@ -33,7 +33,7 @@ import org.jetbrains.compose.resources.getString
|
||||||
class RollViewModel(
|
class RollViewModel(
|
||||||
private val characterSheetRepository: CharacterSheetRepository,
|
private val characterSheetRepository: CharacterSheetRepository,
|
||||||
private val rollHistoryRepository: RollHistoryRepository,
|
private val rollHistoryRepository: RollHistoryRepository,
|
||||||
private val skillComputation: SkillValueComputationUseCase,
|
private val skillComputation: ExpressionUseCase,
|
||||||
private val skillStepUseCase: SkillStepUseCase,
|
private val skillStepUseCase: SkillStepUseCase,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
|
@ -172,7 +172,7 @@ class RollViewModel(
|
||||||
|
|
||||||
val roll = skillComputation.computeRoll(
|
val roll = skillComputation.computeRoll(
|
||||||
sheet = sheet,
|
sheet = sheet,
|
||||||
roll = rollAction,
|
expression = rollAction,
|
||||||
)
|
)
|
||||||
|
|
||||||
val success = rollStep?.let {
|
val success = rollStep?.let {
|
||||||
|
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
package com.pixelized.desktop.lwa.parser
|
|
||||||
|
|
||||||
import com.pixelized.desktop.lwa.parser.arithmetic.Instruction
|
|
||||||
import com.pixelized.desktop.lwa.parser.arithmetic.ArithmeticParser
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertFails
|
|
||||||
|
|
||||||
class InstructionParserTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testDiceInstructionParse() {
|
|
||||||
val parser = ArithmeticParser()
|
|
||||||
|
|
||||||
fun test(
|
|
||||||
instruction: String,
|
|
||||||
expectedModifier: Instruction.Dice.Modifier?,
|
|
||||||
expectedQuantity: Int,
|
|
||||||
expectedFaces: Int,
|
|
||||||
) {
|
|
||||||
val dice = parser.parse(value = instruction).first()
|
|
||||||
|
|
||||||
assert(dice is Instruction.Dice) {
|
|
||||||
"Instruction should be ArithmeticInstruction.Dice but was: ${dice::class.java.simpleName}"
|
|
||||||
}
|
|
||||||
(dice as? Instruction.Dice)?.let {
|
|
||||||
assert(dice.modifier == expectedModifier) {
|
|
||||||
"$instruction modifier should be:\"$expectedModifier\", but was: ${dice.modifier}"
|
|
||||||
}
|
|
||||||
assert(dice.quantity == expectedQuantity) {
|
|
||||||
"$instruction quantity should be \"$expectedQuantity\" but was ${dice.quantity}"
|
|
||||||
}
|
|
||||||
assert(dice.faces == expectedFaces) {
|
|
||||||
"$instruction faces should be \"$expectedFaces\" but was ${dice.faces}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test(
|
|
||||||
instruction = "1d100",
|
|
||||||
expectedModifier = null,
|
|
||||||
expectedQuantity = 1,
|
|
||||||
expectedFaces = 100,
|
|
||||||
)
|
|
||||||
test(
|
|
||||||
instruction = "a2d6",
|
|
||||||
expectedModifier = Instruction.Dice.Modifier.ADVANTAGE,
|
|
||||||
expectedQuantity = 2,
|
|
||||||
expectedFaces = 6,
|
|
||||||
)
|
|
||||||
test(
|
|
||||||
instruction = "d1d2",
|
|
||||||
expectedModifier = Instruction.Dice.Modifier.DISADVANTAGE,
|
|
||||||
expectedQuantity = 1,
|
|
||||||
expectedFaces = 2,
|
|
||||||
)
|
|
||||||
test(
|
|
||||||
instruction = "e6d6",
|
|
||||||
expectedModifier = Instruction.Dice.Modifier.EMPHASIS,
|
|
||||||
expectedQuantity = 6,
|
|
||||||
expectedFaces = 6,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testWordInstructionParse() {
|
|
||||||
val parser = ArithmeticParser()
|
|
||||||
|
|
||||||
ArithmeticParser.words.map { instruction ->
|
|
||||||
val word = parser.parse(value = instruction).first()
|
|
||||||
|
|
||||||
assert(word is Instruction.Word) {
|
|
||||||
"Instruction should be ArithmeticInstruction.Word but was: ${word::class.java.simpleName}"
|
|
||||||
}
|
|
||||||
(word as? Instruction.Word)?.let {
|
|
||||||
assert(it.type.name == instruction) {
|
|
||||||
"Instruction should be $instruction, but was ${it.type.name}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testFlatInstructionParse() {
|
|
||||||
val parser = ArithmeticParser()
|
|
||||||
|
|
||||||
"100".let { instruction ->
|
|
||||||
val flat = parser.parse(value = instruction).first()
|
|
||||||
|
|
||||||
assert(flat is Instruction.Flat) {
|
|
||||||
"Instruction should be ArithmeticInstruction.Flat but was: ${flat::class.java.simpleName}"
|
|
||||||
}
|
|
||||||
(flat as? Instruction.Flat)?.let {
|
|
||||||
assert("${it.value}" == instruction) {
|
|
||||||
"Instruction should be $instruction, but was ${it.value}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testRollParse() {
|
|
||||||
val parser = ArithmeticParser()
|
|
||||||
|
|
||||||
fun test(
|
|
||||||
instruction: Instruction,
|
|
||||||
expectedInstruction: Instruction,
|
|
||||||
) {
|
|
||||||
assert(instruction == expectedInstruction) {
|
|
||||||
"Arithmetic instruction should be $expectedInstruction but was: $instruction"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val instructions = parser.parse(
|
|
||||||
value = "1+1d6+2-BDC+BDD",
|
|
||||||
)
|
|
||||||
|
|
||||||
test(
|
|
||||||
instruction = instructions[0],
|
|
||||||
expectedInstruction = Instruction.Flat(sign = 1, value = 1),
|
|
||||||
)
|
|
||||||
test(
|
|
||||||
instruction = instructions[1],
|
|
||||||
expectedInstruction = Instruction.Dice(sign = 1, modifier = null, quantity = 1, faces = 6),
|
|
||||||
)
|
|
||||||
test(
|
|
||||||
instruction = instructions[2],
|
|
||||||
expectedInstruction = Instruction.Flat(sign = 1, value = 2),
|
|
||||||
)
|
|
||||||
test(
|
|
||||||
instruction = instructions[3],
|
|
||||||
expectedInstruction = Instruction.Word(sign = -1, type = Instruction.Word.Type.BDC),
|
|
||||||
)
|
|
||||||
test(
|
|
||||||
instruction = instructions[4],
|
|
||||||
expectedInstruction = Instruction.Word(sign = 1, type = Instruction.Word.Type.BDD),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
package com.pixelized.desktop.lwa.parser.dice
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class DiceParserTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testInvalidExpression() {
|
||||||
|
val parser = DiceParser()
|
||||||
|
parser.test(expression = "", expected = null)
|
||||||
|
parser.test(expression = " ", expected = null)
|
||||||
|
parser.test(expression = "1", expected = null)
|
||||||
|
parser.test(expression = "d6", expected = null)
|
||||||
|
parser.test(expression = "ad6", expected = null)
|
||||||
|
parser.test(expression = "dd6", expected = null)
|
||||||
|
parser.test(expression = "ed6", expected = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFaces() {
|
||||||
|
val parser = DiceParser()
|
||||||
|
parser.test(expression = "1d4", expected = Dice(modifier = null, quantity = 1, faces = 4))
|
||||||
|
parser.test(expression = "1d6", expected = Dice(modifier = null, quantity = 1, faces = 6))
|
||||||
|
parser.test(expression = "1d8", expected = Dice(modifier = null, quantity = 1, faces = 8))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testQuantity() {
|
||||||
|
val parser = DiceParser()
|
||||||
|
parser.test(expression = "2d6", expected = Dice(modifier = null, quantity = 2, faces = 6))
|
||||||
|
parser.test(expression = "3d6", expected = Dice(modifier = null, quantity = 3, faces = 6))
|
||||||
|
parser.test(expression = "4d6", expected = Dice(modifier = null, quantity = 4, faces = 6))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testWhitespace() {
|
||||||
|
val parser = DiceParser()
|
||||||
|
parser.test(expression = " 2d6", expected = Dice(modifier = null, quantity = 2, faces = 6))
|
||||||
|
parser.test(expression = "2d6 ", expected = Dice(modifier = null, quantity = 2, faces = 6))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testModifier() {
|
||||||
|
val parser = DiceParser()
|
||||||
|
parser.test(
|
||||||
|
expression = "a1d6",
|
||||||
|
expected = Dice(modifier = Dice.Modifier.ADVANTAGE, quantity = 1, faces = 6)
|
||||||
|
)
|
||||||
|
parser.test(
|
||||||
|
expression = "d1d6",
|
||||||
|
expected = Dice(modifier = Dice.Modifier.DISADVANTAGE, quantity = 1, faces = 6)
|
||||||
|
)
|
||||||
|
parser.test(
|
||||||
|
expression = "e1d6",
|
||||||
|
expected = Dice(modifier = Dice.Modifier.EMPHASIS, quantity = 1, faces = 6)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DiceParser.test(
|
||||||
|
expression: String,
|
||||||
|
expected: Dice?,
|
||||||
|
) {
|
||||||
|
val result = parse(expression = expression)
|
||||||
|
assert(result == expected) {
|
||||||
|
"DiceParser.parse(expression=$expression) is expected to return:$expected, but was:$result"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
package com.pixelized.desktop.lwa.parser.expression
|
||||||
|
|
||||||
|
import com.pixelized.desktop.lwa.parser.dice.DiceParser
|
||||||
|
import com.pixelized.desktop.lwa.parser.expression.ExpressionParser.Error
|
||||||
|
import com.pixelized.desktop.lwa.parser.word.WordParser
|
||||||
|
import org.junit.Test
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
|
class ExpressionParserTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testInvalidExpression() {
|
||||||
|
val parser = ExpressionParser(
|
||||||
|
diceParser = DiceParser(),
|
||||||
|
wordParser = WordParser(),
|
||||||
|
)
|
||||||
|
parser.test("", null)
|
||||||
|
parser.test(" ", null)
|
||||||
|
assertFailsWith(Error.UnRecognizedToken::class) {
|
||||||
|
parser.test("pouet", null)
|
||||||
|
}
|
||||||
|
assertFailsWith(Error.ExpectedTokenCharacter::class) {
|
||||||
|
parser.test("1+", null)
|
||||||
|
}
|
||||||
|
assertFailsWith(Error.ExpectedOperator::class) {
|
||||||
|
parser.test("(153", null)
|
||||||
|
}
|
||||||
|
assertFailsWith(Error.ExpectedOperator::class) {
|
||||||
|
parser.test("min(1+1)", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testArithmeticExpression() {
|
||||||
|
val parser = ExpressionParser(
|
||||||
|
diceParser = DiceParser(),
|
||||||
|
wordParser = WordParser(),
|
||||||
|
)
|
||||||
|
parser.test(
|
||||||
|
expression = "-1",
|
||||||
|
expected = Expression.Inversion(Expression.Flat(1)),
|
||||||
|
)
|
||||||
|
parser.test(
|
||||||
|
expression = "+1",
|
||||||
|
expected = Expression.Flat(1),
|
||||||
|
)
|
||||||
|
parser.test(
|
||||||
|
expression = "1+2",
|
||||||
|
expected = Expression.Add(Expression.Flat(1), Expression.Flat(2))
|
||||||
|
)
|
||||||
|
parser.test(
|
||||||
|
expression = "1-2",
|
||||||
|
expected = Expression.Minus(Expression.Flat(1), Expression.Flat(2))
|
||||||
|
)
|
||||||
|
parser.test(
|
||||||
|
expression = "1*2",
|
||||||
|
expected = Expression.Prod(Expression.Flat(1), Expression.Flat(2))
|
||||||
|
)
|
||||||
|
parser.test(
|
||||||
|
expression = "1/2",
|
||||||
|
expected = Expression.Div(Expression.Flat(1), Expression.Flat(2))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFunctionExpression() {
|
||||||
|
val parser = ExpressionParser(
|
||||||
|
diceParser = DiceParser(),
|
||||||
|
wordParser = WordParser(),
|
||||||
|
)
|
||||||
|
parser.test(
|
||||||
|
expression = "min(1,2)",
|
||||||
|
expected = Expression.Minimum(Expression.Flat(1), Expression.Flat(2))
|
||||||
|
)
|
||||||
|
parser.test(
|
||||||
|
expression = "max(1,2)",
|
||||||
|
expected = Expression.Maximum(Expression.Flat(1), Expression.Flat(2))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testArithmeticPriorityExpression() {
|
||||||
|
val parser = ExpressionParser(
|
||||||
|
diceParser = DiceParser(),
|
||||||
|
wordParser = WordParser(),
|
||||||
|
)
|
||||||
|
parser.test(
|
||||||
|
expression = "(1+2)*3",
|
||||||
|
expected = Expression.Prod(
|
||||||
|
Expression.Add(Expression.Flat(1), Expression.Flat(2)),
|
||||||
|
Expression.Flat(3),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
parser.test(
|
||||||
|
expression = "1+2*3",
|
||||||
|
expected = Expression.Add(
|
||||||
|
Expression.Flat(1),
|
||||||
|
Expression.Prod(Expression.Flat(2), Expression.Flat(3)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ExpressionParser.test(
|
||||||
|
expression: String,
|
||||||
|
expected: Expression?,
|
||||||
|
) {
|
||||||
|
val result = parse(input = expression)
|
||||||
|
assert(result == expected) {
|
||||||
|
"ExpressionParser.parse(input=$expression) is expected to return:$expected, but was:$result"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.pixelized.desktop.lwa.parser.word
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class WordParserTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testInvalidExpression() {
|
||||||
|
val parser = WordParser()
|
||||||
|
parser.test(expression = "", expected = null)
|
||||||
|
parser.test(expression = " ", expected = null)
|
||||||
|
parser.test(expression = "1", expected = null)
|
||||||
|
parser.test(expression = "1d6", expected = null)
|
||||||
|
parser.test(expression = "pouet", expected = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testValidExpression() {
|
||||||
|
val parser = WordParser()
|
||||||
|
Word.Type.entries.forEach { type ->
|
||||||
|
parser.test(expression = type.name, expected = Word(type))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun WordParser.test(
|
||||||
|
expression: String,
|
||||||
|
expected: Word?,
|
||||||
|
) {
|
||||||
|
val result = parse(value = expression)
|
||||||
|
assert(result == expected) {
|
||||||
|
"WordParser.parse(value=$expression) is expected to return:$expected, but was:$result"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,7 +28,7 @@ typealias Server = EmbeddedServer<NettyApplicationEngine, NettyApplicationEngine
|
||||||
|
|
||||||
class LocalServer {
|
class LocalServer {
|
||||||
private var server: Server? = null
|
private var server: Server? = null
|
||||||
private val json = Json { explicitNulls = false }
|
private val json = Json { explicitNulls = true }
|
||||||
private val outgoingMessageBuffer = MutableSharedFlow<Message>()
|
private val outgoingMessageBuffer = MutableSharedFlow<Message>()
|
||||||
|
|
||||||
fun create(): LocalServer {
|
fun create(): LocalServer {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue