Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pla 982 add bestemshe #185

Merged
merged 8 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
2 changes: 1 addition & 1 deletion src/main/scala/GameCollection.scala
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ object GameFamily {
def name = "Amazons"
def key = "amazons"
def gameLogic = GameLogic.FairySF()
def hasFishnet = true
def hasFishnet = false
def hasAnalysisBoard = false
def defaultVariant = Variant.FairySF(strategygames.fairysf.variant.Amazons)
def variants = Variant.all(GameLogic.FairySF()).filter(_.gameFamily == this)
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/fairysf/variant/Amazons.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ case object Amazons
override val switchPlayerAfterMove = false

override def hasAnalysisBoard: Boolean = false
override def hasFishnet: Boolean = true
override def hasFishnet: Boolean = false

// cache this rather than checking with the API everytime
override def initialFen =
Expand Down
8 changes: 7 additions & 1 deletion src/main/scala/togyzkumalak/Board.scala
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ object Board {
}

object BoardSize {
val all: List[BoardSize] = List(Dim9x2)
val all: List[BoardSize] = List(Dim9x2, Dim5x2)
}

case object Dim9x2
Expand All @@ -89,4 +89,10 @@ object Board {
height = 2
)

case object Dim5x2
extends BoardSize(
width = 5,
height = 2
)

}
4 changes: 2 additions & 2 deletions src/main/scala/togyzkumalak/File.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ object File {
val H = new File(7)
val I = new File(8)

val all = List(A, B, C, D, E, F, G, H, I)
val allReversed: List[File] = all.reverse
val all = List(A, B, C, D, E, F, G, H, I)
def allByWidth(width: Int) = all.take(width)
}
19 changes: 15 additions & 4 deletions src/main/scala/togyzkumalak/Pos.scala
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,15 @@ case class Pos private (index: Int) extends AnyVal {
def piotr: Char = Piotr.lookup.get(index).getOrElse('?')
def piotrStr = piotr.toString

def player: Player = if (index < 9) Player.P1 else Player.P2
def player: Player = if (index < File.all.size) Player.P1 else Player.P2

def last: Boolean = (index + 1) % 9 == 0
def last(width: Int): Boolean =
if (rank.index == 0) (index + 1) % width == 0
else index == Pos.all.size - 1

def indexByWidth(width: Int): Int =
if (rank.index == 0) index
else index - (File.all.size - width) * 2

def key = file.toString + rank.toString
override def toString = key
Expand All @@ -88,14 +94,17 @@ object Pos {
if (0 <= index && index < File.all.size * Rank.all.size) Some(new Pos(index))
else None

def apply(file: File, rank: Rank): Pos = new Pos(file.index + File.all.size * rank.index)
def apply(file: File, rank: Rank): Pos =
if (rank.index == 0) new Pos(file.index)
else new Pos(File.all.size * (rank.index + 1) - 1 - file.index)

def at(x: Int, y: Int): Option[Pos] =
if (0 <= x && x < File.all.size && 0 <= y && y < Rank.all.size)
Some(new Pos(x + (File.all.size - x) * y))
else None

def opposite(index: Int): Option[Pos] = apply(if (index < 9) index + 9 else index - 9)
def opposite(index: Int): Option[Pos] =
apply(if (index < File.all.size) index + File.all.size else index - File.all.size)

def fromKey(key: String): Option[Pos] = allKeys get key

Expand Down Expand Up @@ -137,6 +146,8 @@ object Pos {
// if adding new Pos check for use of Pos.all
val all: List[Pos] = (0 to (File.all.size * Rank.all.size) - 1).map(new Pos(_)).toList

def allByWidth(width: Int): List[Pos] = all.filter(_.file.index < width)

val allKeys: Map[String, Pos] = all
.map { pos =>
pos.key -> pos
Expand Down
14 changes: 7 additions & 7 deletions src/main/scala/togyzkumalak/format/FEN.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package strategygames.togyzkumalak.format

import strategygames.Player
import strategygames.togyzkumalak.{ Piece, PieceMap, Pos, Stone, Tuzdik }
import strategygames.togyzkumalak.{ File, Piece, PieceMap, Pos, Stone, Tuzdik }

final case class FEN(value: String) extends AnyVal {

Expand All @@ -24,7 +24,7 @@ final case class FEN(value: String) extends AnyVal {
private def intFromFen(index: Int): Option[Int] =
value.split(' ').lift(index).flatMap(_.toIntOption)

private def width: Int = 9
private def width: Int = mancalaStoneArray.size / 2

def mancalaStoneArray: Array[Int] =
(
Expand All @@ -34,9 +34,9 @@ final case class FEN(value: String) extends AnyVal {
)
.map(c =>
c.toString() match {
case x if 1 to width map (_.toString) contains x => Array.fill(x.toInt)(0)
case x if x.length > 1 => Array(c.dropRight(1).toInt)
case _ => Array(-1)
case x if 1 to File.all.size map (_.toString) contains x => Array.fill(x.toInt)(0)
case x if x.length > 1 => Array(c.dropRight(1).toInt)
case _ => Array(-1)
}
)
.flatten
Expand All @@ -46,8 +46,8 @@ final case class FEN(value: String) extends AnyVal {
mancalaStoneArray.zipWithIndex
.filterNot { case (s, _) => s == 0 }
.map { case (stones, index) =>
Pos(index) -> (if (stones == -1) (Tuzdik, 1)
else (Stone, stones))
Pos.allByWidth(width).lift(index) -> (if (stones == -1) (Tuzdik, 1)
else (Stone, stones))
}
.flatMap {
case (Some(pos), (r, c)) if r == Tuzdik => Some((pos -> Tuple2(Piece(!pos.player, r), c)))
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/togyzkumalak/format/Forsyth.scala
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ object Forsyth {
var empty = 0
for (y <- Rank.allReversed) {
empty = 0
val files = if (y.index == 0) File.all else File.allReversed
val files = File.allByWidth(board.variant.boardSize.width)
for (x <- files) {
board(x, y) match {
case None => empty = empty + 1
Expand Down
47 changes: 47 additions & 0 deletions src/main/scala/togyzkumalak/variant/Bestemshe.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package strategygames.togyzkumalak
package variant

import strategygames.togyzkumalak._
import strategygames.{ GameFamily, Player }

case object Bestemshe
extends Variant(
id = 2,
key = "bestemshe",
name = "Bestemshe",
standardInitialPosition = false,
boardSize = Board.Dim5x2
) {

def gameFamily: GameFamily = GameFamily.Togyzkumalak()

def perfIcon: Char = ''
def perfId: Int = 401

override def baseVariant: Boolean = false

override def canOfferDraw = false

// cache this rather than checking with the API everytime
override def initialFen =
format.FEN("5S,5S,5S,5S,5S/5S,5S,5S,5S,5S 0 0 S 1")

override def usesTuzdik = false

// TODO check legalMoves.size == 0 condition
override def specialEnd(situation: Situation) =
(situation.board.history.score.p1 > 25) ||
(situation.board.history.score.p2 > 25) ||
(situation.moves.size == 0)

override def specialDraw(situation: Situation) =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't you want to change this to false ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured best to leave this clause in (which wont be triggered with regular Bestemshe) but when we get to the point where we are offering FromPosition for all variants then it could be possible to create a position (fen) where the scores start as odd and so a draw is possible. if i remove this code then i think it will just look in winner and award p2 as a winner in these 'tie' situations.

situation.board.history.score.p1 == situation.board.history.score.p2

override def winner(situation: Situation): Option[Player] =
if (specialEnd(situation) && !specialDraw(situation)) {
if (situation.board.history.score.p1 > situation.board.history.score.p2)
Player.fromName("p1")
else Player.fromName("p2")
} else None

}
41 changes: 28 additions & 13 deletions src/main/scala/togyzkumalak/variant/Variant.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,52 +47,66 @@ abstract class Variant private[variant] (

def startPlayer: Player = P1

def usesTuzdik: Boolean = true

// looks like this is only to allow King to be a valid promotion piece
// in just atomic, so can leave as true for now
def isValidPromotion(@nowarn promotion: Option[PromotableRole]): Boolean = true

private def destFromOrig(pos: Pos, count: Int): Pos =
(if (count == 1) Pos((pos.index + 1) % 18)
else Pos((pos.index + count - 1) % 18)) match {
private def destFromOrig(pos: Pos, count: Int, width: Int): Pos =
(if (count == 1) Pos.allByWidth(width).lift((pos.indexByWidth(width) + 1) % (width * 2))
else Pos.allByWidth(width).lift((pos.indexByWidth(width) + count - 1) % (width * 2))) match {
case Some(dest) => dest
case None => sys.error(s"Invalid dest from orig(${pos.index}) in togyz move(${count})")
case None => sys.error(s"Invalid dest from orig(${pos.index}) in togyz(${width}) move(${count})")
}

private lazy val emptyPieceMap: PieceMap =
Pos.all.map(pos => (pos, (Piece(pos.player, Stone), 0))).toMap
Pos.allByWidth(boardSize.width).map(pos => (pos, (Piece(pos.player, Stone), 0))).toMap

private def pieceMapWithEmpties(pieces: PieceMap): PieceMap = emptyPieceMap.map {
case (pos, _) if pieces.get(pos).nonEmpty => (pos -> pieces(pos))
case piece => piece
}

private def stonesAfterMove(origStones: Int, thisStones: Int, origIndex: Int, thisIndex: Int): Int = {
val thisDiff = if (thisIndex < origIndex) thisIndex + 18 else thisIndex;
val thisDiff = if (thisIndex < origIndex) thisIndex + (boardSize.width * 2) else thisIndex;
if (origStones == 1) {
if (origIndex == thisIndex) 0
else if (thisDiff - origIndex == 1) thisStones + 1
else thisStones
} else {
val remainder = if ((thisDiff - origIndex) < origStones % 18) 1 else 0;
val remainder = if ((thisDiff - origIndex) < origStones % (boardSize.width * 2)) 1 else 0;
val remaining = if (origIndex == thisIndex) 0 else thisStones
remaining + (origStones / 18) + remainder
remaining + (origStones / (boardSize.width * 2)) + remainder
}
}

def piecesAfterMove(pieces: PieceMap, orig: Pos, dest: Pos, oppTuzdik: Option[Pos]): PieceMap =
pieceMapWithEmpties(pieces)
.map {
case (pos, posInfo) if posInfo._1.role != Tuzdik =>
(pos, (posInfo._1, stonesAfterMove(pieces(orig)._2, posInfo._2, orig.index, pos.index)))
(
pos,
(
posInfo._1,
stonesAfterMove(
pieces(orig)._2,
posInfo._2,
orig.indexByWidth(boardSize.width),
pos.indexByWidth(boardSize.width)
)
)
)
case (pos, posInfo) => (pos, posInfo)
}
// now remove stones
.map {
case (pos, posInfo) if pos == dest && orig.player != dest.player && posInfo._2 % 2 == 0 =>
(pos, (posInfo._1, 0))
case (pos, posInfo) if pos == dest && orig.player != dest.player && posInfo._2 == 3 && pieces.filter {
case (pos, posInfo)
if usesTuzdik && pos == dest && orig.player != dest.player && posInfo._2 == 3 && pieces.filter {
case (pos2, posInfo2) => posInfo2._1.role == Tuzdik && pos2.player == dest.player
}.isEmpty && oppTuzdik != Pos.opposite(dest.index) && !dest.last =>
}.isEmpty && oppTuzdik != Pos.opposite(dest.index) && !dest.last(boardSize.width) =>
(pos, (Piece(!pos.player, Tuzdik), 1))
case (pos, posInfo) => (pos, posInfo)
}
Expand Down Expand Up @@ -128,7 +142,7 @@ abstract class Variant private[variant] (
}
.map {
case (pos, posInfo) => {
val dest = destFromOrig(pos, posInfo._2);
val dest = destFromOrig(pos, posInfo._2, boardSize.width);
(
pos,
List(
Expand Down Expand Up @@ -208,7 +222,8 @@ abstract class Variant private[variant] (
object Variant {

lazy val all: List[Variant] = List(
Togyzkumalak
Togyzkumalak,
Bestemshe
)
val byId = all map { v =>
(v.id, v)
Expand Down
9 changes: 7 additions & 2 deletions src/test/scala/backgammon/BackgammonTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.specs2.matcher.ValidatedMatchers
import org.specs2.mutable.Specification

import strategygames.backgammon.format.Uci
import strategygames.backgammon.variant.Variant

class BackgammonTest extends Specification with ValidatedMatchers {

Expand All @@ -13,9 +14,13 @@ class BackgammonTest extends Specification with ValidatedMatchers {
vg.flatMap { g => g.apply(action).map(_._1) }
}

def playActionStrs(actionStrs: List[String], game: Option[Game] = None): Validated[String, Game] =
def playActionStrs(
actionStrs: List[String],
game: Option[Game] = None,
variant: Option[Variant] = None
): Validated[String, Game] =
playUciList(
game.getOrElse(Game.apply(variant.Backgammon)),
game.getOrElse(Game.apply(variant.getOrElse(Variant.default))),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a difference here between writing Game.apply(<some_param>) and Game(<some_param>) ?
If not, do you prefer I write the apply in the tests I am currently writing for Abalone ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both Game.apply(variant) and Game(variant) work here. Personally I prefer Game() to be used when you are instantiating an instance of Game with its paramter-set and call Game.apply when you are explicitly instantiating a Game through an apply method (in this case

def apply(variant: strategygames.togyzkumalak.variant.Variant): Game =
).
Despite that being my personal preference, I don't think this standard is followed throughout the codebase and I have no problem with you doing either approach

Uci.readList(actionStrs.mkString(" ")).getOrElse(List())
)

Expand Down
77 changes: 77 additions & 0 deletions src/test/scala/togyzkumalak/BestemsheVariantTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package strategygames.togyzkumalak

import org.specs2.matcher.ValidatedMatchers

import strategygames.{ Player, Score }

import strategygames.togyzkumalak.variant.Bestemshe

class BestemsheVariantTest extends TogyzkumalakTest with ValidatedMatchers {

"valid moves in initial situation" should {
val board = Board.init(Bestemshe)
val situation = Situation(board, Player.P1)

val moves = Bestemshe.validMoves(situation)
"be valid" in {
moves.size must_== 5
}
}

"valid opening moves" should {
"valid situation after first move" in {
playActionStrs(List("c1d2"), variant = Some(Bestemshe)) must beValid.like { g =>
g.situation.player must_== Player.P2
g.situation.moves.size must_== 4
g.situation.board.history.score must_== Score(6, 0)
g.situation.end must_== false
}
}
"valid situation after first two moves" in {
playActionStrs(List("c1d2", "c2b1"), variant = Some(Bestemshe)) must beValid.like { g =>
g.situation.player must_== Player.P1
g.situation.moves.size must_== 4
g.situation.board.history.score must_== Score(6, 6)
}
}
}

// no tuzdik created
// "tuzdik rules are respected" should {
// val actionStrs = List("f1e2", "d2e1", "i1a2", "b2i1", "b1g2", "f2e1")
// "no tuzdiks initially" in {
// playActionStrs(actionStrs.dropRight(1)) must beValid.like { g =>
// g.situation.player must_== Player.P2
// g.situation.board.pieces.filter {
// case (_, (p, _)) if p.role == Tuzdik => true; case _ => false
// }.size must_== 0
// g.situation.board.pieces(Pos.E1) must_== ((Piece(Player.P1, Stone), 2))
// g.situation.board.history.score must_== Score(22, 12)
// }
// }
// "tuzdik created when landing on space with 2 stones" in {
// playActionStrs(actionStrs) must beValid.like { g =>
// g.situation.player must_== Player.P1
// g.situation.board.pieces
// .filter {
// case (_, (p, _)) if p.role == Tuzdik => true; case _ => false
// } must_== Map(Pos.E1 -> ((Piece(Player.P2, Tuzdik), 1)))
// g.situation.board.history.score must_== Score(22, 15)
// }
// }
// }

// "game ends properly" should {
// // https://playstrategy.org/FgWSk5be
// val actionStrs = List(
// )
// "when a player has > 25 stones" in {
// playActionStrs(actionStrs) must beValid.like { g =>
// g.situation.end must_== true
// g.situation.winner must_== Some(Player.P1)
// g.situation.board.history.score must_== Score(82, 43)
// }
// }
// }

}
Loading