Skip to content

Commit bed7aa2

Browse files
Merge pull request #187 from Mind-Sports-Games/pla-927-sg-fen-notation
feat: FEN notation
2 parents 130f5bd + bfe1d02 commit bed7aa2

12 files changed

+491
-97
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 (and by extension, pushes) are basically just marbles jumping over other ones)
366363
None,
367364
None,
368365
None,

src/main/scala/abalone/Board.scala

+13-12
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 {
@@ -54,9 +55,9 @@ object Board {
5455

5556
sealed abstract class BoardSize(
5657
val width: Int,
57-
val height: Int
58+
val height: Int,
59+
val irregular: Boolean = true
5860
) {
59-
val regular = false
6061

6162
val key = s"${width}x${height}"
6263
val sizes = List(width, height)

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

+22-20
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,16 @@ case class Pos private (index: Int) extends AnyVal {
190190
def right: Option[Pos] = Pos.at(file.index + 1, rank.index)
191191
def downRight: Option[Pos] = Pos.at(file.index, rank.index - 1)
192192
def upRight: Option[Pos] = Pos.at(file.index + 1, rank.index + 1)
193+
def neighbours: List[Option[Pos]] = List(left, downLeft, upLeft, right, downRight, upRight)
193194

194195
@inline def file = File of this // column (as if it was an index in a 1D array)
195196
@inline def rank = Rank of this // horizontal row, makes sense in a 2D array
196197

198+
// these 3 below might be handy
199+
// def touches(other: Pos): Boolean = xDist(other) <= 1 && yDist(other) <= 1
200+
// def xDist(other: Pos) = abs(file - other.file)
201+
// def yDist(other: Pos) = abs(rank - other.rank)
202+
197203
// @TODO VFR: test these
198204
def >|(stop: Pos => Boolean): List[Pos] = |<>|(stop, _.right)
199205
def |<(stop: Pos => Boolean): List[Pos] = |<>|(stop, _.left)
@@ -214,15 +220,10 @@ case class Pos private (index: Int) extends AnyVal {
214220
def <->(other: Pos): Iterable[Pos] =
215221
min(file.index, other.file.index) to max(file.index, other.file.index) flatMap { Pos.at(_, rank.index) }
216222
217-
def touches(other: Pos): Boolean = xDist(other) <= 1 && yDist(other) <= 1
218-
219223
def onSameDiagonal(other: Pos): Boolean =
220224
file.index - rank.index == other.file.index - other.rank.index || file.index + rank.index == other.file.index + other.rank.index
221225
def onSameLine(other: Pos): Boolean = ?-(other) || ?|(other)
222226
223-
def xDist(other: Pos) = abs(file - other.file)
224-
def yDist(other: Pos) = abs(rank - other.rank)
225-
226227
def isLight: Boolean = (file.index + rank.index) % 2 == 1
227228
*/
228229

@@ -235,25 +236,26 @@ case class Pos private (index: Int) extends AnyVal {
235236
)
236237
def piotrStr = piotr.toString
237238

238-
def key = file.toString + rank.toString
239-
override def toString = key
240-
239+
def key = file.toString + rank.toString
240+
def officialNotationKey = s"${File(rank.index).getOrElse("")}${Rank(file.index).getOrElse("")}"
241+
override def toString = officialNotationKey
241242
}
242243

243244
object Pos {
244245
/*
245-
row col
246-
9 - 72 73 74 75 & \' ( ) * 8: >3 (<9)
247-
8 - 63 64 65 7 8 9 ! ? ¥ 7: >2 (<9)
248-
7 - 54 55 Y Z 0 1 2 3 £ 6: >1 (<9)
249-
6 - 45 P Q R S T U V ¡ 5: >0 (<9)
250-
5 - G H I J K L M N } 4: <9
251-
4 - y z A B C D E F 35 3: <8
252-
3 - q r s t u v w 25 26 2: <7
253-
2 - i j k l m n 15 16 17 1: <6
254-
1 - a b c d e 5 6 7 8 0: <5
255-
| | | | | | | | |
256-
A B C D E F G H I
246+
indexes of Pos outside of the hexagon :
247+
row col
248+
9 - 72 73 74 75 '&' ''' '(' ')' '*' 8: >3 (<9)
249+
8 - 63 64 65 '7' '8' '9' '!' '?' '¥' 7: >2 (<9)
250+
7 - 54 55 'Y' 'Z' '0' '1' '2' '3' '£' 6: >1 (<9)
251+
6 - 45 'P' 'Q' 'R' 'S' 'T' 'U' 'V' '¡' 5: >0 (<9)
252+
5 - 'G' 'H' 'I' 'J' 'K' 'L' 'M' 'N' '}' 4: <9
253+
4 - 'y' 'z' 'A' 'B' 'C' 'D' 'E' 'F' 35 3: <8
254+
3 - 'q' 'r' 's' 't' 'u' 'v' 'w' 25 26 2: <7
255+
2 - 'i' 'j' 'k' 'l' 'm' 'n' 15 16 17 1: <6
256+
1 - 'a' 'b' 'c' 'd' 'e' 5 6 7 8 0: <5
257+
| | | | | | | | |
258+
A B C D E F G H I
257259
*/
258260
def isInHexagon(index: Int): Boolean = {
259261
if (index < 0) return false

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import strategygames.{ GameFamily, P1, P2, Player }
44

55
sealed trait Role {
66
val forsyth: Char
7-
// lazy val forsythUpper: Char = forsyth.toUpper //this contradicts what the piece is now!
7+
lazy val forsythUpper: Char = forsyth.toUpper
88
lazy val pgn: Char = forsyth
99
lazy val name = toString
1010
lazy val groundName = s"${forsyth}-piece"
@@ -19,9 +19,11 @@ sealed trait Role {
1919

2020
sealed trait PromotableRole extends Role
2121

22-
case object Stone extends Role { // @TODO VFR: just use Stone for Marbles
22+
case object Stone extends Role {
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

+21-9
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,45 @@
11
package strategygames.abalone.format
22

33
import strategygames.Player
4-
import strategygames.abalone.PieceMap
4+
import strategygames.abalone.{ P1, P2, Piece, PieceMap, Pos, Role }
55

66
final case class FEN(value: String) extends AnyVal {
7+
// squares are described from topLeft to bottomRight in the FEN :
8+
// eg. Belgian Daisy: SS1ss/SSSsss/1SS1ss1/8/9/8/1ss1SS1/sssSSS/ss1SS
9+
// Snakes: sssss/s5/s6/s1SSSSS1/1s5S1/1sssss1S/6S/5S/SSSSS
710

811
override def toString = value
912

10-
def player: Option[Player] =
11-
value.split(' ').lift(3) flatMap (_.headOption) flatMap Player.apply
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 {
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
1223

1324
def player1Score: Int = intFromFen(1).getOrElse(0)
1425

1526
def player2Score: Int = intFromFen(2).getOrElse(0)
1627

17-
def fullMove: Option[Int] = intFromFen(4)
28+
def player: Option[Player] = value.split(' ').lift(3).flatMap(_.headOption).flatMap(Player.apply).map( !_ )
29+
30+
def halfMovesSinceLastCapture: Option[Int] = intFromFen(4)
31+
32+
def fullMove: Option[Int] = intFromFen(5)
1833

1934
def ply: Option[Int] =
2035
fullMove map { fm =>
2136
fm * 2 - (if (player.exists(_.p1)) 2 else 1)
2237
}
2338

24-
private def intFromFen(index: Int): Option[Int] =
25-
value.split(' ').lift(index).flatMap(_.toIntOption)
26-
2739
def initial = value == Forsyth.initial.value
2840

29-
// TODO Abalone Set
30-
def pieces: PieceMap = Map.empty
41+
private def intFromFen(index: Int): Option[Int] =
42+
value.split(' ').lift(index).flatMap(_.toIntOption)
3143
}
3244

3345
object FEN {
+2-10
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(
@@ -18,13 +18,5 @@ case object Abalone
1818
def perfIcon: Char = ''
1919
def perfId: Int = 700
2020

21-
override def baseVariant: Boolean = true
22-
23-
// pieces, scoreP1, scoreP2, turn, halfMovesSinceLastCapture (triggering condition could be when == 100 && total moves > 50 ? => draw), total moves
24-
override def initialFen = format.FEN("pp1PP/pppPPP/1pp1pp1/8/9/8/1PP1pp1/PPPppp/PP1pp 0 0 b 0 0")
25-
26-
// TODO: Abalone set
27-
override def winner(situation: Situation): Option[Player] =
28-
None // winner is the one who pushed out 6 or when the opponent can not move anymore (which is an extremely rare case)
29-
21+
override def baseVariant: Boolean = true // Belgian Daisy initialFen is defined in Abalone default "Variant" file
3022
}

0 commit comments

Comments
 (0)