Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/main/scala/wordbots/AstValidator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
*/
Expand All @@ -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 {
Expand All @@ -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)
}
}
Expand Down
16 changes: 15 additions & 1 deletion src/main/scala/wordbots/ErrorAnalyzer.scala
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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] = {
Expand Down
6 changes: 4 additions & 2 deletions src/main/scala/wordbots/Lexicon.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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))))}})
)) +
Expand Down
14 changes: 9 additions & 5 deletions src/main/scala/wordbots/Parser.scala
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"))
}
Expand All @@ -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. */
Expand Down
18 changes: 10 additions & 8 deletions src/main/scala/wordbots/Server.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/test/scala/wordbots/ErrorAnalyzerSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
10 changes: 7 additions & 3 deletions src/test/scala/wordbots/ParserSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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))
Expand Down