diff --git a/src/main/scala/wordbots/AstValidator.scala b/src/main/scala/wordbots/AstValidator.scala index 566b75f..7972ec5 100644 --- a/src/main/scala/wordbots/AstValidator.scala +++ b/src/main/scala/wordbots/AstValidator.scala @@ -80,7 +80,7 @@ object NoUnimplementedRules extends AstRule { } /** - * Disallow the following behaviors within a triggered ability (excepted AfterPlayed, which can be treated more like an action): + * Disallow the following behaviors within a triggered ability or conditional action (except AfterPlayed trigger, which can be treated more like an action): * * choosing targets (because there's no UI support for having a player choose targets during event execution) * * rewriting card text (because calling the parser is expensive and should only happen from direct player interaction) */ @@ -101,13 +101,14 @@ object NoChooseOrRewriteInTriggeredAction extends AstRule { node match { case TriggeredAbility(AfterPlayed(_), _) => Success() // Choosing targets and rewriting text *is* allowed for AfterPlayed triggers. case TriggeredAbility(_, _) => validateChildren(NoChooseTargetOrRewrite, node) // (but not for any other trigger). + case ConditionalAction(_, _) => validateChildren(NoChooseTargetOrRewrite, node) case n: AstNode => validateChildren(this, n) } } } /** - * Disallow paying energy within triggered abilities, + * Disallow paying energy within triggered abilities or conditional actions, * because there's no gameplay support for "rolling back" the action triggered if there's not enough energy to pay. */ object NoPayEnergyInTriggeredAction extends AstRule { @@ -123,6 +124,7 @@ object NoPayEnergyInTriggeredAction extends AstRule { override def validate(node: AstNode): Try[Unit] = { node match { case TriggeredAbility(_, _) => validateChildren(NoPayEnergy, node) + case ConditionalAction(_, _) => validateChildren(NoPayEnergy, node) case n: AstNode => validateChildren(this, n) } } diff --git a/src/main/scala/wordbots/ErrorAnalyzer.scala b/src/main/scala/wordbots/ErrorAnalyzer.scala index cf15f31..c13a4e0 100644 --- a/src/main/scala/wordbots/ErrorAnalyzer.scala +++ b/src/main/scala/wordbots/ErrorAnalyzer.scala @@ -1,7 +1,7 @@ package wordbots import com.workday.montague.ccg.CcgCat -import com.workday.montague.parser.SemanticParseNode +import com.workday.montague.parser.{SemanticParseNode, SemanticParseResult} import com.workday.montague.semantics.{Form, Lambda, Nonsense} import scalaz.Memo @@ -58,6 +58,20 @@ object ErrorAnalyzer { (result, t1 - t0) } + /** If any parses are semantically complete, selects the first one that passes AstValidator. Otherwise, selects bestParse (if any) as determined by the parser. */ + def bestValidParse(parseResult: SemanticParseResult[CcgCat])(implicit validationMode: ValidationMode = ValidateUnknownCard): Option[SemanticParseNode[CcgCat]] = { + val bestValidParses: List[SemanticParseNode[CcgCat]] = for { + parse <- parseResult.semanticCompleteParses + ast <- parse.semantic match { + case Form(ast: AstNode) => Some(ast) + case _ => None + } + if AstValidator(validationMode).validate(ast).isSuccess + } yield parse + + bestValidParses.headOption.orElse(parseResult.bestParse) + } + // Note: "fast mode" disables finding syntax/semantics suggestions and just does the bare minimum to diagnose the error def diagnoseError(input: String, parseResult: Option[SemanticParseNode[CcgCat]], isFastMode: Boolean = false) (implicit validationMode: ValidationMode = ValidateUnknownCard): Option[ParserError] = { diff --git a/src/main/scala/wordbots/Lexicon.scala b/src/main/scala/wordbots/Lexicon.scala index 407a861..f2c85e9 100644 --- a/src/main/scala/wordbots/Lexicon.scala +++ b/src/main/scala/wordbots/Lexicon.scala @@ -71,11 +71,13 @@ object Lexicon { ((NP/NP)\N, λ {o: ObjectType => λ {t: TargetObjectOrTile => ObjectsMatchingConditions(o, Seq(AdjacentTo(t)))}}), (PP/NP, λ {c: ObjectsMatchingConditions => ObjectsMatchingConditions(c.objectType, Seq(AdjacentTo(ThisObject)) ++ c.conditions)}) )) + - ("is" /?/ Seq("adjacent to", "adjacent to a", "adjacent to an") -> Seq( + ("is" / Seq("adjacent to", "adjacent to a", "adjacent to an") -> Seq( + ((S\NP)/N, λ {t: ObjectType => λ {o: TargetObject => CollectionExists(ObjectsMatchingConditions(t, Seq(AdjacentTo(o))))}}), ((S\N)/NP, λ {o: TargetObject => λ {e: EnemyObject => CollectionExists(ObjectsMatchingConditions(e.objectType, Seq(AdjacentTo(o), ControlledBy(Opponent))))}}), ((S\NP)/NP, λ {c: ObjectsMatchingConditions => λ {o: TargetObject => CollectionExists(ObjectsMatchingConditions(c.objectType, c.conditions :+ AdjacentTo(o)))}}) )) + - ("is not" /?/ Seq("adjacent to", "adjacent to a", "adjacent to an") -> Seq( + ("is not" / Seq("adjacent to", "adjacent to a", "adjacent to an") -> Seq( + ((S\NP)/N, λ {t: ObjectType => λ {o: TargetObject => NotGC(CollectionExists(ObjectsMatchingConditions(t, Seq(AdjacentTo(o)))))}}), ((S\N)/NP, λ {o: TargetObject => λ {e: EnemyObject => NotGC(CollectionExists(ObjectsMatchingConditions(e.objectType, Seq(AdjacentTo(o), ControlledBy(Opponent)))))}}), ((S\NP)/NP, λ {c: ObjectsMatchingConditions => λ {o: TargetObject => NotGC(CollectionExists(ObjectsMatchingConditions(c.objectType, c.conditions :+ AdjacentTo(o))))}}) )) + diff --git a/src/main/scala/wordbots/Parser.scala b/src/main/scala/wordbots/Parser.scala index 6dec20c..6497f22 100644 --- a/src/main/scala/wordbots/Parser.scala +++ b/src/main/scala/wordbots/Parser.scala @@ -1,7 +1,7 @@ package wordbots import com.workday.montague.ccg._ -import com.workday.montague.parser.{ParserDict, SemanticParseResult, SemanticParser} +import com.workday.montague.parser.{ParserDict, SemanticParseNode, SemanticParseResult, SemanticParser} import com.workday.montague.semantics._ import scala.language.postfixOps @@ -18,8 +18,11 @@ object Parser extends SemanticParser[CcgCat](Lexicon.lexicon) { val input = args.mkString(" ") val result: SemanticParseResult[CcgCat] = parse(input) - val output: String = result.bestParse.map(p => s"${p.semantic.toString} [${p.syntactic.toString}]").getOrElse("(failed to parse)") - val code: Try[String] = Try { result.bestParse.get.semantic }.flatMap { + val bestParse: Option[SemanticParseNode[CcgCat]] = ErrorAnalyzer.bestValidParse(result) + val allSemanticallyCompleteParses: List[String] = result.semanticCompleteParses.map(p => s"${p.semantic.toString} [${p.syntactic.toString}]") + + val output: String = bestParse.map(p => s"${p.semantic.toString} [${p.syntactic.toString}]").getOrElse("(failed to parse)") + val code: Try[String] = Try { bestParse.get.semantic }.flatMap { case Form(v: Semantics.AstNode) => CodeGenerator.generateJS(v) case _ => Failure(new RuntimeException("Parser did not produce a valid expression")) } @@ -28,12 +31,13 @@ object Parser extends SemanticParser[CcgCat](Lexicon.lexicon) { println(s"Input: $input") println(s"Tokens: ${tokenizer(input).mkString("[\"", "\", \"", "\"]")}") println(s"Parse result: $output") - println(s"Error diagnosis: ${ErrorAnalyzer.diagnoseError(input, result.bestParse)}") + println(s"All semantically complete parse results:\n ${allSemanticallyCompleteParses.mkString("\n ")}") + println(s"Error diagnosis: ${ErrorAnalyzer.diagnoseError(input, bestParse)}") println(s"Generated JS code: ${code.getOrElse(code.failed.get)}") // scalastyle:on regex // For debug purposes, output the best parse tree (if one exists) to SVG. - //result.bestParse.foreach(result => new java.io.PrintWriter("test.svg") { write(result.toSvg); close() }) + //bestParse.foreach(result => new java.io.PrintWriter("test.svg") { write(result.toSvg); close() }) } /** Parses the input. */ diff --git a/src/main/scala/wordbots/Server.scala b/src/main/scala/wordbots/Server.scala index 4acbe4f..5d38016 100644 --- a/src/main/scala/wordbots/Server.scala +++ b/src/main/scala/wordbots/Server.scala @@ -2,7 +2,7 @@ package wordbots import com.roundeights.hasher.Implicits._ import com.workday.montague.ccg.CcgCat -import com.workday.montague.parser.SemanticParseNode +import com.workday.montague.parser.{SemanticParseNode, SemanticParseResult} import com.workday.montague.semantics.{Form, SemanticState} import org.http4s.{Response => H4sResponse, _} import org.http4s.circe._ @@ -12,6 +12,7 @@ import org.http4s.server.blaze.BlazeBuilder import io.circe._ import io.circe.generic.auto._ import io.circe.syntax._ +import org.log4s.MDC.result import scalaz.Memo import scalaz.concurrent.Task import wordbots.Semantics.AstNode @@ -51,25 +52,26 @@ object MemoParser { case _ => ValidateUnknownCard } - val result = Parser.parse(input).bestParse - val parsedTokens = { - result.toSeq + val parseResult: SemanticParseResult[CcgCat] = Parser.parse(input) + val bestParse: Option[SemanticParseNode[CcgCat]] = ErrorAnalyzer.bestValidParse(parseResult) + val parsedTokens: Seq[String] = { + bestParse.toSeq .flatMap(_.terminals) .flatMap(_.parseTokens) .map(_.tokenString) .filter(token => Lexicon.listOfTerms.contains(token) && token != "\"") } - val unrecognizedTokens = ErrorAnalyzer.findUnrecognizedTokens(input) + val unrecognizedTokens: Seq[String] = ErrorAnalyzer.findUnrecognizedTokens(input) - ErrorAnalyzer.diagnoseError(input, result, fastErrorAnalysisMode) match { + ErrorAnalyzer.diagnoseError(input, bestParse, fastErrorAnalysisMode) match { case Some(error) => print(" [F]") FailedParse(error, unrecognizedTokens) case None => - result.map(_.semantic) match { + bestParse.map(_.semantic) match { case Some(Form(ast: AstNode)) => print(" [S]") - SuccessfulParse(result.get, ast, parsedTokens) + SuccessfulParse(bestParse.get, ast, parsedTokens) case _ => print(" [F]") FailedParse(ParserError("Unspecified parser error"), unrecognizedTokens) diff --git a/src/test/scala/wordbots/ErrorAnalyzerSpec.scala b/src/test/scala/wordbots/ErrorAnalyzerSpec.scala index 3101a51..7f8ada0 100644 --- a/src/test/scala/wordbots/ErrorAnalyzerSpec.scala +++ b/src/test/scala/wordbots/ErrorAnalyzerSpec.scala @@ -4,7 +4,7 @@ import org.scalatest._ class ErrorAnalyzerSpec extends FlatSpec with Matchers { def analyze(input: String): Option[ParserError] = { - val parseResult = Parser.parse(input).bestParse + val parseResult = ErrorAnalyzer.bestValidParse(Parser.parse(input)) ErrorAnalyzer.diagnoseError(input, parseResult) } diff --git a/src/test/scala/wordbots/ParserSpec.scala b/src/test/scala/wordbots/ParserSpec.scala index f0fcef7..f9cd153 100644 --- a/src/test/scala/wordbots/ParserSpec.scala +++ b/src/test/scala/wordbots/ParserSpec.scala @@ -13,7 +13,7 @@ class ParserSpec extends FlatSpec with Matchers { //scalastyle:off regex def parse(input: String, validationMode: ValidationMode = ValidateUnknownCard): Any = { println(s"Parsing $input...") - Parser.parse(input).bestParse match { + ErrorAnalyzer.bestValidParse(Parser.parse(input)) match { case Some(parse) => parse.semantic match { case Form(v: AstNode) => println(s" $v") @@ -424,8 +424,12 @@ class ParserSpec extends FlatSpec with Matchers { TriggeredAbility(AfterDestroyed(ThisObject), DealDamage(ObjectsMatchingConditions(AllObjects, Seq(WithinDistanceOf(Scalar(2), ThisObject))), Scalar(2))) //scalastyle:off magic.number - parse("When this robot is played, if it is adjacent to an enemy robot, it gains 5 health.") shouldEqual - TriggeredAbility(AfterPlayed(ThisObject), If(CollectionExists(ObjectsMatchingConditions(Robot, List(ControlledBy(Opponent), AdjacentTo(ItO)))), ModifyAttribute(ItO, Health, Plus(Scalar(5))))) + // (two possible parses here, both valid and both achieving the same result) + parse("When this robot is played, if it is adjacent to an enemy robot, it gains 5 health.") should ( + equal(TriggeredAbility(AfterPlayed(ThisObject), If(CollectionExists(ObjectsMatchingConditions(Robot, List(ControlledBy(Opponent), AdjacentTo(ItO)))), ModifyAttribute(ItO, Health, Plus(Scalar(5)))))) + or equal(TriggeredAbility(AfterPlayed(ThisObject), If(TargetMeetsCondition(ItO, AdjacentTo(ChooseO(ObjectsMatchingConditions(Robot, List(ControlledBy(Opponent))), Scalar(1)))), ModifyAttribute(ItO, Health, Plus(Scalar(5)))))) + ) + //scalastyle:on magic.number parse("When this robot attacks, it can attack again.") shouldEqual TriggeredAbility(AfterAttack(ThisObject), CanAttackAgain(ItO))