Skip to content

Commit 01a24ad

Browse files
author
Daniel Barclay
committed
ColoredLines: Moved tap-UI selection state up into UpperGameState--not cleaned.
1 parent e501269 commit 01a24ad

File tree

7 files changed

+135
-107
lines changed

7 files changed

+135
-107
lines changed

src/main/scala/com/us/dsb/explore/algs/coloredlines/manual/game/GameLogicSupport.scala

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ object GameLogicSupport {
3232
//???? probably split into BoardState-level vs. level of BoardState + score
3333
case class MoveResult(boardPlus: BoardPlus,
3434
//??? clarify re placing next three balls (re interpreting differently in different contexts
35-
anyRemovals: Boolean)
35+
anyRemovals: Boolean,
36+
//?????? clean this hack (used in only one case; re-plumb that case without this):
37+
clearSelection: Boolean
38+
)
3639
{
3740
println(s"??? $this")
3841
//??? print("")
@@ -49,13 +52,13 @@ object GameLogicSupport {
4952
private[game] def placeInitialBalls(boardPlus: BoardPlus)(implicit rng: Random): MoveResult = {
5053
val postPlacementsResult =
5154
//???? parameterize:
52-
(1 to 5).foldLeft(MoveResult(boardPlus, false)) {
55+
(1 to 5).foldLeft(MoveResult(boardPlus, false, false)) {
5356
case (resultSoFar, _) =>
5457
val address =
5558
pickRandomEmptyCell(resultSoFar.boardPlus).getOrElse(scala.sys.error("Unexpectedly full board"))
5659
val postPlacementBoardPlus = resultSoFar.boardPlus.withBallAt(address, pickRandomBallKind())
5760
val placementHandlingResult = LineDetector.handleBallArrival(postPlacementBoardPlus, address)
58-
MoveResult(placementHandlingResult.boardPlus, placementHandlingResult.anyRemovals)
61+
MoveResult(placementHandlingResult.boardPlus, placementHandlingResult.anyRemovals, false)
5962
}
6063

6164
val replenishedOnDeckBoard = replenishOnDeckBalls(postPlacementsResult.boardPlus.boardState)
@@ -72,12 +75,12 @@ object GameLogicSupport {
7275
}
7376
import Action._
7477

75-
def interpretTapLocationToTapAction(boardPlus: BoardPlus,
78+
def interpretTapLocationToTapAction(tapUiState: UpperGameState,
7679
address: CellAddress): Action =
77-
tapAndStateToTapAction(onABall = boardPlus.hasABallAt(address),
78-
isSelectedAt = boardPlus.isSelectedAt(address),
79-
hasABallSelected = boardPlus.hasABallSelected,
80-
hasAnyCellSelected = boardPlus.hasAnyCellSelected)
80+
tapAndStateToTapAction(onABall = tapUiState.boardPlus.hasABallAt(address),
81+
isSelectedAt = tapUiState.isSelectedAt(address),
82+
hasABallSelected = tapUiState.hasABallSelected,
83+
hasAnyCellSelected = tapUiState.hasAnyCellSelected)
8184

8285
private def tapAndStateToTapAction(onABall: Boolean,
8386
isSelectedAt: Boolean,
@@ -128,7 +131,7 @@ object GameLogicSupport {
128131
//???? for 1 to 3, consume on-deck ball from list, and then place (better for internal state view);;
129132
// can replenish incrementally or later; later might show up better in internal state view
130133
boardPlus.boardState.getOnDeckBalls
131-
.foldLeft(MoveResult(boardPlus, false)) {
134+
.foldLeft(MoveResult(boardPlus, false, false)) {
132135
case (curMoveResult, onDeckBall) =>
133136
pickRandomEmptyCell(curMoveResult.boardPlus) match {
134137
case None => // board full; break out early (game will become over)
@@ -210,20 +213,29 @@ object GameLogicSupport {
210213
}
211214

212215

213-
private[game] def doTryMoveBall(boardPlus: BoardPlus, //???? change to game state to carry and update score?
216+
private[game] def doTryMoveBall(boardPlus: BoardPlus,
214217
from: CellAddress,
215218
to: CellAddress
216219
)(implicit rng: Random): MoveResult = {
220+
//?????? re-plumb returning indication of whether to clear selection
221+
// - first, pull clear-selection flag out of (current) MoveResult; return
222+
// tuple or wrapper move-result adding flag
223+
// - eventually, separate move-ball move validation from actually moving
224+
// (selection clearing depends on just validity of move, not on deleting
225+
// any lines)
226+
// - see note near some Option/etc. re encoding only valid moves at
227+
// that point in move-execution path
217228
val canMoveBall = pathExists(boardPlus, from, to)
218229
canMoveBall match {
219230
case false => // can't move--ignore (keep selection state)
220-
MoveResult(boardPlus, false)
231+
MoveResult(boardPlus, anyRemovals = false, clearSelection = false)
221232
case true =>
222-
val deselectedBoardPlus = boardPlus.withNoSelection
223-
val moveBallColor = deselectedBoardPlus.getBallStateAt(from).get //????
224-
val postMoveBoard = deselectedBoardPlus.withNoBallAt(from).withBallAt(to, moveBallColor)
233+
val moveBallColor = boardPlus.getBallStateAt(from).get //????
234+
val postMoveBoard = boardPlus.withNoBallAt(from).withBallAt(to, moveBallColor)
225235

226-
val postHandlingResult = LineDetector.handleBallArrival(postMoveBoard, to)
236+
val postHandlingResult =
237+
LineDetector.handleBallArrival(postMoveBoard, to)
238+
.copy(clearSelection = true)
227239
if (! postHandlingResult.anyRemovals )
228240
placeNextBalls(postHandlingResult.boardPlus)
229241
else

src/main/scala/com/us/dsb/explore/algs/coloredlines/manual/game/UpperGameState.scala

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ private[manual] object UpperGameState {
1414
/**
1515
* Result of completed game.
1616
*/
17-
private[manual] sealed trait GameResult //???? change to final score (and maybe stats?)
17+
private[manual] sealed trait GameResult
1818
private[manual] object GameResult {
1919
private[manual] case class Done(score: Int) extends GameResult
2020
}
@@ -23,7 +23,7 @@ private[manual] object UpperGameState {
2323
val initialPlacementResult = GameLogicSupport.placeInitialBalls(BoardPlus.empty)
2424
//????? probably split GameState level from slightly lower game state
2525
// carrying board plus score (probably modifying MoveResult for that)
26-
UpperGameState(initialPlacementResult.boardPlus, None)
26+
UpperGameState(initialPlacementResult.boardPlus, None, None)
2727
}
2828

2929
private[manual/*game*/] def initial(seed: Long): UpperGameState = makeInitialState(new Random(seed))
@@ -35,12 +35,30 @@ import UpperGameState._
3535

3636
/** Game state AND currently controller.
3737
* @constructor
38-
* @param gameResult `None` means no win or draw yet
38+
* @param gameResult
39+
* `None` means not ended yet
3940
*/
4041
private[manual] case class UpperGameState(boardPlus: BoardPlus,
42+
selectionAddress: Option[CellAddress],
4143
gameResult: Option[GameResult]
4244
)(implicit rng: Random) {
4345

46+
// top-UI selection:
47+
48+
private[manual /*game*/ ] def withCellSelected(address: CellAddress): UpperGameState =
49+
copy(selectionAddress = Some(address))
50+
51+
private[manual/*game*/] def withNoSelection: UpperGameState =
52+
copy(selectionAddress = None)
53+
54+
private[manual/*game*/] def hasAnyCellSelected: Boolean = selectionAddress.isDefined
55+
private[manual/*game*/] def getSelectionCoordinates: Option[CellAddress] = selectionAddress
56+
private[manual] def isSelectedAt(address: CellAddress): Boolean =
57+
selectionAddress.fold(false)(_ == address)
58+
59+
private[game] def hasABallSelected: Boolean =
60+
selectionAddress.fold(false)(boardPlus.hasABallAt(_))
61+
4462
//????? Probably move to GameLogicSupport
4563

4664
// ?? later refine from Either[String, ...] to "fancier" error type
@@ -49,36 +67,49 @@ private[manual] case class UpperGameState(boardPlus: BoardPlus,
4967
// game history
5068
private[manual] def tryMoveAt(tapAddress: CellAddress): Either[String, UpperGameState] = {
5169
import GameLogicSupport.Action._
52-
val tapAction = GameLogicSupport.interpretTapLocationToTapAction(boardPlus, tapAddress)
70+
val tapAction = GameLogicSupport.interpretTapLocationToTapAction(this, tapAddress)
5371
println("tryMoveAt: tapAction = " + tapAction)
54-
val moveResult: MoveResult =
72+
val postMoveState: UpperGameState =
5573
tapAction match {
5674
case SelectBall |
5775
SelectEmpty =>
58-
MoveResult(boardPlus.withCellSelected(tapAddress), false) //???? ?
76+
this.withCellSelected(tapAddress)
5977
case Deselect =>
60-
MoveResult(boardPlus.withNoSelection, false) //????
78+
this.withNoSelection
6179
case TryMoveBall =>
6280
//???? should TryMoveBall carry coordinates?:
6381
//???? need to split logical moves/plays (e.g., move ball from source
6482
// to target from top-/selection-level ~UI (keep that separate from cursor-to-taps UI))
6583
val fromAddress =
66-
boardPlus.getSelectionCoordinates.getOrElse(sys.error("Shouldn't be able to happen"))
67-
GameLogicSupport.doTryMoveBall(boardPlus, fromAddress, tapAddress)
68-
//???? try to move (conditional) .withNoSelection out of doTryMoveBall
69-
// up to here; need additional could-move flag (addedScore None would be ambiguous)
84+
this.getSelectionCoordinates.getOrElse(sys.error("Shouldn't be able to happen"))
85+
86+
val tryMoveResult =
87+
GameLogicSupport.doTryMoveBall(boardPlus, fromAddress, tapAddress)
7088

89+
//?????? clean:
90+
val xxx =
91+
if (tryMoveResult.clearSelection)
92+
this.withNoSelection
93+
else
94+
this
95+
xxx.copy(boardPlus = tryMoveResult.boardPlus)
7196
case Pass =>
7297
val passResult = GameLogicSupport.doPass(boardPlus)
73-
passResult.copy(boardPlus = passResult.boardPlus.withNoSelection)
98+
//?????? clean the several "this."
99+
this.copy(boardPlus = passResult.boardPlus)
100+
.withNoSelection
74101
}
75102

76103
val nextState =
77-
if (! moveResult.boardPlus.isFull) {
78-
UpperGameState(moveResult.boardPlus, gameResult).asRight
104+
if (! postMoveState.boardPlus.isFull) {
105+
//?????? use .copy
106+
UpperGameState(postMoveState.boardPlus, postMoveState.selectionAddress, gameResult).asRight
79107
}
80108
else {
81-
UpperGameState(moveResult.boardPlus, Some(GameResult.Done(moveResult.boardPlus.getScore))).asRight
109+
UpperGameState(postMoveState.boardPlus,
110+
postMoveState.selectionAddress,
111+
Some(GameResult.Done(postMoveState.boardPlus.getScore))
112+
).asRight
82113
}
83114
nextState
84115
}

src/main/scala/com/us/dsb/explore/algs/coloredlines/manual/game/board/BoardPlus.scala

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,24 @@ package com.us.dsb.explore.algs.coloredlines.manual.game.board
22

33
private[game] object BoardPlus {
44

5-
private[game] def empty: BoardPlus = new BoardPlus(BoardState.empty,0, None)
5+
private[game] def empty: BoardPlus = new BoardPlus(BoardState.empty, 0)
66
}
77

88
//???? move tap-UI selection state out of this low-level game state
99
/**
1010
* CURRENTLY: Core board state (now wrapped), not score yet, tap-UI selection state
1111
*/
1212
private[game] class BoardPlus(private[manual] val boardState: BoardState,
13-
private[this] val score: Int,
14-
//???? move to (low-level) tap-UI state:
15-
private[this] val selectionAddress: Option[CellAddress]
13+
private[this] val score: Int
1614
) {
1715
println("??? BoardPlus : " + this)
1816
//print("")
1917

2018
// internal/support methods:
2119

22-
private[this] def copy(boardState: BoardState = boardState,
23-
score: Int = score,
24-
selectionAddress: Option[CellAddress] = selectionAddress) =
25-
new BoardPlus(boardState, score, selectionAddress)
20+
private[this] def copy(boardState: BoardState = boardState,
21+
score: Int = score) =
22+
new BoardPlus(boardState, score)
2623

2724
// grid balls:
2825

@@ -41,12 +38,6 @@ private[game] class BoardPlus(private[manual] val boardState: BoardState,
4138
copy(boardState = boardState.withNoBallAt(address))
4239

4340

44-
private[game] def withCellSelected(address: CellAddress): BoardPlus =
45-
copy(selectionAddress = Some(address))
46-
47-
private[game] def withNoSelection: BoardPlus =
48-
copy(selectionAddress = None)
49-
5041
//???? move out?
5142
private[manual] def getCellBallStateChar(ballState: Option[BallKind], isSelected: Boolean): String = {
5243
ballState match {
@@ -67,21 +58,13 @@ private[game] class BoardPlus(private[manual] val boardState: BoardState,
6758

6859
private[manual] def getScore: Int = score
6960

70-
// top-UI selection:
71-
72-
private[game] def hasAnyCellSelected: Boolean = selectionAddress.isDefined
73-
private[game] def getSelectionCoordinates: Option[CellAddress] = selectionAddress
74-
private[manual] def isSelectedAt(address: CellAddress): Boolean =
75-
selectionAddress.fold(false)(_ == address)
76-
77-
private[game] def hasABallSelected: Boolean = selectionAddress.fold(false)(hasABallAt)
7861

7962
// renderings:
8063

8164
/** Makes compact single-line string. */
8265
override def toString: String = "< " + boardState.toString + "; " + score + " pts" + ">"
8366

84-
private[this] def renderMultiline: String = {
67+
private[this] def renderMultiline(selectionAddress: Option[CellAddress]): String = {
8568
val cellWidth = " X ".length
8669
val cellSeparator = "|"
8770
val wholeWidth =
@@ -92,16 +75,18 @@ private[game] class BoardPlus(private[manual] val boardState: BoardState,
9275
rowIndices.map { row =>
9376
columnIndices.map { column =>
9477
val addr = CellAddress(row, column)
95-
"" + getCellBallStateChar(getBallStateAt(addr), isSelectedAt(addr)) + " "
78+
val isSelected = selectionAddress.fold(false)(_ == addr)
79+
"" + getCellBallStateChar(getBallStateAt(addr), isSelected) + " "
9680
}.mkString(cellSeparator) // make each row line
9781
}.mkString(rowSeparator) // make whole-board multi-line string
9882
}
9983

100-
private[this] def renderCompactMultiline: String = {
84+
private[this] def renderCompactMultiline(selectionAddress: Option[CellAddress]): String = {
10185
rowIndices.map { row =>
10286
columnIndices.map { column =>
10387
val addr = CellAddress(row, column)
104-
getCellBallStateChar(getBallStateAt(addr), isSelectedAt(addr))
88+
val isSelected = selectionAddress.fold(false)(_ == addr)
89+
getCellBallStateChar(getBallStateAt(addr), isSelected)
10590
}.mkString("|") // make each row line
10691
}.mkString("\n")
10792
}

src/main/scala/com/us/dsb/explore/algs/coloredlines/manual/game/lines/LineDetector.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ object LineDetector { //????? adjust most from using BoardPlus to using just Bo
144144
(postLinesRemovalBoard.withAddedScore(ballPlacementScore), Some(ballPlacementScore))
145145
}
146146
//println(s"-handleBallArrival(... ballTo = $ballTo...).9 = score result = $scoreResult")
147-
MoveResult(boardResult, scoreResult.isDefined)
147+
MoveResult(boardResult, scoreResult.isDefined, false)
148148
}
149149

150150
}

src/main/scala/com/us/dsb/explore/algs/coloredlines/manual/ui/GameUIState.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ private[this] case class GameUIState(gameState: UpperGameState,
5151
val scanAddress = CellAddress(row, column)
5252
val cellStateStr =
5353
gameState.boardPlus.getCellBallStateChar(gameState.boardPlus.getBallStateAt(scanAddress),
54-
gameState.boardPlus.isSelectedAt(scanAddress))
54+
gameState.isSelectedAt(scanAddress))
5555
if (scanAddress == cursorAddress ) {
5656
"*" + cellStateStr + "*"
5757
}

src/test/scala/com/us/dsb/explore/algs/coloredlines/manual/game/UpperGameStateTest.scala

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,58 @@
11
package com.us.dsb.explore.algs.coloredlines.manual.game
22

3+
import com.us.dsb.explore.algs.coloredlines.manual.game.board.{BoardPlus, CellAddress, ColumnIndex, Index, RowIndex, columnIndices, rowIndices}
4+
5+
import org.scalatest.PrivateMethodTester
36
import org.scalatest.funspec.AnyFunSpec
4-
import cats.syntax.option._
5-
import com.us.dsb.explore.algs.coloredlines.manual.game.board.{ColumnIndex, Index, RowIndex}
7+
import org.scalatest.matchers.should.Matchers._
8+
69

710
class UpperGameStateTest extends AnyFunSpec {
811

12+
describe("GameState selection:") {
13+
//???? randomize?
14+
lazy val someRow = rowIndices.head
15+
lazy val someCol = columnIndices.head
16+
lazy val upperGameState0 = UpperGameState.initial()
17+
lazy val address = CellAddress(someRow, someCol)
18+
19+
describe("hasAnyCellSelected should:") {
20+
it("- return false for fresh, initial game state") {
21+
upperGameState0.hasAnyCellSelected shouldBe false
22+
}
23+
it("- return true for game state with selection") {
24+
val selectedGameState = upperGameState0.withCellSelected(address)
25+
selectedGameState.hasAnyCellSelected shouldBe true
26+
}
27+
}
28+
29+
describe("withCellSelected should") {
30+
lazy val selectedGameState = upperGameState0.withCellSelected(address)
31+
32+
it("- select _something_") {
33+
selectedGameState.hasAnyCellSelected shouldBe true
34+
}
35+
it("- select _specified_ cell") {
36+
selectedGameState.isSelectedAt(address) shouldBe true
37+
}
38+
}
39+
40+
describe("withNoSelection should:") {
41+
lazy val selectedGameState = upperGameState0.withCellSelected(address)
42+
lazy val deselectedGameState = selectedGameState.withNoSelection
43+
44+
it("- deselect (anything)") {
45+
deselectedGameState.hasAnyCellSelected shouldBe false
46+
}
47+
it("- deselect selected cell") {
48+
deselectedGameState.isSelectedAt(address) shouldBe false
49+
}
50+
}
51+
//???? test works when no selection anyway
52+
//???? test isSelectedAt matches row/column with withCellSelected
53+
}
54+
55+
956
describe("XxGameState$?. tryMoveAt") {
1057
// import Player._
1158
import scala.language.implicitConversions

0 commit comments

Comments
 (0)