Skip to content

Commit 1dd9793

Browse files
feat: FEN pieces
also starts the work on valid moves generation
1 parent 7a26107 commit 1dd9793

13 files changed

+282
-117
lines changed

src/main/scala/Move.scala

+1-4
Original file line numberDiff line numberDiff line change
@@ -359,10 +359,7 @@ object Move {
359359
Situation.Abalone(m.situationBefore),
360360
Board.Abalone(m.after),
361361
m.autoEndTurn,
362-
m.capture match {
363-
case Some(capture) => Option(List(Pos.Abalone(capture)))
364-
case None => None
365-
},
362+
None, // capture. @TODO: Could be the pos of the piece that was on targetSquare described by the move (because line moves are basically just marbles jumping over other ones)
366363
None,
367364
None,
368365
None,

src/main/scala/abalone/Board.scala

+11-10
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,16 @@ case class Board(
99
history: History,
1010
variant: Variant
1111
) {
12-
13-
def apply(at: Pos): Option[Piece] = pieces get at
12+
def apply(at: Pos): Option[Piece] = pieces.get(at)
1413
def apply(file: File, rank: Rank): Option[Piece] = {
1514
val pos = Pos(file, rank)
1615
pos match {
17-
case Some(pos) => pieces get pos
16+
case Some(pos) => pieces.get(pos)
1817
case None => None
1918
}
2019
}
2120

22-
lazy val actors: Map[Pos, Actor] = pieces map { case (pos, piece) =>
23-
(pos, Actor(piece, pos, this))
24-
}
25-
26-
lazy val posMap: Map[Piece, Iterable[Pos]] = pieces.groupMap(_._2)(_._1)
27-
28-
lazy val piecesOnBoardCount: Int = pieces.keys.size
21+
def piecesOf(player: Player): PieceMap = pieces.filter(_._2.is(player))
2922

3023
def withHistory(h: History): Board = copy(history = h)
3124
def updateHistory(f: History => History) = copy(history = f(history))
@@ -41,6 +34,14 @@ case class Board(
4134
def materialImbalance: Int = variant.materialImbalance(this)
4235

4336
override def toString = s"$variant Position after ${history.recentTurnUciString}"
37+
38+
lazy val actors: Map[Pos, Actor] = pieces.map {
39+
case (pos, piece) => (pos, Actor(piece, pos, this))
40+
}
41+
42+
lazy val posMap: Map[Piece, Iterable[Pos]] = pieces.groupMap(_._2)(_._1)
43+
44+
lazy val piecesOnBoardCount: Int = pieces.keys.size
4445
}
4546

4647
object Board {

src/main/scala/abalone/History.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ case class History(
99
currentTurn: List[Uci] = List.empty,
1010
positionHashes: PositionHash = Array.empty,
1111
score: Score = Score(0, 0),
12-
// this might be tracking fullMove for Abalone
12+
// this is tracking fullMove for Abalone
1313
halfMoveClock: Int = 0
1414
) {
1515

src/main/scala/abalone/Move.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ case class Move(
1010
situationBefore: Situation,
1111
after: Board,
1212
autoEndTurn: Boolean,
13-
capture: Option[Pos] = None,
13+
capture: Option[Pos] = None, // @TODO: could just be the dest Pos in a capture move (because line moves are basically just marbles jumping over other ones)
1414
promotion: Option[PromotableRole] = None,
1515
metrics: MoveMetrics = MoveMetrics()
1616
) extends Action(situationBefore, after, metrics) {

src/main/scala/abalone/Pos.scala

+8-8
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,11 @@ case class Pos private (index: Int) extends AnyVal {
194194
@inline def file = File of this // column (as if it was an index in a 1D array)
195195
@inline def rank = Rank of this // horizontal row, makes sense in a 2D array
196196

197+
// these 3 below might be handy
198+
// def touches(other: Pos): Boolean = xDist(other) <= 1 && yDist(other) <= 1
199+
// def xDist(other: Pos) = abs(file - other.file)
200+
// def yDist(other: Pos) = abs(rank - other.rank)
201+
197202
// @TODO VFR: test these
198203
def >|(stop: Pos => Boolean): List[Pos] = |<>|(stop, _.right)
199204
def |<(stop: Pos => Boolean): List[Pos] = |<>|(stop, _.left)
@@ -214,15 +219,10 @@ case class Pos private (index: Int) extends AnyVal {
214219
def <->(other: Pos): Iterable[Pos] =
215220
min(file.index, other.file.index) to max(file.index, other.file.index) flatMap { Pos.at(_, rank.index) }
216221
217-
def touches(other: Pos): Boolean = xDist(other) <= 1 && yDist(other) <= 1
218-
219222
def onSameDiagonal(other: Pos): Boolean =
220223
file.index - rank.index == other.file.index - other.rank.index || file.index + rank.index == other.file.index + other.rank.index
221224
def onSameLine(other: Pos): Boolean = ?-(other) || ?|(other)
222225
223-
def xDist(other: Pos) = abs(file - other.file)
224-
def yDist(other: Pos) = abs(rank - other.rank)
225-
226226
def isLight: Boolean = (file.index + rank.index) % 2 == 1
227227
*/
228228

@@ -235,9 +235,9 @@ case class Pos private (index: Int) extends AnyVal {
235235
)
236236
def piotrStr = piotr.toString
237237

238-
def key = file.toString + rank.toString
239-
override def toString = key
240-
238+
def key = file.toString + rank.toString
239+
def officialNotationKey = File(rank.index).getOrElse("").toString + "" + Rank(file.index).getOrElse("").toString
240+
override def toString = officialNotationKey
241241
}
242242

243243
object Pos {

src/main/scala/abalone/Replay.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ object Replay {
8383
// TODO Abalone Set
8484
after = before.situation.board.copy(),
8585
autoEndTurn = endTurn,
86-
capture = None,
86+
capture = None, // @TODO: consider if it's difficult to determine if a capture was made from here (a marble pushed off the board)
8787
promotion = None
8888
)
8989

src/main/scala/abalone/Role.scala

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ sealed trait Role {
1919

2020
sealed trait PromotableRole extends Role
2121

22-
case object Stone extends Role { // @TODO VFR: consider if we want a new "Marble" Role or if we just use Stone for Marbles (because these can move)
22+
case object Stone extends Role { // @TODO VFR: consider if we want a new "Marble" Role or if we just use Stone for Marbles (because these can be rolling stones, not Oware "Stones")
2323
val forsyth = 's'
2424
val binaryInt = 0
25+
26+
val dirs: Directions = List(_.right, _.left, _.upLeft, _.upRight, _.downLeft, _.downRight)
2527
}
2628

2729
object Role {

src/main/scala/abalone/Situation.scala

+17-23
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,22 @@ import strategygames.abalone.format.Uci
99

1010
case class Situation(board: Board, player: Player) {
1111

12-
lazy val moves: Map[Pos, List[Move]] = board.variant.validMoves(this)
13-
14-
lazy val destinations: Map[Pos, List[Pos]] = moves.view.mapValues { _ map (_.dest) }.to(Map)
15-
1612
def history = board.history
1713

1814
def checkMate: Boolean = false
19-
def staleMate: Boolean = false
2015

21-
private def variantEnd = false || board.variant.specialEnd(this)
16+
def staleMate: Boolean = board.variant.specialDraw(this)
2217

23-
def end: Boolean = checkMate || staleMate || variantEnd
18+
def end: Boolean = staleMate || variantEnd
2419

2520
def winner: Option[Player] = board.variant.winner(this)
2621

2722
def playable(strict: Boolean): Boolean =
28-
(board valid strict) && !end
29-
30-
lazy val status: Option[Status] =
31-
if (checkMate) Status.Mate.some
32-
else if (variantEnd) Status.VariantEnd.some
33-
else if (staleMate) Status.Stalemate.some
34-
else none
23+
(board.valid(strict)) && !end
3524

3625
// TODO Abalone set this
3726
// in case someone does not have more than 2 pieces, it could be considered as insufficient material to do anything
38-
// But I'm not sure we do not want to allow having a board containing only 1 marble for a player : Could be interesting for teaching or puzzle purpose.
27+
// But I'm not sure we do not want to allow having a board containing only 1 marble for a player : for didactic consideration e.g. the famous "hunt as 4 v 1"
3928
def opponentHasInsufficientMaterial: Boolean =
4029
false
4130

@@ -45,17 +34,22 @@ case class Situation(board: Board, player: Player) {
4534
def move(uci: Uci.Move): Validated[String, Move] =
4635
board.variant.move(this, uci.orig, uci.dest, uci.promotion)
4736

48-
def withHistory(history: History) =
49-
copy(
50-
board = board withHistory history
51-
)
37+
def withHistory(history: History) = copy(board = board.withHistory(history))
5238

53-
def withVariant(variant: strategygames.abalone.variant.Variant) =
54-
copy(
55-
board = board withVariant variant
56-
)
39+
def withVariant(variant: strategygames.abalone.variant.Variant) = copy(board = board.withVariant(variant))
5740

5841
def unary_! = copy(player = !player)
42+
43+
lazy val destinations: Map[Pos, List[Pos]] = moves.view.mapValues { _.map(_.dest) }.to(Map)
44+
45+
lazy val moves: Map[Pos, List[Move]] = board.variant.validMoves(this)
46+
47+
lazy val status: Option[Status] =
48+
if (variantEnd) Status.VariantEnd.some
49+
else if (staleMate) Status.Stalemate.some
50+
else none
51+
52+
private def variantEnd = board.variant.specialEnd(this)
5953
}
6054

6155
object Situation {

src/main/scala/abalone/format/FEN.scala

+11-26
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
package strategygames.abalone.format
22

33
import strategygames.Player
4-
import strategygames.abalone.File
5-
import strategygames.abalone.Piece
6-
import strategygames.abalone.PieceMap
7-
import strategygames.abalone.Pos
8-
import strategygames.abalone.Stone
4+
import strategygames.abalone.{ P1, P2, Piece, PieceMap, Pos, Role }
95

106
final case class FEN(value: String) extends AnyVal {
117
// squares are described from topLeft to bottomRight in the FEN :
@@ -14,18 +10,16 @@ final case class FEN(value: String) extends AnyVal {
1410

1511
override def toString = value
1612

17-
def pieces: PieceMap = squares.zipWithIndex.map {
18-
case (pieces, row) => pieces.zipWithIndex.map[Option[(Option[strategygames.abalone.Pos], strategygames.abalone.Piece)]] {
19-
case (piece, index) => piece match {
20-
case Stone.forsyth => Some((Pos.at(index, (File.all.size - 1 - row)), new Piece(strategygames.abalone.P1, Stone)))
21-
case Stone.forsythUpper => Some((Pos.at(index, (File.all.size - 1 - row)), new Piece(strategygames.abalone.P2, Stone)))
22-
case _ => None
23-
}
24-
}.flatMap {
25-
case Some((Some(pos), piece)) => Some(pos -> piece)
26-
case _ => None
27-
}
28-
}.flatten.toMap
13+
def pieces: PieceMap = value.split(' ')(0).split('/').reverse.flatMap {
14+
_.toCharArray
15+
}.flatMap{
16+
case square if (square.isDigit) => { Array.fill(square.asDigit)('1') }
17+
case square => Array(square)
18+
}.zip(Pos.all).flatMap { // map + flatten (that get rid of the 'Some' and the 'None' (meaning we map to None for cases we want to filter out))
19+
case (piece, pos) if (piece == Role.defaultRole.forsyth) => Some((pos, Piece(P1, Role.defaultRole)))
20+
case (piece, pos) if (piece == Role.defaultRole.forsythUpper) => Some((pos, Piece(P2, Role.defaultRole)))
21+
case _ => None
22+
}.toMap
2923

3024
def player1Score: Int = intFromFen(1).getOrElse(0)
3125

@@ -46,15 +40,6 @@ final case class FEN(value: String) extends AnyVal {
4640

4741
private def intFromFen(index: Int): Option[Int] =
4842
value.split(' ').lift(index).flatMap(_.toIntOption)
49-
50-
// represents pieces on the Board, and '0' or '1' depending if the empty square is in the hexagon or not
51-
private def squares: Array[Array[Char]] = value.split(' ')(0).split('/').zipWithIndex.map {
52-
case (pieces, row) if (row < (File.all.size / 2 + 1)) => { Array.fill(Math.abs(row + (File.all.size / 2 + 1) - File.all.size)){'0'} ++ pieces.toArray }
53-
case (pieces, row) => { pieces.toArray ++ Array.fill(Math.abs((File.all.size / 2) - row)){'0'} }
54-
}.flatten.flatMap {
55-
case (d) if (d.isDigit && d.asDigit > 0) => Array.fill(d.asDigit){Array('1')}
56-
case (d) => Array.fill(1){Array(d)}
57-
}.flatten.grouped(File.all.size).toArray
5843
}
5944

6045
object FEN {

src/main/scala/abalone/format/Uci.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ object Uci {
2424
promotion: Option[PromotableRole] = None
2525
) extends Uci {
2626

27-
def keys = orig.key + dest.key
27+
def keys = orig.officialNotationKey + dest.officialNotationKey
2828
def uci = keys + promotionString
2929

3030
def keysPiotr = orig.piotrStr + dest.piotrStr

src/main/scala/abalone/variant/Abalone.scala

+1-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package strategygames.abalone
22
package variant
33

4+
import strategygames.GameFamily
45
import strategygames.abalone._
5-
import strategygames.{ GameFamily, Player }
66

77
case object Abalone
88
extends Variant(
@@ -19,9 +19,4 @@ case object Abalone
1919
def perfId: Int = 700
2020

2121
override def baseVariant: Boolean = true
22-
23-
// TODO: Abalone set
24-
override def winner(situation: Situation): Option[Player] =
25-
None // winner is the one who pushed out 6 or prevented opponent to move (which is an extremely rare case)
26-
2722
}

0 commit comments

Comments
 (0)