Summa sidvisningar

fredag 7 januari 2011

Skapa Piece och Position

Jag var inte helt nöjd med hur klasserna såg ut i varken C++ eller Java-versionen. Nu har jag chansen att förbättra detta. Dags sätta igång och skriva koden för att sätta en bit på spelplanen. För att klara det behövs ett antal nya klasser och min tanke är att ha en klass Position som ansvarar för att sätta ut en Piece på våran Board. Den håller en referens till Piece med dess position och en Board som hanterar brädet och en GameSettings som hanterar olika sättningar, t ex bitens startposition.

Jag har brutit ut testkod till klassen BaseTest som nu alla test använder sig av:
package net.sf.tetrisai

import org.scalatest.junit.{JUnitSuite, ShouldMatchersForJUnit}

abstract class BaseTest extends JUnitSuite with ShouldMatchersForJUnit

Testen för Position, PositionTest, ser ut enligt följande:
package net.sf.tetrisai

import piece.{PieceT, Piece, PieceS}
import org.junit.{Before, Test}
import settings.{GameSettings, DefaultGameSettings}

class PositionTest extends BaseTest {
  var settings: GameSettings = null
  var board: Board = null
  var piece: Piece = null

  @Before def setUp() {
    settings = new DefaultGameSettings
  }

  def emptyPosition = {
    board = Board(5,4)
    piece = new Piece(new PieceS)
    new Position(board, piece, settings)
  }

  @Test def setPiece() {
    val position = emptyPosition
    position.setPiece()

    board should be (Board(Array(
      "#--xx-#",
      "#-xx--#",
      "#-----#",
      "#-----#",
      "#######")))
  }

  @Test def clearPiece() {
    val board = Board(Array(
      "#-xxx-#",
      "#--x--#",
      "#-----#",
      "#-----#",
      "#######"))
    val piece: Piece = new Piece(new PieceT)
    val position = new Position(board, piece, settings)
    position.clearPiece()

    board should be (Board(5,4))
  }

  @Test def rotatePieceOnce() {
    val position = emptyPosition
    piece.rotate()
    position.setPiece

    board should be (Board(Array(
      "#-x---#",
      "#-xx--#",
      "#--x--#",
      "#-----#",
      "#######")))
  }

  @Test def rotatePieceTwice() {
    val position = emptyPosition
    piece.rotate()
    piece.rotate()
    position.setPiece

    board should be (Board(Array(
      "#--xx-#",
      "#-xx--#",
      "#-----#",
      "#-----#",
      "#######")))
  }
}

Det man kan notera här är att jag bryter mot inkapsling av klassen Position då jag utanför klassen kan ändra i instanser av Piece och Board. För att hindra detta hade jag behövt göra Piece och Position immutable och t ex lägga till metoden rotatePiece till klassen Position. Detta är i nuläget ett val jag gjort och vi får se om det löper väl ut.

För att kunna jämföra mina instanser av Board i testet har jag implementerat metoden equals i både BoardLine (har tagit bort oväsentlig kod)...
class BoardLine(val line: Array[Byte]) {
  ...

  override def equals(that: Any) = that match {
    case other: BoardLine => line.toList == other.line.toList
    case _ => false
  }
}
...och Board:
class Board(
    val width: Int,
    height: Int,
    private val lines: Array[BoardLine]) {
  ...
 
  override def equals(that: Any) = that match {
    case other: Board => lines.toList == other.lines.toList
    case _ => false
  }

Här använder jag mig av Scalas mönstermatchning. Det är ett ganska smidigt sätt att slippa använda metoderna isInstanceOf och asInstanceOf. En annan sak att notera är att Scala fungerar på samma sätt som Java när man ska jämföra arrayer, t ex kommer man få false som svar om man jämför två olika instanser av en array trots att de har samma innehåll. Jag har valt att lagra mina data i Arrayer för att de är något effektivare än Listor. Då det endast är testen som behöver jämföra instanser gör det ingenting att vi här tvingas konvertera dessa till listor via metoden toList.

Vår nya klass Piece ser ut så här:
package net.sf.tetrisai.piece

object Piece {
  def apply(pieceType: PieceType) = new Piece(pieceType)

  def apply(index: Int) = {
    val pieceType = PieceType(index)
    new Piece(pieceType)
  }
}

class Piece(
     private val pieceType: PieceType,
     private var rotation: Int = 0,
     private val rotationDirection: Int = 1) {
  def height = pieceType.height(rotation)
  def dots = pieceType.shape(rotation).dots
  def rotate() = rotation = (rotation + rotationDirection) & pieceType.rotationModulus
}

Rad 6 ser till att man kan skapa en bit genom att skriva t ex Piece(1). Rad 4 stödjer att man instansierar en bit med syntaxen, t ex Piece(new PieceT). Rad 13-15 definierar både hur konstruktorn ser ut och vilka medlemsvariabler som lagras av klassen. Rad 14-15 använder sig av default-värden som har det goda med siga att man reducerar antalet konstruktorer som vi t ex i Java hade behövt skapa.

Piece lutar sig mycket mot PieceType:
package net.sf.tetrisai.piece

object PieceType {
  private val rotationModulus: Array[Int] = Array(0, 0, 1, 0, 3)
  private val pieces: Array[PieceType] = Array(
    new PieceI, new PieceZ, new PieceS, new PieceJ, new PieceL, new PieceT, new PieceO
  )

  def apply(index: Int): PieceType = pieces(index)
}

abstract class PieceType {
  def index: Int
  def character: String
  def maxRotations = heights.length
  def rotationModulus = PieceType.rotationModulus(maxRotations)
  def height(rotation: Int) = heights(rotation)
  def shape(rotation: Int) = shapes(rotation)
  protected def heights: Array[Int]
  protected def shapes: Array[PieceShape]
}

Klassen PieceType är en abstrakt klass som kan definierar en viss typ av bit. Varje typ av bit finns sedan i sin egen konkreta klass t ex PieceS...
package net.sf.tetrisai.piece

class PieceS extends PieceType {
  val index = 2
  val character = "S"
  protected val heights = Array(2, 3)
  protected val shapes = Array(
    new PieceShape(Array(Point(1,0), Point(2,0), Point(0,1), Point(1,1))),
    new PieceShape(Array(Point(0,0), Point(0,1), Point(1,1), Point(1,2)))
  )
}

...eller PieceJ:
package net.sf.tetrisai.piece

class PieceJ extends PieceType {
  val index = 3
  val character = "J"
  protected val heights = Array(2, 3, 2, 3)
  protected val shapes = Array(
    new PieceShape(Array(Point(0,0), Point(1,0), Point(2,0), Point(2,1))),
    new PieceShape(Array(Point(0,0), Point(1,1), Point(0,1), Point(0,2))),
    new PieceShape(Array(Point(0,0), Point(0,1), Point(1,1), Point(2,1))),
    new PieceShape(Array(Point(1,0), Point(1,1), Point(0,2), Point(1,2)))
  )
}

PieceShape kapslar in de fyra "dot":s som en bit i ett visst rotationsläge består av:
package net.sf.tetrisai.piece

class PieceShape(val dots: Array[Point]) {
}

Övriga bitar och klasser relaterade till Piece finns i paketet piece.

Klassen Position ser ut så här:
package net.sf.tetrisai

import piece.{Point, Piece}
import settings.GameSettings

class Position(board: Board, piece: Piece, settings: GameSettings) {
  var piecePosition: Point = settings.pieceStartPosition(board.width)

  def setPiece() = piece.dots.foreach(dot => board.set(dot + piecePosition))
  def clearPiece() = piece.dots.foreach(dot => board.clear(dot + piecePosition))
}

En rolig detalj här är att jag lagt till operatorn plus (+) till klassen Point för att kunna placera biten i metoderna setPiece() och clearPiece():
package net.sf.tetrisai.piece

object Point {
  def apply(x: Int, y: Int) = new Point(x, y)
}

class Point(val x: Int, val y: Int) {
  def +(that: Point): Point = new Point(x + that.x, y + that.y)

  override def equals(that: Any) = that match {
    case other: Point => x == other.x && y == other.y
    case _ => false
  }
}

Nu går det att flytta, rotera och ta bort en bit från en Board, en bra början!

Inga kommentarer:

Skicka en kommentar