Skip to content

Commit

Permalink
Board: refactor, use a companion Object for fromJson.
Browse files Browse the repository at this point in the history
  • Loading branch information
Sjlver committed Aug 9, 2015
1 parent 16503e7 commit 72ffdd2
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 85 deletions.
2 changes: 1 addition & 1 deletion ArtificialIntelligence/src/main/scala/AIRunner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class AIRunner(
def run(): JsArray = {
val solutions = ArrayBuffer.empty[JsObject]
while (board.startNewGame()) {
val seed = board.sourceSeeds(board.sourceSeedIndex)
val seed = board.currentSourceSeed
val ai = aiFactory(board)
val commands = ai.run()
val encoder = encoderFactory(commands)
Expand Down
110 changes: 62 additions & 48 deletions ArtificialIntelligence/src/main/scala/Board.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,68 @@ import scala.collection.mutable.HashSet

// Contains the game board, does moves, undoes moves, checks validity, computes
// scores, ...
class Board {
// Thrown when a move would be invalid (i.e., lead to a score of zero according to the specification)
class InvalidMoveException(message: String) extends Exception

def fromJson(jsonString: String) {
// We ignore potential invalid JSON, hence the @unchecked.

object Board {
// Create a board from a JSON string
def fromJson(jsonString: String): Board = {
// We ignore potential invalid JSON, hence the @unchecked.
val jsonObject = jsonString.parseJson.asJsObject
jsonObject.getFields("id", "width", "height", "sourceLength") match {
case Seq(JsNumber(id), JsNumber(w), JsNumber(h), JsNumber(sl)) =>
problemId = id.toInt
width = w.toInt
height = h.toInt
sourceLength = sl.toInt
}

val problemId = (jsonObject.fields("id"): @unchecked) match { case JsNumber(id) => id.toInt }
val width = (jsonObject.fields("width"): @unchecked) match { case JsNumber(w) => w.toInt }
val height = (jsonObject.fields("height"): @unchecked) match { case JsNumber(h) => h.toInt }
val sourceLength = (jsonObject.fields("sourceLength"): @unchecked) match { case JsNumber(sl) => sl.toInt }

This comment has been minimized.

Copy link
@samuelgruetter

samuelgruetter Aug 13, 2015

Please have a look at how we did this and get excited about how much boilerplate spray-json's convertTo can save you ;-)


initialGrid = Array.fill[Boolean](width, height)(false)
jsonObject.getFields("filled") match {
case Seq(JsArray(cells)) => {
val initialGrid = Array.fill[Boolean](width, height)(false)
(jsonObject.fields("filled"): @unchecked) match {
case JsArray(cells) => {
cells.foreach { cellObject =>
val cell = HexCell.fromJsonObject(cellObject.asJsObject)
initialGrid(cell.x)(cell.y) = true
}
}
}

jsonObject.getFields("sourceSeeds") match {
case Seq(JsArray(ss)) =>
sourceSeeds = ss.map(jsValue => (jsValue: @unchecked) match {case JsNumber(s) => s.toInt}).toArray
val sourceSeeds = (jsonObject.fields("sourceSeeds"): @unchecked) match {
case JsArray(ss) => ss.map {
jsValue => (jsValue: @unchecked) match {case JsNumber(s) => s.toInt}
}.toArray
}

jsonObject.getFields("units") match {
case Seq(JsArray(units)) => {
blocks = units.map(unit => BlockTemplate.fromJsonObject(unit.asJsObject)).toArray
}
val blocks = (jsonObject.fields("units"): @unchecked) match {
case JsArray(units) => units.map { unit =>
BlockTemplate.fromJsonObject(unit.asJsObject)
}.toArray
}

new Board(problemId, width, height, sourceLength, initialGrid, sourceSeeds, blocks)
}

}

class Board(
// The problem id from the input file
_problemId: Int,

// Width and height of the board
width: Int,
height: Int,

// The number of blocks in the source
sourceLength: Int,

// The grid at the start of a game
initialGrid: Array[Array[Boolean]],

// The list of source seeds
sourceSeeds: Array[Int],

// The list of blocks
blocks: Array[BlockTemplate]
) {

// Thrown when a move would be invalid (i.e., lead to a score of zero according to the specification)
class InvalidMoveException(message: String) extends Exception

def toJsonObject: JsObject = {
val filledCells = ArrayBuffer.empty[(Int, Int)]
0.to(width - 1).foreach { x =>
Expand Down Expand Up @@ -79,8 +103,10 @@ class Board {
return false
}

grid = initialGrid.clone()
random = new DavarRandom(sourceSeeds(sourceSeedIndex))
0.to(width - 1).foreach { x =>
Array.copy(initialGrid(x), 0, grid(x), 0, initialGrid(x).size)
}
random.seed = currentSourceSeed
numBlocksPlayed = -1
blockIndex = -1
isActive = true
Expand Down Expand Up @@ -122,6 +148,13 @@ class Board {
gridToString()
}

override def clone(): Board = {
null
}

def problemId = _problemId
def currentSourceSeed = sourceSeeds(sourceSeedIndex)

private def spawnNextBlock(): Boolean = {
numBlocksPlayed += 1
if (numBlocksPlayed == sourceLength) {
Expand Down Expand Up @@ -223,47 +256,28 @@ class Board {
lines.mkString
}

// Width and height of the board
var height = -1
var width = -1

// The problem id from the input file
var problemId = -1

// The grid ((0, 3) is column zero, row three).
var grid = Array.ofDim[Boolean](0, 0)

// The grid at the start of a game
var initialGrid = Array.ofDim[Boolean](0, 0)
val grid = Array.ofDim[Boolean](width, height)

// The current game we're playing
var sourceSeedIndex = -1

// The list of source seeds
var sourceSeeds = Array.emptyIntArray

// How many units have already completed their move
var numBlocksPlayed = -1

// The number of blocks in the source
var sourceLength = -1

// The index of the current unit into the blocks
var blockIndex = -1

// The list of blocks
var blocks = Array.empty[BlockTemplate]

// This board's random number generator
var random: DavarRandom = null
val random: DavarRandom = new DavarRandom(0)

// The block that is currently active. This is the block that is moved around.
// It will become part of the grid once it is locked.
var activeBlock: Block = null

// This buffer stores the list of past positions/rotations of the active block.
// Used to detect invalid moves
var pastBlockStates = HashSet.empty[Block]
val pastBlockStates = HashSet.empty[Block]

// The score of the current game
var score = 0
Expand Down
3 changes: 1 addition & 2 deletions ArtificialIntelligence/src/main/scala/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,7 @@ object Main {
println("Must specify an input file!")
return
}
val board = new Board()
board.fromJson(scala.io.Source.fromFile(inputFname).getLines.mkString)
val board = Board.fromJson(scala.io.Source.fromFile(inputFname).getLines.mkString)
val aiRunner = new AIRunner(
board,
b => new SamplingAI(b),
Expand Down
11 changes: 5 additions & 6 deletions ArtificialIntelligence/src/test/scala/AIRunnerSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ object AIRunnerSpec {
class AIRunnerSpec extends UnitSpec {
"An AIRunner" should "run AIs" in {
Random.setSeed(42)
val board = new Board
board.fromJson(BoardSpec.BOARD_JSON)
val board = Board.fromJson(BoardSpec.BOARD_JSON)

val aiRunner = new AIRunner(
board,
Expand All @@ -43,9 +42,7 @@ class AIRunnerSpec extends UnitSpec {

"An AIRunner" should "process all seeds" in {
Random.setSeed(42)
val board = new Board
board.fromJson(AIRunnerSpec.TWOSEEDS_JSON)
board.sourceSeeds should be (Array(17, 42))
val board = Board.fromJson(AIRunnerSpec.TWOSEEDS_JSON)

val aiRunner = new AIRunner(
board,
Expand All @@ -54,10 +51,12 @@ class AIRunnerSpec extends UnitSpec {
"AIRunnerSpec")

val solutionsArray = aiRunner.run()
println(solutionsArray.prettyPrint)
//println(solutionsArray.prettyPrint)
solutionsArray match {
case JsArray(solutions) =>
solutions.size should be (2)
(solutions(0).asJsObject.fields("seed"): @unchecked) match { case JsNumber(s) => s.toInt should be (17) }
(solutions(1).asJsObject.fields("seed"): @unchecked) match { case JsNumber(s) => s.toInt should be (42) }
}
}
}
49 changes: 23 additions & 26 deletions ArtificialIntelligence/src/test/scala/BoardSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,34 @@ object BoardSpec {
"sourceLength":100
}
"""

val MINIMAL_BOARD_JSON = """
{
"height":3,
"width":3,
"sourceSeeds":[17],
"units":[{"members":[{"x":0,"y":0}],"pivot":{"x":0,"y":0}}],
"id":42,
"filled":[],
"sourceLength":1
}
"""

}

class BoardSpec extends UnitSpec {

"A Board" should "load itself from JSON" in {
val board = new Board()
board.fromJson(BoardSpec.BOARD_JSON)
val board = Board.fromJson(BoardSpec.BOARD_JSON)

board.height should be (10)
board.width should be (5)
board.problemId should be (42)

board.sourceLength should be (100)
board.sourceSeedIndex should be (-1)
board.isActive should be (false)
}

"A Board" should "write itself to JSON" in {
val board = new Board
board.fromJson(BoardSpec.BOARD_JSON)
val board = Board.fromJson(BoardSpec.BOARD_JSON)
board.startNewGame()

board.toJsonObject.compactPrint should be (
Expand All @@ -49,8 +57,7 @@ class BoardSpec extends UnitSpec {
}

"A Board" should "should process moves" in {
val board = new Board()
board.fromJson(BoardSpec.BOARD_JSON)
val board = Board.fromJson(BoardSpec.BOARD_JSON)
board.startNewGame()

board.activeBlock.template.members should be (Array(HexCell.fromXY(-1, 0), HexCell.fromXY(1, 0)))
Expand All @@ -69,8 +76,7 @@ class BoardSpec extends UnitSpec {
}

"A Board" should "detect translations that lead to repetition" in {
val board = new Board()
board.fromJson(BoardSpec.BOARD_JSON)
val board = Board.fromJson(BoardSpec.BOARD_JSON)
board.startNewGame()

board.doMove(Moves.E)
Expand All @@ -80,8 +86,7 @@ class BoardSpec extends UnitSpec {
}

"A Board" should "detect rotations that lead to repetition" in {
val board = new Board()
board.fromJson(BoardSpec.BOARD_JSON)
val board = Board.fromJson(BoardSpec.BOARD_JSON)
board.startNewGame()

board.doMove(Moves.SE)
Expand All @@ -93,8 +98,7 @@ class BoardSpec extends UnitSpec {
}

"A Board" should "detect moves that exit the grid" in {
val board = new Board()
board.fromJson(BoardSpec.BOARD_JSON)
val board = Board.fromJson(BoardSpec.BOARD_JSON)
board.startNewGame()

board.doMove(Moves.SW)
Expand All @@ -112,8 +116,7 @@ class BoardSpec extends UnitSpec {
}

"A Board" should "clear full rows" in {
val board = new Board()
board.fromJson(BoardSpec.BOARD_JSON)
val board = Board.fromJson(BoardSpec.BOARD_JSON)
board.startNewGame()

// First block, fill (4, 3) and (3, 1)
Expand Down Expand Up @@ -150,8 +153,7 @@ class BoardSpec extends UnitSpec {
}

"A Board" should "keep track of score correctly" in {
val board = new Board()
board.fromJson(BoardSpec.BOARD_JSON)
val board = Board.fromJson(BoardSpec.BOARD_JSON)
board.startNewGame()

board.score should be (0)
Expand Down Expand Up @@ -183,8 +185,7 @@ class BoardSpec extends UnitSpec {
}

"A Board" should "should end the game when the grid is full" in {
val board = new Board()
board.fromJson(BoardSpec.BOARD_JSON)
val board = Board.fromJson(BoardSpec.BOARD_JSON)
board.startNewGame() should be (true)
board.isActive should be (true)

Expand All @@ -199,13 +200,9 @@ class BoardSpec extends UnitSpec {
}

"A Board" should "should end the game when all blocks have been played" in {
val board = new Board()
board.fromJson(BoardSpec.BOARD_JSON)
val board = Board.fromJson(BoardSpec.MINIMAL_BOARD_JSON)
board.startNewGame() should be (true)

// Override the source length for testing
board.sourceLength = 1

// Lock the first block
board.doMove(Moves.W) should be (true)
board.doMove(Moves.W) should be (false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ piimiiippiimmmeemimiipimmimmipppimmimeemeemimiieemimmmm"""
"id":6,"filled":[],"sourceLength":150}"""

"A Board" should "correctly replay a sample solution" in {
val board = new Board
board.fromJson(PROBLEM_6_JSON)
val board = Board.fromJson(PROBLEM_6_JSON)
board.startNewGame()

val resultSteps = ArrayBuffer.empty[JsObject]
Expand Down

0 comments on commit 72ffdd2

Please sign in to comment.