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 3 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
38 changes: 36 additions & 2 deletions src/main/scala/abalone/Pos.scala
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,41 @@ 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]) = Map(
"left" -> ((this.downLeft, this.upLeft)),
"upLeft" -> ((this.left, this.upRight)),
"upRight" -> ((this.upLeft, this.right)),
"right" -> ((this.upRight, this.downRight)),
"downRight" -> ((this.right, this.downLeft)),
"downLeft" -> ((this.downRight, this.left))
).getOrElse(dir.getOrElse(""), (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] = Map(
"left" -> (this.left),
"upLeft" -> (this.upLeft),
"upRight" -> (this.upRight),
"right" -> (this.right),
"downRight" -> (this.downRight),
"downLeft" -> (this.downLeft)
).getOrElse(dir.getOrElse(""), (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 +272,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
115 changes: 95 additions & 20 deletions src/main/scala/abalone/variant/Variant.scala
Original file line number Diff line number Diff line change
Expand Up @@ -54,27 +54,38 @@ abstract class Variant private[variant] (

/*
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]] = {
Expand All @@ -90,18 +101,82 @@ abstract class Variant private[variant] (
}
}

def validMovesOf1(situation: Situation): Map[Pos, List[Move]] =
situation.board.piecesOf(situation.player).flatMap {
// @TODO: move this into the validMoves method and make it become private as it's used by validMovesOf1 AND validMovesOf2
def turnPieces(s: Situation): PieceMap = s.board.piecesOf(s.player)

def validMovesOf1(s: Situation): Map[Pos, List[Move]] =
this.turnPieces(s).flatMap {
case ((pos, piece)) =>
Map(pos ->
pos.neighbours.flatten
.filterNot(situation.board.pieces.contains(_))
.filterNot(s.board.pieces.contains(_))
.map(landingSquare =>
Move(piece, pos, landingSquare, situation, boardAfter(situation, pos, landingSquare), false)
Move(piece, pos, landingSquare, s, boardAfter(s, pos, landingSquare), false)
)
)
}.toMap

def validMovesOf2(s: Situation): Map[Pos, List[(String, Move)]] = {
def generateMove(orig: Pos, dest: Pos, category: String) = category match {
case "pushout" => Some( (category, Move(Piece(s.player, Role.defaultRole), orig, dest, s, boardAfter(s, orig, dest), true, Some(dest))) )
case _ => Some( (category, Move(Piece(s.player, Role.defaultRole), orig, dest, s, boardAfter(s, orig, dest), true)) )
}

def generateSideMovesOf2(pos: Pos, neighbour: Pos, direction: Option[String]): List[(String, Move)] = List(
if (
pos.sideMovesDirsFromDir(direction)._1 != None &&
s.board.isEmptySquare(pos.sideMovesDirsFromDir(direction)._1) &&
neighbour.sideMovesDirsFromDir(direction)._1 != None &&
s.board.isEmptySquare(neighbour.sideMovesDirsFromDir(direction)._1)
)
generateMove(pos, neighbour.sideMovesDirsFromDir(direction)._1.get, "side")
else
None,
if (
pos.sideMovesDirsFromDir(direction)._2 != None &&
s.board.isEmptySquare(pos.sideMovesDirsFromDir(direction)._2) &&
neighbour.sideMovesDirsFromDir(direction)._2 != None &&
s.board.isEmptySquare(neighbour.sideMovesDirsFromDir(direction)._2)
)
generateMove(pos, neighbour.sideMovesDirsFromDir(direction)._2.get, "side")
else
None
).flatten

this.turnPieces(s).map {
case ( (pos, _) ) => pos ->
pos.neighbours.flatMap {
case Some(neighbour) if(s.board.piecesOf(s.player).contains(neighbour)) => Some( (neighbour, pos.dir(neighbour)) )
case _ => None
}
}.flatMap {
case (pos, neighbourAndDir) => Map( pos ->
neighbourAndDir.flatMap {
case (neighbour, direction) =>
neighbour.dir(direction) match {
case Some(neighbourOfNeighbour) =>
List(
if (s.board.isEmptySquare(Some(neighbourOfNeighbour)))
generateMove(pos, neighbourOfNeighbour, "line")
else None,
if (s.board.piecesOf(!s.player).contains(neighbourOfNeighbour))
if(neighbourOfNeighbour.dir(direction) == None)
generateMove(pos, neighbourOfNeighbour, "pushout")
else if (s.board.isEmptySquare(neighbourOfNeighbour.dir(direction)))
generateMove(pos, neighbourOfNeighbour, "push")
else None
else None, // here, adding else if (s.board.piecesOf(s.player).contains(neighbourOfNeighbour)), we could generate moves of 2 marbles
generateSideMovesOf2(pos, neighbour, direction)
).flatten
case None => {
generateSideMovesOf2(pos, neighbour, direction)
}
}
}
)
}.toMap
}

def validSideMoves(@nowarn situation: Situation): Map[Pos, List[Move]] = {
Map()
}
Expand Down
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"
}
}

}
115 changes: 115 additions & 0 deletions src/test/scala/abalone/AbaloneVariantTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package strategygames.abalone

import org.specs2.matcher.ValidatedMatchers

import _root_.strategygames.Score

class AbaloneVariantTest extends AbaloneTest with ValidatedMatchers {

"valid Moves from custom basic position" should {
val fen = format.FEN("5/6/7/8/4sssS1/4S3/7/6/5 0 0 b 0 0")
val board = Board(fen.pieces, History(score = Score(0, 0)), variant.Abalone)
val situation = Situation(board, P1)
val movesOf1 = board.variant.validMovesOf1(situation)
val movesOf2 = board.variant.validMovesOf2(situation)

"compute the correct number of moves of 1 marble" in {
movesOf1.foldLeft(0)(_ + _._2.size) must_== 11
}

"compute the following moves of 2 marbles : 1 push 1 line move and 5 side moves" in {
movesOf2.pp.foldLeft(0)(_ + _._2.size) must_== 1 + 1 + 5
}
}

"valid Moves from \"Belgian Daisy\" start position" should {
val fen = variant.Abalone.initialFen
val board = Board(fen.pieces, History(score = Score(0, 0)), variant.Abalone)
val situation = Situation(board, P1)
val movesOf1 = board.variant.validMovesOf1(situation)
val movesOf2 = board.variant.validMovesOf2(situation)

"compute the correct number of moves of 1 marble" in {
movesOf1.foldLeft(0)(_ + _._2.size) must_== 20
}

"compute the correct number of moves of 2 marbles " in {
movesOf2.foldLeft(0)(_ + _._2.size) must_== 28
}
}

"valid Moves from \"Snakes\" start position" should {
val fen = format.FEN("sssss/s5/s6/s1SSSSS1/1s5S1/1sssss1S/6S/5S/SSSSS 0 0 b 0 0")
val board = Board(fen.pieces, History(score = Score(0, 0)), variant.Abalone)
val situation = Situation(board, P1)
val movesOf1 = board.variant.validMovesOf1(situation)
val movesOf2 = board.variant.validMovesOf2(situation)

"compute the correct number of moves of 1 marble" in {
movesOf1.foldLeft(0)(_ + _._2.size) must_== 40
}

"compute the correct number of moves of 2 marbles" in { // 2 + 5 + 4 + 4 + 4 + 2 + 3 + 2 + 1 + 2 + 1 + 2 + 2 + 1
movesOf2.foldLeft(0)(_ + _._2.size) must_== 35
}
}

"valid Moves from \"Alien Attack\" start position" should {
val fen = format.FEN("s1s1s/1sSSs1/1sSsSs1/3ss3/9/3SS3/1SsSsS1/1SssS1/S1S1S 0 0 b 0 0")
val board = Board(fen.pieces, History(score = Score(0, 0)), variant.Abalone)
val situation = Situation(board, P1)
val movesOf1 = board.variant.validMovesOf1(situation)
val movesOf2 = board.variant.validMovesOf2(situation)

"compute 28 moves of 1 marble" in {
movesOf1.foldLeft(0)(_ + _._2.size) must_== 4 + 6 + 2 * (8) + 2
}

"compute the following moves of 2 marbles : 6 push 10 line moves and 4 side moves" in {
movesOf2.foldLeft(0)(_ + _._2.size) must_== 6 + (2 * 2 + 4 + 2) + 4
}
}

"valid Moves from \"Domination\" start position" should {
val fen = format.FEN("5/S4s/SS3ss/SSSS1sss/3s1s3/sss1SSSS/ss3SS/s4S/5 0 0 b 0 0")
val board = Board(fen.pieces, History(score = Score(0, 0)), variant.Abalone)
val situation = Situation(board, P1)
val movesOf1 = board.variant.validMovesOf1(situation)
val movesOf2 = board.variant.validMovesOf2(situation)

"compute 28 moves of 1 marble" in {
movesOf1.foldLeft(0)(_ + _._2.size) must_== 14 * 2
}

"compute the following moves of 2 marbles : 2 pushes 14 line moves and 18 side moves" in {
movesOf2.foldLeft(0)(_ + _._2.size) must_== 2 * (1 + 7 + 9)
}
}

"valid Moves from custom start position with a total of 29 marbles" should {
val fen = format.FEN("2S1S/3sS1/7/8/4sss2/SSss2s1/3S2s/2SsSS/1S3 5 5 b 0 0")
val board = Board(fen.pieces, History(score = Score(0, 0)), variant.Abalone)
val situation = Situation(board, P1)
val movesOf1 = board.variant.validMovesOf1(situation)
val movesOf2 = board.variant.validMovesOf2(situation)

"compute 32 moves of 1 marble" in {
movesOf1.foldLeft(0)(_ + _._2.size) must_== 3 + 4 + (4 + 3 + 4 + 4 + 4 + 4 + 2)
}

"compute the following moves of 2 marbles : 0 push 6 line moves and 14 side moves" in {
movesOf2.foldLeft(0)(_ + _._2.size) must_== 6 + 14
}

val situationP2 = Situation(board, P2)
val movesOf1P2 = board.variant.validMovesOf1(situationP2)
val movesOf2P2 = board.variant.validMovesOf2(situationP2)
"for P2 : compute 31 moves of 1 marble" in {
movesOf1P2.foldLeft(0)(_ + _._2.size) must_== 3 + 2 + 4 + 3 + 4 + 2 + 4 + 3 + 3 + 3
}

"for P2 : compute the following moves of 2 marbles : 0 push 2 line moves and 9 side moves" in {
movesOf2P2.foldLeft(0)(_ + _._2.size) must_== 2 + (1 + 3 + 3 + 2)
}
}
}