Skip to content

Commit 7a26107

Browse files
fix: read FEN properly, use Stones for pieces, improve codestyle
1 parent a08757b commit 7a26107

File tree

5 files changed

+69
-61
lines changed

5 files changed

+69
-61
lines changed

src/main/scala/abalone/Role.scala

+2-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,7 +19,7 @@ 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 { // @TODO VFR: consider if we want a new "Marble" Role or if we just use Stone for Marbles (because these can move)
2323
val forsyth = 's'
2424
val binaryInt = 0
2525
}

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

+24-51
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,37 @@
11
package strategygames.abalone.format
22

33
import strategygames.Player
4+
import strategygames.abalone.File
45
import strategygames.abalone.Piece
56
import strategygames.abalone.PieceMap
67
import strategygames.abalone.Pos
78
import strategygames.abalone.Stone
8-
import strategygames.abalone.File
99

10-
// FEN is described from bottomLeft to topRight
1110
final case class FEN(value: String) extends AnyVal {
12-
// Belgian Daisy: pp1PP/pppPPP/1pp1PP1/8/9/8/1PP1pp1/PPPppp/PP1pp 0 0 b 0 0
13-
// Snakes: PPPPP/5P/6P/1ppppp1P/1p5P1/p1PPPPP1/p6/p5/ppppp 0 0 b 0 0
11+
// squares are described from topLeft to bottomRight in the FEN :
12+
// eg. Belgian Daisy: SS1ss/SSSsss/1SS1ss1/8/9/8/1ss1SS1/sssSSS/ss1SS
13+
// Snakes: sssss/s5/s6/s1SSSSS1/1s5S1/1sssss1S/6S/5S/SSSSS
1414

1515
override def toString = value
1616

17-
def pieces: PieceMap = {
18-
/*
19-
squared hex representation of the Belgian Daisy FEN :
20-
row col
21-
9 - 72 73 74 75 'P' 'P' '1' 'p' 'p' 8: >3 (<9)
22-
8 - 63 64 65 'P' 'P' 'P' 'p' 'p' 'p' 7: >2 (<9) next line : +4
23-
7 - 54 55 '1' 'P' 'P' '1' 'p' 'p' '1' 6: >1 (<9) next line : +3
24-
6 - 45 '8' 5: >0 (<9) next line : +2
25-
5 - '9' 4: <9 next line : +1
26-
4 - '8' 35 3: <8 next line : +1
27-
3 - '1' 'p' 'p' '1' 'P' 'P' '1' 25 26 2: <7 next line : +2
28-
2 - 'p' 'p' 'p' 'P' 'P' 'P' 15 16 17 1: <6 next line : +3
29-
1 - 'p' 'p' '1' 'P' 'P' 5 6 7 8 0: <5 next line : +4
30-
| | | | | | | | |
31-
A B C D E F G H I
32-
*/
33-
val position = value.split(' ')(0)
34-
var currentIndex = 0
35-
var currentLine = 0
36-
position.map {
37-
case 'p' => {
38-
currentIndex = currentIndex + 1
39-
Some((Pos.apply(currentIndex - 1), new Piece(strategygames.abalone.P1, Stone)))
40-
}
41-
case 'P' => {
42-
currentIndex = currentIndex + 1
43-
Some((Pos.apply(currentIndex - 1), new Piece(strategygames.abalone.P2, Stone)))
44-
}
45-
case d if d.isDigit => {
46-
currentIndex = currentIndex + d.asDigit
47-
None
48-
}
49-
case '/' => {
50-
currentLine = currentLine + 1
51-
currentIndex = if(currentLine < 5) {
52-
currentIndex + (File.all.size / 2 + 1) - (currentLine)
53-
}
54-
else {
55-
currentIndex + Math.abs((File.all.size / 2 + 1) - currentLine - 1)
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
5623
}
57-
None
24+
}.flatMap {
25+
case Some((Some(pos), piece)) => Some(pos -> piece)
26+
case _ => None
5827
}
59-
}.flatten.flatMap { // flatMap = map + flatten, which gets rid of None and returns the Object instead of a Some(Object) which we would get with map
60-
case (Some(pos), piece) => Some(pos -> piece)
61-
case _ => None
62-
}.toMap
63-
}
28+
}.flatten.toMap
6429

6530
def player1Score: Int = intFromFen(1).getOrElse(0)
6631

6732
def player2Score: Int = intFromFen(2).getOrElse(0)
6833

69-
def player: Option[Player] =
70-
value.split(' ').lift(3) flatMap (_.headOption) flatMap Player.apply map( !_ )
34+
def player: Option[Player] = value.split(' ').lift(3).flatMap(_.headOption).flatMap(Player.apply).map( !_ )
7135

7236
def halfMovesSinceLastCapture: Option[Int] = intFromFen(4)
7337

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

8347
private def intFromFen(index: Int): Option[Int] =
8448
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
8558
}
8659

8760
object FEN {

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

-3
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@ case object Abalone
2020

2121
override def baseVariant: Boolean = true
2222

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-
2623
// TODO: Abalone set
2724
override def winner(situation: Situation): Option[Player] =
2825
None // winner is the one who pushed out 6 or prevented opponent to move (which is an extremely rare case)

src/main/scala/abalone/variant/Variant.scala

+4-3
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ abstract class Variant private[variant] (
4141
def perfId: Int
4242
def perfIcon: Char
4343

44-
def initialFen: FEN = format.FEN("pp1PP/pppPPP/1pp1PP1/8/9/8/1PP1pp1/PPPppp/PP1pp 0 0 b 0 0") // Belgian Daisy
44+
// pieces, scoreP1, scoreP2, turn, halfMovesSinceLastCapture (triggering condition could be when == 100 && total moves > 50 ? => draw), total moves
45+
def initialFen: FEN = format.FEN("SS1ss/SSSsss/1SS1ss1/8/9/8/1ss1SS1/sssSSS/ss1SS 0 0 b 0 0") // Belgian Daisy
4546

4647
def pieces: PieceMap = initialFen.pieces
4748

@@ -60,9 +61,9 @@ abstract class Variant private[variant] (
6061
situation: Situation,
6162
from: Pos,
6263
to: Pos,
63-
promotion: Option[PromotableRole]
64+
promotion: Option[PromotableRole] // @TODO: try to see if it can be removed, check if it needs an update on the wrapperLayer. Not mandatory though
6465
): Validated[String, Move] = {
65-
// Find the move in the variant specific list of valid moves
66+
// Find the move in the variant specific list of valid moves !
6667
situation.moves get from flatMap (_.find(m => m.dest == to && m.promotion == promotion)) toValid
6768
s"Not a valid move: ${from}${to} with prom: ${promotion}. Allowed moves: ${situation.moves}"
6869
}

src/test/scala/abalone/AbaloneFenTest.scala

+39-2
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ class AbaloneFenTest extends AbaloneTest with ValidatedMatchers {
6565
}
6666

6767
"Snakes start position" should {
68-
val snakesFen = new format.FEN("PPPPP/5P/6P/1ppppp1P/1p5P1/p1PPPPP1/p6/p5/ppppp 0 0 b 0 0")
68+
val snakesFen = new format.FEN("sssss/s5/s6/s1SSSSS1/1s5S1/1sssss1S/6S/5S/SSSSS 0 0 b 0 0")
6969
val pieces = snakesFen.pieces
7070

7171
"have Black starting the game" in {
@@ -80,7 +80,7 @@ class AbaloneFenTest extends AbaloneTest with ValidatedMatchers {
8080
}
8181

8282
"Atomouche start position" should {
83-
val atomoucheFen = new format.FEN("3pP/PpP2p/4P2/p4p1P/P2p1P2p/p1P4P/2p4/P2pPp/pP3 0 0 b 0 0")
83+
val atomoucheFen = new format.FEN("sS3/S2sSs/2s4/s1S4S/S2s1S2s/s4s1S/4S2/SsS2s/3sS 0 0 b 0 0")
8484
val pieces = atomoucheFen.pieces
8585

8686
"have Black starting the game" in {
@@ -122,6 +122,43 @@ class AbaloneFenTest extends AbaloneTest with ValidatedMatchers {
122122
}
123123
}
124124

125+
"Fun little game situation \"5/6/2s4/2ssss2/2SSsS3/1SSSsss1/2s4/1SSs2/3S1 5 3 b 11 42\"" should {
126+
val puzzleFen = new format.FEN("5/6/2s4/2ssss2/2SSsS3/1SSSsss1/2s4/1SSs2/3S1 5 3 b 11 42")
127+
val pieces = puzzleFen.pieces
128+
129+
"have a score of 5 for P1 and a score of 3 for P2" in {
130+
puzzleFen.player1Score must_== 5
131+
puzzleFen.player2Score must_== 3
132+
}
133+
134+
"have a total of 14 marbles per player, on the board and pushed out" in {
135+
pieces.filter(p => p._2.player == P1).size + puzzleFen.player2Score must_== 14
136+
pieces.filter(p => p._2.player == P2).size + puzzleFen.player1Score must_== 14
137+
}
138+
139+
"11 plies were played since last time a marble was pushed out" in {
140+
puzzleFen.halfMovesSinceLastCapture must_== Some(11)
141+
}
142+
143+
"42 moves were played in total" in {
144+
puzzleFen.fullMove must_== Some(42)
145+
}
146+
}
147+
148+
// @TODO: keep adding interesting cases once we are able to instanciate a Board from a FEN.
149+
150+
// "Game in progress since ages" should {
151+
// val ongoingGame = new format.FEN("")
152+
153+
// "have a score of 3 for P2 and 2 for P1" in {
154+
// fen.player1Score must_== 2
155+
// fen.player2Score must_== 3
156+
// }
157+
158+
// "be declared a draw" in {
159+
// ...
160+
// }
161+
// }
125162

126163
/*
127164
"a score of 6" should {

0 commit comments

Comments
 (0)