feat: NCS-40 Rework Draw System (#34)
Reviewed-on: #34 Reviewed-by: Shahd Lala <shosho996@blackhole.local> Co-authored-by: Janis <janis.e.20@gmx.de> Co-committed-by: Janis <janis.e.20@gmx.de>
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
package de.nowchess.chess.engine
|
||||
|
||||
import scala.collection.mutable
|
||||
import de.nowchess.api.board.Color
|
||||
import de.nowchess.api.game.{DrawReason, GameResult}
|
||||
import de.nowchess.chess.observer.{
|
||||
DrawEvent,
|
||||
DrawOfferDeclinedEvent,
|
||||
DrawOfferEvent,
|
||||
GameEvent,
|
||||
InvalidMoveEvent,
|
||||
InvalidMoveReason,
|
||||
Observer,
|
||||
}
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class GameEngineDrawOfferTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("White offers draw"):
|
||||
val engine = new GameEngine()
|
||||
val observer = new DrawOfferMockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
engine.offerDraw(Color.White)
|
||||
|
||||
observer.events should have length 1
|
||||
observer.events.head match
|
||||
case event: DrawOfferEvent =>
|
||||
event.offeredBy shouldBe Color.White
|
||||
case other =>
|
||||
fail(s"Expected DrawOfferEvent, but got $other")
|
||||
|
||||
test("Black accepts White's draw offer"):
|
||||
val engine = new GameEngine()
|
||||
val observer = new DrawOfferMockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
engine.offerDraw(Color.White)
|
||||
observer.events.clear()
|
||||
engine.acceptDraw(Color.Black)
|
||||
|
||||
observer.events should have length 1
|
||||
observer.events.head match
|
||||
case event: DrawEvent =>
|
||||
event.reason shouldBe DrawReason.Agreement
|
||||
event.context.result shouldBe Some(GameResult.Draw(DrawReason.Agreement))
|
||||
case other =>
|
||||
fail(s"Expected DrawEvent, but got $other")
|
||||
|
||||
test("Black declines White's draw offer"):
|
||||
val engine = new GameEngine()
|
||||
val observer = new DrawOfferMockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
engine.offerDraw(Color.White)
|
||||
observer.events.clear()
|
||||
engine.declineDraw(Color.Black)
|
||||
|
||||
observer.events should have length 1
|
||||
observer.events.head match
|
||||
case event: DrawOfferDeclinedEvent =>
|
||||
event.declinedBy shouldBe Color.Black
|
||||
case other =>
|
||||
fail(s"Expected DrawOfferDeclinedEvent, but got $other")
|
||||
|
||||
test("Black offers draw"):
|
||||
val engine = new GameEngine()
|
||||
val observer = new DrawOfferMockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
engine.offerDraw(Color.Black)
|
||||
|
||||
observer.events should have length 1
|
||||
observer.events.head match
|
||||
case event: DrawOfferEvent =>
|
||||
event.offeredBy shouldBe Color.Black
|
||||
case other =>
|
||||
fail(s"Expected DrawOfferEvent, but got $other")
|
||||
|
||||
test("White accepts Black's draw offer"):
|
||||
val engine = new GameEngine()
|
||||
val observer = new DrawOfferMockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
engine.offerDraw(Color.Black)
|
||||
observer.events.clear()
|
||||
engine.acceptDraw(Color.White)
|
||||
|
||||
observer.events should have length 1
|
||||
observer.events.head match
|
||||
case event: DrawEvent =>
|
||||
event.reason shouldBe DrawReason.Agreement
|
||||
event.context.result shouldBe Some(GameResult.Draw(DrawReason.Agreement))
|
||||
case other =>
|
||||
fail(s"Expected DrawEvent, but got $other")
|
||||
|
||||
test("Cannot accept draw when no offer pending"):
|
||||
val engine = new GameEngine()
|
||||
val observer = new DrawOfferMockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
engine.acceptDraw(Color.Black)
|
||||
|
||||
observer.events should have length 1
|
||||
observer.events.head match
|
||||
case event: InvalidMoveEvent =>
|
||||
event.reason shouldBe InvalidMoveReason.NoDrawOfferToAccept
|
||||
case other =>
|
||||
fail(s"Expected InvalidMoveEvent, but got $other")
|
||||
|
||||
test("Cannot decline draw when no offer pending"):
|
||||
val engine = new GameEngine()
|
||||
val observer = new DrawOfferMockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
engine.declineDraw(Color.Black)
|
||||
|
||||
observer.events should have length 1
|
||||
observer.events.head match
|
||||
case event: InvalidMoveEvent =>
|
||||
event.reason shouldBe InvalidMoveReason.NoDrawOfferToDecline
|
||||
case other =>
|
||||
fail(s"Expected InvalidMoveEvent, but got $other")
|
||||
|
||||
test("Cannot offer draw when game is already over"):
|
||||
val engine = new GameEngine()
|
||||
val observer = new DrawOfferMockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
// End the game with checkmate
|
||||
engine.processUserInput("f2f3")
|
||||
engine.processUserInput("e7e5")
|
||||
engine.processUserInput("g2g4")
|
||||
observer.events.clear()
|
||||
engine.processUserInput("d8h4")
|
||||
|
||||
// Try to offer draw
|
||||
observer.events.clear()
|
||||
engine.offerDraw(Color.White)
|
||||
|
||||
observer.events should have length 1
|
||||
observer.events.head match
|
||||
case event: InvalidMoveEvent =>
|
||||
event.reason shouldBe InvalidMoveReason.GameAlreadyOver
|
||||
case other =>
|
||||
fail(s"Expected InvalidMoveEvent, but got $other")
|
||||
|
||||
test("Cannot accept your own draw offer"):
|
||||
val engine = new GameEngine()
|
||||
val observer = new DrawOfferMockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
engine.offerDraw(Color.White)
|
||||
observer.events.clear()
|
||||
engine.acceptDraw(Color.White)
|
||||
|
||||
observer.events should have length 1
|
||||
observer.events.head match
|
||||
case event: InvalidMoveEvent =>
|
||||
event.reason shouldBe InvalidMoveReason.CannotAcceptOwnDrawOffer
|
||||
case other =>
|
||||
fail(s"Expected InvalidMoveEvent, but got $other")
|
||||
|
||||
test("Cannot decline your own draw offer"):
|
||||
val engine = new GameEngine()
|
||||
val observer = new DrawOfferMockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
engine.offerDraw(Color.White)
|
||||
observer.events.clear()
|
||||
engine.declineDraw(Color.White)
|
||||
|
||||
observer.events should have length 1
|
||||
observer.events.head match
|
||||
case event: InvalidMoveEvent =>
|
||||
event.reason shouldBe InvalidMoveReason.CannotDeclineOwnDrawOffer
|
||||
case other =>
|
||||
fail(s"Expected InvalidMoveEvent, but got $other")
|
||||
|
||||
test("Cannot make second draw offer when one is already pending"):
|
||||
val engine = new GameEngine()
|
||||
val observer = new DrawOfferMockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
engine.offerDraw(Color.White)
|
||||
observer.events.clear()
|
||||
engine.offerDraw(Color.Black)
|
||||
|
||||
observer.events should have length 1
|
||||
observer.events.head match
|
||||
case event: InvalidMoveEvent =>
|
||||
event.reason shouldBe InvalidMoveReason.DrawOfferPending
|
||||
case other =>
|
||||
fail(s"Expected InvalidMoveEvent, but got $other")
|
||||
|
||||
test("Draw offer is cleared when game ends by resignation (accept)"):
|
||||
val engine = new GameEngine()
|
||||
val observer = new DrawOfferMockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
engine.offerDraw(Color.White)
|
||||
observer.events.clear()
|
||||
engine.resign(Color.Black)
|
||||
|
||||
// Try to accept the now-cleared draw offer
|
||||
observer.events.clear()
|
||||
engine.acceptDraw(Color.White)
|
||||
|
||||
observer.events should have length 1
|
||||
observer.events.head match
|
||||
case event: InvalidMoveEvent =>
|
||||
event.reason shouldBe InvalidMoveReason.GameAlreadyOver
|
||||
case other =>
|
||||
fail(s"Expected InvalidMoveEvent, but got $other")
|
||||
|
||||
test("Draw offer is cleared when game ends by resignation (decline)"):
|
||||
val engine = new GameEngine()
|
||||
val observer = new DrawOfferMockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
engine.offerDraw(Color.White)
|
||||
observer.events.clear()
|
||||
engine.resign(Color.Black)
|
||||
|
||||
// Try to accept the now-cleared draw offer
|
||||
observer.events.clear()
|
||||
engine.declineDraw(Color.White)
|
||||
|
||||
observer.events should have length 1
|
||||
observer.events.head match
|
||||
case event: InvalidMoveEvent =>
|
||||
event.reason shouldBe InvalidMoveReason.GameAlreadyOver
|
||||
case other =>
|
||||
fail(s"Expected InvalidMoveEvent, but got $other")
|
||||
|
||||
private class DrawOfferMockObserver extends Observer:
|
||||
val events = mutable.ListBuffer[GameEvent]()
|
||||
|
||||
override def onGameEvent(event: GameEvent): Unit =
|
||||
events += event
|
||||
+3
-3
@@ -3,7 +3,7 @@ package de.nowchess.chess.engine
|
||||
import de.nowchess.api.board.{Board, Color, File, PieceType, Rank, Square}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import de.nowchess.chess.observer.{GameEvent, InvalidMoveEvent, MoveRedoneEvent, Observer}
|
||||
import de.nowchess.chess.observer.{GameEvent, InvalidMoveEvent, InvalidMoveReason, MoveRedoneEvent, Observer}
|
||||
import de.nowchess.io.GameContextImport
|
||||
import de.nowchess.rules.RuleSet
|
||||
import de.nowchess.rules.sets.DefaultRules
|
||||
@@ -50,8 +50,8 @@ class GameEngineIntegrationTest extends AnyFunSuite with Matchers:
|
||||
engine.processUserInput("e2e5")
|
||||
|
||||
events.exists {
|
||||
case InvalidMoveEvent(_, reason) => reason.contains("Illegal move")
|
||||
case _ => false
|
||||
case InvalidMoveEvent(_, InvalidMoveReason.IllegalMove) => true
|
||||
case _ => false
|
||||
} shouldBe true
|
||||
|
||||
test("loadGame returns Left when importer fails"):
|
||||
|
||||
@@ -4,7 +4,16 @@ import de.nowchess.api.board.{Board, Color, File, Piece, PieceType, Rank, Square
|
||||
import de.nowchess.api.game.{DrawReason, GameContext}
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import de.nowchess.io.fen.FenParser
|
||||
import de.nowchess.chess.observer.*
|
||||
import de.nowchess.chess.observer.{
|
||||
CheckDetectedEvent,
|
||||
CheckmateEvent,
|
||||
DrawEvent,
|
||||
GameEvent,
|
||||
InvalidMoveEvent,
|
||||
InvalidMoveReason,
|
||||
MoveExecutedEvent,
|
||||
Observer,
|
||||
}
|
||||
import de.nowchess.rules.RuleSet
|
||||
import de.nowchess.rules.sets.DefaultRules
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
@@ -30,8 +39,8 @@ class GameEnginePromotionTest extends AnyFunSuite with Matchers:
|
||||
engine.processUserInput("e7e8")
|
||||
|
||||
events.exists {
|
||||
case InvalidMoveEvent(_, reason) => reason.contains("Promotion piece required")
|
||||
case _ => false
|
||||
case InvalidMoveEvent(_, InvalidMoveReason.PromotionPieceRequired) => true
|
||||
case _ => false
|
||||
} should be(true)
|
||||
}
|
||||
|
||||
@@ -198,5 +207,5 @@ class GameEnginePromotionTest extends AnyFunSuite with Matchers:
|
||||
case _ => false
|
||||
} should be(true)
|
||||
val invalidEvt = events.collect { case e: InvalidMoveEvent => e }.last
|
||||
invalidEvt.reason should include("Error completing promotion")
|
||||
invalidEvt.reason shouldBe InvalidMoveReason.PromotionPieceInvalid
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package de.nowchess.chess.engine
|
||||
|
||||
import scala.collection.mutable
|
||||
import de.nowchess.api.board.Color
|
||||
import de.nowchess.api.game.GameResult
|
||||
import de.nowchess.chess.observer.{GameEvent, InvalidMoveEvent, InvalidMoveReason, Observer, ResignEvent}
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class GameEngineResignTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("White resigns"):
|
||||
val engine = new GameEngine()
|
||||
val observer = new ResignMockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
engine.resign(Color.White)
|
||||
|
||||
observer.events should have length 1
|
||||
observer.events.head match
|
||||
case event: ResignEvent =>
|
||||
event.resignedColor shouldBe Color.White
|
||||
event.context.result shouldBe Some(GameResult.Win(Color.Black))
|
||||
case other =>
|
||||
fail(s"Expected ResignEvent, but got $other")
|
||||
|
||||
test("Black resigns"):
|
||||
val engine = new GameEngine()
|
||||
val observer = new ResignMockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
engine.resign(Color.Black)
|
||||
|
||||
observer.events should have length 1
|
||||
observer.events.head match
|
||||
case event: ResignEvent =>
|
||||
event.resignedColor shouldBe Color.Black
|
||||
event.context.result shouldBe Some(GameResult.Win(Color.White))
|
||||
case other =>
|
||||
fail(s"Expected ResignEvent, but got $other")
|
||||
|
||||
test("Cannot resign when game is already over"):
|
||||
val engine = new GameEngine()
|
||||
val observer = new ResignMockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
// End the game with checkmate
|
||||
engine.processUserInput("f2f3")
|
||||
engine.processUserInput("e7e5")
|
||||
engine.processUserInput("g2g4")
|
||||
observer.events.clear()
|
||||
engine.processUserInput("d8h4")
|
||||
|
||||
// Try to resign
|
||||
observer.events.clear()
|
||||
engine.resign(Color.White)
|
||||
|
||||
// Should get InvalidMoveEvent with GameAlreadyOver reason
|
||||
observer.events.length shouldBe 1
|
||||
observer.events.head match
|
||||
case event: InvalidMoveEvent =>
|
||||
event.reason shouldBe InvalidMoveReason.GameAlreadyOver
|
||||
case other =>
|
||||
fail(s"Expected InvalidMoveEvent, but got $other")
|
||||
|
||||
private class ResignMockObserver extends Observer:
|
||||
val events = mutable.ListBuffer[GameEvent]()
|
||||
|
||||
override def onGameEvent(event: GameEvent): Unit =
|
||||
events += event
|
||||
Reference in New Issue
Block a user