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

validMoves and boardAfter #188

Draft
wants to merge 26 commits into
base: 503-abalone
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5d63998
feat: moves of 2 marbles
vincentfrochot Oct 21, 2024
033fd13
chore: codestyle
vincentfrochot Oct 21, 2024
471f1bc
feat: generate movesOf2 based on pairs of marbles
vincentfrochot Oct 21, 2024
2fc34c9
feat: generate validMoves
vincentfrochot Oct 23, 2024
b8a3c35
chore: move sideMoves methods under parent one
vincentfrochot Oct 23, 2024
8d08872
chore: use dropRight on List
vincentfrochot Oct 23, 2024
17357a6
chore: map(func) equals map(func(_))
vincentfrochot Oct 23, 2024
70b3abd
chore: situation.board.piecesOf(x) as val
vincentfrochot Oct 23, 2024
832dc59
chore: codestyle & reword some tests
vincentfrochot Oct 23, 2024
14b016e
chore: codestyle - direction can be computed from 2 Pos
vincentfrochot Oct 23, 2024
ece31e5
chore: refactor side moves
vincentfrochot Oct 24, 2024
d8f559b
chore: improve codestyle
vincentfrochot Oct 24, 2024
763e0db
feat: boardAfter
vincentfrochot Oct 25, 2024
ef24f2e
chore: use Directions instead of Option[String]
vincentfrochot Oct 25, 2024
6b42fcb
chore: remove done TODO
vincentfrochot Oct 25, 2024
e5f54a4
feat: 3fold, improve codestyle
vincentfrochot Oct 29, 2024
c75eea8
chore: codestyle Variant.piecesAfterAction
vincentfrochot Oct 29, 2024
93be03b
fix: Game make use of metrics
vincentfrochot Oct 29, 2024
8e3b9dc
chore: remove copy pasted false comment
vincentfrochot Oct 29, 2024
3e2f3be
fix: get rid of autoEndTurn in Move
vincentfrochot Oct 29, 2024
cfc8552
fix: revert autoEndTurn to get it back
vincentfrochot Oct 29, 2024
182321a
fix: side moves pieceMap update
vincentfrochot Oct 29, 2024
92d5961
feat: remove move cat in validMovesOf2And3
vincentfrochot Oct 29, 2024
69066c6
chore: remove unused dir as String methods
vincentfrochot Oct 29, 2024
4d85c75
feat: update PieceMap based on 2 Pos only
vincentfrochot Oct 30, 2024
98de331
feat: replayMove invoke boardAfter
vincentfrochot Oct 30, 2024
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: 2 additions & 0 deletions src/main/scala/abalone/Board.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ case class Board(

def piecesOf(player: Player): PieceMap = pieces.filter(_._2.is(player))

def isEmptySquare(pos: Option[Pos]): Boolean = pos.fold(false)(!this.pieces.contains(_))

def withHistory(h: History): Board = copy(history = h)
def updateHistory(f: History => History) = copy(history = f(history))

Expand Down
40 changes: 38 additions & 2 deletions src/main/scala/abalone/Pos.scala
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,43 @@ case class Pos private (index: Int) extends AnyVal {
def right: Option[Pos] = Pos.at(file.index + 1, rank.index)
def downRight: Option[Pos] = Pos.at(file.index, rank.index - 1)
def upRight: Option[Pos] = Pos.at(file.index + 1, rank.index + 1)
def neighbours: List[Option[Pos]] = List(left, downLeft, upLeft, right, downRight, upRight)

def neighbours: List[Option[Pos]] = List(left, upLeft, upRight, right, downRight, downLeft)
def sideMovesDirsFromDir(dir: Option[String]): (Option[Pos], Option[Pos]) = dir match {
case Some("left") => (this.downLeft, this.upLeft)
case Some("upLeft") => (this.left, this.upRight)
case Some("upRight") => (this.upLeft, this.right)
case Some("right") => (this.upRight, this.downRight)
case Some("downRight") => (this.right, this.downLeft)
case Some("downLeft") => (this.downRight, this.left)
case _ => (None, None)
}

// NOTE - *neighbourhood
// these below only work for neighbour pos but that's probably fine as in Abalone we only move to (potentially extended) neighbourhood
def dir(dir: Option[String]): Option[Pos] = dir match {
case Some("left") => this.left
case Some("upLeft") => this.upLeft
case Some("upRight") => this.upRight
case Some("right") => this.right
case Some("downRight") => this.downRight
case Some("downLeft") => this.downLeft
case _ => None
}

def dir(pos: Pos): Option[String] =
(pos.file.index - this.file.index, pos.rank.index - this.rank.index) match {
case (0, 1) => Some("upLeft")
case (0, -1) => Some("downRight")
case (1, 1) => Some("upRight")
case (1, 0) => Some("right")
case (-1, 0) => Some("left")
case (-1, -1) => Some("downLeft")
case _ => None
}

def isInLine(pos1: Pos, pos2: Pos): Boolean = this.dir(pos1) == pos1.dir(pos2)
// *end of note about neighbourhood

@inline def file = File of this // column (as if it was an index in a 1D array)
@inline def rank = Rank of this // horizontal row, makes sense in a 2D array
Expand Down Expand Up @@ -238,7 +274,7 @@ case class Pos private (index: Int) extends AnyVal {

def key = file.toString + rank.toString
def officialNotationKey = s"${File(rank.index).getOrElse("")}${Rank(file.index).getOrElse("")}"
override def toString = officialNotationKey
override def toString = key
}

object Pos {
Expand Down
194 changes: 133 additions & 61 deletions src/main/scala/abalone/variant/Variant.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,52 +46,52 @@ abstract class Variant private[variant] (

def startPlayer: Player = P1

val kingPiece: Option[Role] = None

// 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

/*
In Abalone there are 3 kinds of moves.
Let's use the top 2 rows of a board to illustrate (numbers are empty squares, Z is an opponent marble)
Let's use the top 3 rows of a board to illustrate (capital letters are empty squares, 1 is an opponent marble)

Z a b c 7
1 2 3 4 5 6
A B C D E
1 a b c G H
I J K L M N O

- line moves are marbles moving in line to an empty square :
'a' to '7' would move three marbles to the right. This is the same as if 'a' jumped over 'b' and 'c' to land on '7'.
'c' to '7' would move one marble to the right.
'a' to 'G' would move three marbles to the right.
This is the same as if 'a' jumped over 'b' and 'c' to land on 'G'.
'c' to 'G' would move one marble to the right.
- pushes are line moves targeting a square hosting an opponent marble
They will be processed as two line moves (one jump per player).
'c' to 'Z' and 'b' to 'Z' are pushes to the left
They will be processed later on as two line moves (one jump per player).
'c' to '1' and 'b' to '1' are pushes to the left.
In case we play a move from 'c' to '1', c will land on 1 and 1 will be removed from the board.
When a piece is pushed off the board, the Move is created with an extra parameter.
- side moves can only be described starting from the right origin (figure out the longest diagonal) :
'a' to '5' is a downRight side move of three marbles.
'c' to '2' is a downLeft side move of three marbles.
'a' to '4' is a downRight side move of two marbles (only 'a' and 'b' would move).
'c' to '3' is a downLeft side move of two marbles (only 'c' and 'b' would move)
'a' to '3' or 'c' to '4' are line moves of a single marble

we want to have :
1. moves of 1 marble + side moves (as we are reusing moves of 1 marble)
2. line moves of 2+ marbles + pushes ((re-do a pass on line moves that were stuck ?))
'a' to 'M' is a downRight side move of three marbles.
'c' to 'J' is a downLeft side move of three marbles.
'a' to 'L' is a downRight side move of two marbles (only 'a' and 'b' would move).
'c' to 'K' is a downLeft side move of two marbles (only 'c' and 'b' would move)
'a' to 'K' or 'c' to 'L' are line moves of a single marble

For moves of 2 marbles or more, once you get the direction of the line,
you can easily generate the side moves as being the one before and the one after,
following a rotation :
\ /
- o -
/ \
e.g. if you are moving upRight the side moves to consider are "upLeft" and "right".

1. moves of 1 marble
2. generate any possible pair of marbles and use it to generate moves of 2 and 3 marbles
then merge these as valid moves.
*/
def validMoves(situation: Situation): Map[Pos, List[Move]] = {
val movesOf1 = validMovesOf1(situation)
val lineMoves = movesOf1 ++ validLineMoves(situation).map {
case (k, v) => k -> (v ++ movesOf1.getOrElse(k, Iterable.empty))
}
val lineMovesAndPushes = lineMoves ++ validPushes(situation).map {
case (k, v) => k -> (v ++ lineMoves.getOrElse(k, Iterable.empty))
}
lineMovesAndPushes ++ validSideMoves(situation).map { // @TODO: this should reuse what was computed in validMovesOf1
case (k, v) => k -> (v ++ lineMovesAndPushes.getOrElse(k, Iterable.empty))
}
}
def validMoves(situation: Situation): Map[Pos, List[Move]] =
(validMovesOf1(situation).toList ++ validMovesOf2And3(situation).view.mapValues(_.map(_._2)).toList)
.groupBy(_._1).map{case(k, v) => k -> v.map(_._2).toSeq.flatten}.toMap

def validMovesOf1(situation: Situation): Map[Pos, List[Move]] =
situation.board.piecesOf(situation.player).flatMap {
turnPieces(situation).flatMap {
case ((pos, piece)) =>
Map(pos ->
pos.neighbours.flatten
Expand All @@ -102,17 +102,86 @@ abstract class Variant private[variant] (
)
}.toMap

def validSideMoves(@nowarn situation: Situation): Map[Pos, List[Move]] = {
Map()
}
def validMovesOf2And3(situation: Situation): Map[Pos, List[(String, Move)]] = {
def generateMove(orig: Pos, dest: Pos, category: String): Some[(String, Move)] =
Some( (category, Move(Piece(situation.player, Role.defaultRole), orig, dest, situation, boardAfter(situation, orig, dest), true, if (category == "pushout") Some(dest) else None)) )

def generateSideMoves(lineOfMarbles: List[Pos], direction: Option[String]): List[(String, Move)] = {
// "left" is related to the direction
def canLeftSideMove(pos: Pos): Boolean =
pos.sideMovesDirsFromDir(direction)._1.fold(false)(p => situation.board.isEmptySquare(Some(p)))
def canRightSideMove(pos: Pos): Boolean =
pos.sideMovesDirsFromDir(direction)._2.fold(false)(p => situation.board.isEmptySquare(Some(p)))

List(
if (lineOfMarbles.size == 3) generateSideMoves(lineOfMarbles.dropRight(1), direction)
else None,
if (!lineOfMarbles.map(canLeftSideMove).contains(false))
generateMove(lineOfMarbles(0), lineOfMarbles.last.sideMovesDirsFromDir(direction)._1.get, "side")
else None,
if (!lineOfMarbles.map(canRightSideMove).contains(false))
generateMove(lineOfMarbles(0), lineOfMarbles.last.sideMovesDirsFromDir(direction)._2.get, "side")
else None,
).flatten
}

def validLineMoves(@nowarn situation: Situation): Map[Pos, List[Move]] = {
Map()
}

def generateMovesForNeighbours(pos: Pos, neighbour: Pos, direction: Option[String]): List[(String, Move)] = {
val moves = List(
neighbour.dir(direction).toList.flatMap {
case (thirdSquareInLine) => {
if (situation.board.isEmptySquare(Some(thirdSquareInLine))) // xx.
generateMove(pos, thirdSquareInLine, "line")
else if (situation.board.piecesOf(!situation.player).contains(thirdSquareInLine)) // xxo
thirdSquareInLine.dir(direction) match { // xxo?
case None => generateMove(pos, thirdSquareInLine, "pushout") // xxo\
case Some(emptySquare) if situation.board.isEmptySquare(Some(emptySquare)) => {
generateMove(pos, thirdSquareInLine, "push") // xxo.
}
case _ => None
}
else if (situation.board.piecesOf(situation.player).contains(thirdSquareInLine)) // xxx
thirdSquareInLine.dir(direction).flatMap {
case (fourthSquareInLine) => // xxx_
if (situation.board.isEmptySquare(Some(fourthSquareInLine))) // xxx.
generateMove(pos, fourthSquareInLine, "line")
else if (situation.board.piecesOf(!situation.player).contains(fourthSquareInLine)) // xxxo
fourthSquareInLine.dir(direction) match { // xxxo?
case None => generateMove(pos, fourthSquareInLine, "pushout") // xxxo\
case Some(emptyPos) if (situation.board.isEmptySquare(Some(emptyPos))) => generateMove(pos, fourthSquareInLine, "push") // xxxo.
case _ => fourthSquareInLine.dir(direction).flatMap { // xxxo?
case (fifthSquareInLine) =>
if (situation.board.piecesOf(!situation.player).contains(fifthSquareInLine)) // xxxoo
fifthSquareInLine.dir(direction) match {
case None => generateMove(pos, fourthSquareInLine, "pushout") // xxxoo\
case Some(emptySquare) if situation.board.isEmptySquare(Some(emptySquare)) => generateMove(pos, emptySquare, "push") // xxxoo.
case _ => None
}
else None
}
}
else None
}
else None
}
}
)

def validPushes(@nowarn situation: Situation): Map[Pos, List[Move]] = {
Map()
moves.flatten ++ generateSideMoves(
List(Some(pos), Some(neighbour), neighbour.dir(direction).flatMap{x => if (situation.board.piecesOf(situation.player).contains(x)) Some(x) else None}).flatten,
direction
)
}

turnPieces(situation).flatMap {
case (pos, _) =>
Map(pos -> pos.neighbours.collect {
case Some(neighbour) if situation.board.piecesOf(situation.player).contains(neighbour) => (pos, neighbour, pos.dir(neighbour))
}.flatMap {
case (pos, neighbour, direction) =>
generateMovesForNeighbours(pos, neighbour, direction)
}
).view.toList
}
}

def boardAfter(situation: Situation, orig: Pos, dest: Pos): Board = {
Expand Down Expand Up @@ -197,39 +266,30 @@ abstract class Variant private[variant] (
// TODO: Abalone. Add some sensible validation checks here if appropriate
def valid(@nowarn board: Board, @nowarn strict: Boolean): Boolean = true

val roles: List[Role] = Role.all
def defaultRole: Role = Role.defaultRole

lazy val rolesByPgn: Map[Char, Role] = roles
.map { r =>
(r.pgn, r)
}
.to(Map)
def gameFamily: GameFamily

override def toString = s"Variant($name)"

override def equals(that: Any): Boolean = this eq that.asInstanceOf[AnyRef]

override def hashCode: Int = id

def defaultRole: Role = Role.defaultRole

def gameFamily: GameFamily
}
private def turnPieces(situation: Situation): PieceMap = situation.board.piecesOf(situation.player)

object Variant {
val kingPiece: Option[Role] = None

lazy val all: List[Variant] = List(
Abalone
)
val byId = all map { v =>
(v.id, v)
} toMap
val byKey = all map { v =>
(v.key, v)
} toMap
val roles: List[Role] = Role.all

val default = Abalone
lazy val rolesByPgn: Map[Char, Role] = roles
.map { r =>
(r.pgn, r)
}
.to(Map)
}

object Variant {
def apply(id: Int): Option[Variant] = byId get id
def apply(key: String): Option[Variant] = byKey get key
def orDefault(id: Int): Variant = apply(id) | default
Expand All @@ -244,4 +304,16 @@ object Variant {

val divisionSensibleVariants: Set[Variant] = Set()

val byId = all map { v =>
(v.id, v)
} toMap
val byKey = all map { v =>
(v.key, v)
} toMap

val default = Abalone

lazy val all: List[Variant] = List(
Abalone
)
}
35 changes: 35 additions & 0 deletions src/test/scala/abalone/AbalonePosTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ class AbalonePosTest extends AbaloneTest with ValidatedMatchers {
Pos.A1.right must_== Some(Pos.B1)
Pos.A1.upLeft must_== Some(Pos.A2)
Pos.A1.upRight must_== Some(Pos.B2)
Pos.A1.dir(Pos.A2) must_== Some("upLeft")
Pos.A1.dir(Pos.B2) must_== Some("upRight")
Pos.A1.dir(Pos.B1) must_== Some("right")
}
}

Expand All @@ -137,6 +140,9 @@ class AbalonePosTest extends AbaloneTest with ValidatedMatchers {
Pos.I9.left must_== Some(Pos.H9)
Pos.I9.downLeft must_== Some(Pos.H8)
Pos.I9.downRight must_== Some(Pos.I8)
Pos.I9.dir(Pos.I8) must_== Some("downRight")
Pos.I9.dir(Pos.H9) must_== Some("left")
Pos.I9.dir(Pos.H8) must_== Some("downLeft")
}
}

Expand All @@ -153,4 +159,33 @@ class AbalonePosTest extends AbaloneTest with ValidatedMatchers {
Pos.C7.upRight must_== Some(Pos.D8)
}
}

"directions from G7" should {
"allow moving everywhere inside the grid" in {
Pos.G7.downLeft must_== Some(Pos.F6)
Pos.G7.right must_== Some(Pos.H7)
Pos.G7.downRight must_== Some(Pos.G6)
Pos.G7.upRight must_== Some(Pos.H8)
Pos.G7.left must_== Some(Pos.F7)
Pos.G7.upLeft must_== Some(Pos.G8)
}
}

// 9 - & \' ( ) *
// 8 - 7 8 9 ! ? ¥
// 7 - Y Z 0 1 2 3 £
// 6 - P Q R S T U V ¡
// 5 - G H I J K L M N }
// 4 - y z A B C D E F
// 3 - q r s t u v w
// 2 - i j k l m n
// 1 - a b c d e
// \ \ \ \ \ \ \ \ \
// A B C D E F G H I
"official notation" should {
"swap file and rank indexes" in {
Pos.G6.officialNotationKey must_== "f7"
}
}

}
Loading