Skip to content

Commit 471f1bc

Browse files
feat: generate movesOf2 based on pairs of marbles
1 parent 033fd13 commit 471f1bc

File tree

4 files changed

+121
-183
lines changed

4 files changed

+121
-183
lines changed

src/main/scala/abalone/Pos.scala

+22-16
Original file line numberDiff line numberDiff line change
@@ -191,28 +191,34 @@ case class Pos private (index: Int) extends AnyVal {
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)
193193

194-
def neighbours: List[Option[Pos]] = List(left, downLeft, upLeft, right, downRight, upRight)
194+
def neighbours: List[Option[Pos]] = List(left, upLeft, upRight, right, downRight, downLeft)
195+
def sideMovesDirsFromDir(dir: Option[String]): (Option[Pos], Option[Pos]) = Map(
196+
"left" -> ((this.downLeft, this.upLeft)),
197+
"upLeft" -> ((this.left, this.upRight)),
198+
"upRight" -> ((this.upLeft, this.right)),
199+
"right" -> ((this.upRight, this.downRight)),
200+
"downRight" -> ((this.right, this.downLeft)),
201+
"downLeft" -> ((this.downRight, this.left))
202+
).getOrElse(dir.getOrElse(""), (None, None))
195203

196204
// NOTE - *neighbourhood
197205
// these below only work for neighbour pos but that's probably fine as in Abalone we only move to (potentially extended) neighbourhood
198-
def dir(dir: Option[String]): Option[Pos] =
199-
dir match {
200-
case Some("left") => this.left
201-
case Some("downLeft") => this.downLeft
202-
case Some("upLeft") => this.upLeft
203-
case Some("right") => this.right
204-
case Some("downRight") => this.downRight
205-
case Some("upRight") => this.upRight
206-
case _ => None
207-
}
206+
def dir(dir: Option[String]): Option[Pos] = Map(
207+
"left" -> (this.left),
208+
"upLeft" -> (this.upLeft),
209+
"upRight" -> (this.upRight),
210+
"right" -> (this.right),
211+
"downRight" -> (this.downRight),
212+
"downLeft" -> (this.downLeft)
213+
).getOrElse(dir.getOrElse(""), (None))
208214

209215
def dir(pos: Pos): Option[String] =
210216
(pos.file.index - this.file.index, pos.rank.index - this.rank.index) match {
211-
case (0, 1) => Some("upLeft")
212-
case (0, -1) => Some("downRight")
213-
case (1, 1) => Some("upRight")
214-
case (1, 0) => Some("right")
215-
case (-1, 0) => Some("left")
217+
case (0, 1) => Some("upLeft")
218+
case (0, -1) => Some("downRight")
219+
case (1, 1) => Some("upRight")
220+
case (1, 0) => Some("right")
221+
case (-1, 0) => Some("left")
216222
case (-1, -1) => Some("downLeft")
217223
case _ => None
218224
}

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

+89-153
Original file line numberDiff line numberDiff line change
@@ -54,28 +54,38 @@ abstract class Variant private[variant] (
5454

5555
/*
5656
In Abalone there are 3 kinds of moves.
57-
Let's use the top 2 rows of a board to illustrate (numbers are empty squares, Z is an opponent marble)
57+
Let's use the top 3 rows of a board to illustrate (capital letters are empty squares, 1 is an opponent marble)
5858
59-
Z a b c 7
60-
1 2 3 4 5 6
59+
A B C D E
60+
1 a b c G H
61+
I J K L M N O
6162
6263
- line moves are marbles moving in line to an empty square :
63-
'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'.
64-
'c' to '7' would move one marble to the right.
64+
'a' to 'G' would move three marbles to the right.
65+
This is the same as if 'a' jumped over 'b' and 'c' to land on 'G'.
66+
'c' to 'G' would move one marble to the right.
6567
- pushes are line moves targeting a square hosting an opponent marble
66-
They will be processed as two line moves (one jump per player).
67-
'c' to 'Z' and 'b' to 'Z' are pushes to the left
68+
They will be processed later on as two line moves (one jump per player).
69+
'c' to '1' and 'b' to '1' are pushes to the left.
70+
In case we play a move from 'c' to '1', c will land on 1 and 1 will be removed from the board.
71+
When a piece is pushed off the board, the Move is created with an extra parameter.
6872
- side moves can only be described starting from the right origin (figure out the longest diagonal) :
69-
'a' to '5' is a downRight side move of three marbles.
70-
'c' to '2' is a downLeft side move of three marbles.
71-
'a' to '4' is a downRight side move of two marbles (only 'a' and 'b' would move).
72-
'c' to '3' is a downLeft side move of two marbles (only 'c' and 'b' would move)
73-
'a' to '3' or 'c' to '4' are line moves of a single marble
73+
'a' to 'M' is a downRight side move of three marbles.
74+
'c' to 'J' is a downLeft side move of three marbles.
75+
'a' to 'L' is a downRight side move of two marbles (only 'a' and 'b' would move).
76+
'c' to 'K' is a downLeft side move of two marbles (only 'c' and 'b' would move)
77+
'a' to 'K' or 'c' to 'L' are line moves of a single marble
78+
79+
For moves of 2 marbles or more, once you get the direction of the line,
80+
you can easily generate the side moves as being the one before and the one after,
81+
following a rotation :
82+
\ /
83+
- o -
84+
/ \
85+
e.g. if you are moving upRight the side moves to consider are "upLeft" and "right".
7486
75-
we want to have :
7687
1. moves of 1 marble
77-
2. moves of 2 marbles, using moves of 1
78-
3. moves of 3 marbles, using moves of 2
88+
2. generate any possible pair of marbles and use it to generate moves of 2 and 3 marbles
7989
then merge these as valid moves.
8090
*/
8191
def validMoves(situation: Situation): Map[Pos, List[Move]] = {
@@ -91,155 +101,81 @@ abstract class Variant private[variant] (
91101
}
92102
}
93103

94-
def validMovesOf1(situation: Situation): Map[Pos, List[Move]] =
95-
situation.board.piecesOf(situation.player).flatMap {
104+
// @TODO: move this into the validMoves method and make it become private as it's used by validMovesOf1 AND validMovesOf2
105+
def turnPieces(s: Situation): PieceMap = s.board.piecesOf(s.player)
106+
107+
def validMovesOf1(s: Situation): Map[Pos, List[Move]] =
108+
this.turnPieces(s).flatMap {
96109
case ((pos, piece)) =>
97110
Map(pos ->
98111
pos.neighbours.flatten
99-
.filterNot(situation.board.pieces.contains(_))
112+
.filterNot(s.board.pieces.contains(_))
100113
.map(landingSquare =>
101-
Move(piece, pos, landingSquare, situation, boardAfter(situation, pos, landingSquare), false)
114+
Move(piece, pos, landingSquare, s, boardAfter(s, pos, landingSquare), false)
102115
)
103116
)
104117
}.toMap
105118

106-
def validMovesOf2(situation: Situation, validMoves1: Map[Pos, List[Move]]): Map[Pos, List[(String, Move)]] =
107-
situation.board.piecesOf(situation.player).flatMap {
108-
case ((pos, piece)) => {
109-
Map(
110-
pos -> pos.neighbours.flatMap {
111-
case Some(neighbour) if(situation.board.piecesOf(situation.player).contains(neighbour)) => {
112-
neighbour.neighbours.flatMap {
113-
case Some(neighbourOfNeighbour)
114-
if (situation.board.isEmptySquare(Some(neighbourOfNeighbour))
115-
&& (pos.isInLine(neighbour, neighbourOfNeighbour) || situation.board.isEmptySquare(pos.dir(neighbour.dir(neighbourOfNeighbour))))
116-
&& !validMoves1.get(pos).toList.flatMap(_.map(_.toUci)).exists(m => m.toString == s"Move(${pos},${neighbourOfNeighbour},None)")) =>
117-
Some( ("nopush", Move(piece, pos, neighbourOfNeighbour, situation, boardAfter(situation, neighbour, neighbourOfNeighbour), false)) )
118-
case Some(neighbourOfNeighbour)
119-
if (pos.isInLine(neighbour, neighbourOfNeighbour) && situation.board.piecesOf(!situation.player).contains(neighbourOfNeighbour)) =>
120-
if (neighbourOfNeighbour.dir(pos.dir(neighbour)) == None)
121-
Some( ("pushout", Move(piece, pos, neighbourOfNeighbour, situation, boardAfter(situation, neighbour, neighbourOfNeighbour), false, neighbour.dir(pos.dir(neighbour)))) )
122-
else
123-
if (situation.board.isEmptySquare(neighbourOfNeighbour.dir(pos.dir(neighbour))))
124-
Some( ("push", Move(piece, pos, neighbourOfNeighbour, situation, boardAfter(situation, neighbour, neighbourOfNeighbour), false)) )
119+
def validMovesOf2(s: Situation): Map[Pos, List[(String, Move)]] = {
120+
def generateMove(orig: Pos, dest: Pos, category: String) = category match {
121+
case "pushout" => Some( (category, Move(Piece(s.player, Role.defaultRole), orig, dest, s, boardAfter(s, orig, dest), true, Some(dest))) )
122+
case _ => Some( (category, Move(Piece(s.player, Role.defaultRole), orig, dest, s, boardAfter(s, orig, dest), true)) )
123+
}
124+
125+
def generateSideMovesOf2(pos: Pos, neighbour: Pos, direction: Option[String]): List[(String, Move)] = List(
126+
if (
127+
pos.sideMovesDirsFromDir(direction)._1 != None &&
128+
s.board.isEmptySquare(pos.sideMovesDirsFromDir(direction)._1) &&
129+
neighbour.sideMovesDirsFromDir(direction)._1 != None &&
130+
s.board.isEmptySquare(neighbour.sideMovesDirsFromDir(direction)._1)
131+
)
132+
generateMove(pos, neighbour.sideMovesDirsFromDir(direction)._1.get, "side")
133+
else
134+
None,
135+
if (
136+
pos.sideMovesDirsFromDir(direction)._2 != None &&
137+
s.board.isEmptySquare(pos.sideMovesDirsFromDir(direction)._2) &&
138+
neighbour.sideMovesDirsFromDir(direction)._2 != None &&
139+
s.board.isEmptySquare(neighbour.sideMovesDirsFromDir(direction)._2)
140+
)
141+
generateMove(pos, neighbour.sideMovesDirsFromDir(direction)._2.get, "side")
142+
else
143+
None
144+
).flatten
145+
146+
this.turnPieces(s).map {
147+
case ( (pos, _) ) => pos ->
148+
pos.neighbours.flatMap {
149+
case Some(neighbour) if(s.board.piecesOf(s.player).contains(neighbour)) => Some( (neighbour, pos.dir(neighbour)) )
150+
case _ => None
151+
}
152+
}.flatMap {
153+
case (pos, neighbourAndDir) => Map( pos ->
154+
neighbourAndDir.flatMap {
155+
case (neighbour, direction) =>
156+
neighbour.dir(direction) match {
157+
case Some(neighbourOfNeighbour) =>
158+
List(
159+
if (s.board.isEmptySquare(Some(neighbourOfNeighbour)))
160+
generateMove(pos, neighbourOfNeighbour, "line")
161+
else None,
162+
if (s.board.piecesOf(!s.player).contains(neighbourOfNeighbour))
163+
if(neighbourOfNeighbour.dir(direction) == None)
164+
generateMove(pos, neighbourOfNeighbour, "pushout")
165+
else if (s.board.isEmptySquare(neighbourOfNeighbour.dir(direction)))
166+
generateMove(pos, neighbourOfNeighbour, "push")
125167
else None
126-
case _ => None
168+
else None, // here, adding else if (s.board.piecesOf(s.player).contains(neighbourOfNeighbour)), we could generate moves of 2 marbles
169+
generateSideMovesOf2(pos, neighbour, direction)
170+
).flatten
171+
case None => {
172+
generateSideMovesOf2(pos, neighbour, direction)
173+
}
127174
}
128175
}
129-
case _ => None
130-
}
131-
)
132-
}
133-
}.toMap
134-
135-
// def validMovesOf3(situation: Situation, @nowarn validMoves2: Map[Pos, List[(String, Move)]]): Map[Pos, List[(String, Move)]] =
136-
// situation.board.piecesOf(situation.player).flatMap {
137-
// case ((pos, piece)) => {
138-
// Map(
139-
// pos -> pos.neighbours.flatMap {
140-
// case Some(neighbour)
141-
// if(situation.board.piecesOf(situation.player).contains(neighbour)
142-
// && (neighbour.dir(pos.dir(neighbour).getOrElse("")) != None)
143-
// && situation.board.piecesOf(situation.player).contains((neighbour.dir(pos.dir(neighbour).getOrElse("")).get))
144-
// ) => { // in case we could potentially have a 3rd marble...
145-
// // if (neighbour.dir(pos.dir(neighbour)).getOrElse("") != None) {
146-
// val thirdMarblePos = neighbour.dir(pos.dir(neighbour).getOrElse("")).get
147-
// thirdMarblePos.neighbours.flatMap {
148-
// case Some(neighbourOf3rdMarble)
149-
// if (
150-
// !situation.board.pieces.contains(neighbourOf3rdMarble)
151-
152-
// // && !validMoves2.contains()
153-
// ) => { // line or side move, but we need to remove the 2 marbles moves
154-
// // if (neighbour)
155-
// Some(
156-
// (
157-
// "lineOrSide",
158-
// Move(
159-
// situation.board.pieces.getOrElse(
160-
// pos,
161-
// Piece(!piece.player, piece.role)
162-
// ),
163-
// pos,
164-
// neighbourOf3rdMarble,
165-
// situation,
166-
// situation.board.variant.boardAfter(situation, neighbour, neighbourOf3rdMarble),
167-
// false
168-
// )
169-
// )
170-
// )
171-
// }
172-
// case Some(neighbourOf3rdMarble) if (situation.board.piecesOf(!situation.player).contains(neighbourOf3rdMarble)) => { // push
173-
// Some(
174-
// (
175-
// "push",
176-
// Move(
177-
// situation.board.pieces.getOrElse(
178-
// pos,
179-
// Piece(!piece.player, piece.role)
180-
// ),
181-
// pos,
182-
// neighbourOf3rdMarble,
183-
// situation,
184-
// situation.board.variant.boardAfter(situation, neighbour, neighbourOf3rdMarble),
185-
// false
186-
// )
187-
// )
188-
// )
189-
// }
190-
// case _ => None
191-
// // }
192-
// }
193-
// }
194-
// case _ => None
195-
// }
196-
// )
197-
// }
198-
// }
199-
200-
201-
// def validMovesOf3(situation: Situation, validMoves2: Map[Pos, List[Move]], validMoves1: Map[Pos, List[Move]]): Map[Pos, List[Move]] =
202-
// situation.board.piecesOf(situation.player).flatMap {
203-
// case ((pos, piece)) => {
204-
// Map(
205-
// pos -> pos.neighbours.flatMap {
206-
// case Some(neighbour) if(situation.board.piecesOf(situation.player).contains(neighbour)) => {
207-
// neighbour.neighbours.flatMap {
208-
// case Some(neighbourOfNeighbour) => {
209-
// if (
210-
// !situation.board.pieces.contains(neighbourOfNeighbour)
211-
// && pos.dir(neighbour.dir(neighbourOfNeighbour).getOrElse("")) != None
212-
// && (
213-
// (
214-
// !situation.board.piecesOf(!situation.player).contains(pos.dir(neighbour.dir(neighbourOfNeighbour).getOrElse("")).getOrElse(pos))
215-
// && !situation.board.piecesOf(situation.player).contains(pos.dir(neighbour.dir(neighbourOfNeighbour).getOrElse("")).getOrElse(pos))
216-
// )
217-
// || pos.dir(neighbour) == neighbour.dir(neighbourOfNeighbour) // line moves
218-
// )
219-
// && !(validMoves1.get(pos).get.contains(
220-
// Move(
221-
// situation.board.pieces.getOrElse(
222-
// pos,
223-
// Piece(
224-
// !piece.player, piece.role)
225-
// ),
226-
// pos,
227-
// neighbourOfNeighbour,
228-
// situation,
229-
// situation.board.variant.boardAfter(situation, neighbour, neighbourOfNeighbour),
230-
// false
231-
// )))
232-
// ) {
233-
// Some(move)
234-
// }
235-
// }
236-
// }
237-
// }
238-
// }
239-
// )
240-
// }
241-
// }
242-
176+
)
177+
}.toMap
178+
}
243179

244180
def validSideMoves(@nowarn situation: Situation): Map[Pos, List[Move]] = {
245181
Map()

src/test/scala/abalone/AbalonePosTest.scala

+2-6
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ class AbalonePosTest extends AbaloneTest with ValidatedMatchers {
169169
Pos.G7.left must_== Some(Pos.F7)
170170
Pos.G7.upLeft must_== Some(Pos.G8)
171171
}
172+
}
172173

173174
// 9 - & \' ( ) *
174175
// 8 - 7 8 9 ! ? ¥
@@ -180,12 +181,7 @@ class AbalonePosTest extends AbaloneTest with ValidatedMatchers {
180181
// 2 - i j k l m n
181182
// 1 - a b c d e
182183
// \ \ \ \ \ \ \ \ \
183-
// A B C D E F G H I
184-
"be computed correctly through neighbours function" in {
185-
Pos.G7.neighbours.flatten.mkString(", ") must_== "f7, f6, g8, h7, g6, h8"
186-
}
187-
}
188-
184+
// A B C D E F G H I
189185
"official notation" should {
190186
"swap file and rank indexes" in {
191187
Pos.G6.officialNotationKey must_== "f7"

0 commit comments

Comments
 (0)