First of, I found an explanation on the difference between the various array-types:
scala - array vs arrayseq
Now on your question on how you could get a one-dimensional Array from a two-dimensional array: But let us assume that, instead of Array[Array[T]]
we have List[List[T]]
(just because I like lists more; but you should have no problem applying this to arrays).
What does foldLeft[A](initial: A)(function: (A, B) => A)
do? It iterates over the collection and aggregates a value of type A
, by applying a function (A, B) => A
at each element. Here, B
is the generic type of the list. In our example, we fold a list of lists of integers (B
), to a list of integers (A
) by concatenating (++
) the intermediate value a
with the next element in the list.
Edit2:
When applying this to the Biome-example, we can create a map from positions to biomes as follows:
def create(width:Int, height:Int): Map[Vector2, Biome] =
{
// we can define functions within functions
def isGrasslandBiome(x: Int, y: Int): Boolean =
y < height / 3 * 2
// A for-comprehension returns a sequence of all computed elements
val positionWithBiomes: Seq[(Vector2, Biome)] =
for{
x <- 0 until width
y <- 0 until height
} yield
{
val biome =
if (isGrasslandBiome(x, y))
GrasslandBiome(DefaultWidth, DefaultHeight)
else
SkyBiome(DefaultWidth, DefaultHeight)
((x, y), biome)
}
// We fold each element of the sequence into a map
val biomes: Map[Vector2, Biome] =
positionWithBiomes.foldLeft[Map[Vector2, Biome]](Map.empty){
case (map, ((x, y), biom)) => map + ((x, y) -> biom)
}
biomes
}
We iterate over a sequence of ((Int, Int), Biome)
s and aggregate them in a Map
. I would prefer immutable Collections (most standard Scala collections) to mutable collections. Mutability can give you a hard time when debugging code for a possible benefit in performance. Also, immutability fits the functional style, because a function (in the mathematical sense) is always pure, i.e., does not alter the state.
Nevertheless, for the sake of the example, and because the internal representation of tileMap
might actually be performance-critical, let us define a class TileMap
, which is essentially a wrapper for a two-dimensional array: (Note: type TileMapInternal = mutable.ArraySeq[mutable.ArraySeq[Int]]
)
/** A tile map is our internal, mutable representation of a Biome
*
* Note that this makes Biome mutable
*/
/*mutable*/ class TileMap private(private val value: TileMapInternal, private val width: Int, private val height: Int) {
def print()
{
value foreach { row =>
row foreach { tile =>
Predef.print(s"$tile ")
}
println()
}
}
def setAllTiles(tileValue: Int) {
for{
x <- 0 until width
y <- 0 until height
}{
setTile(tileValue)(x, y)
}
}
def setTile(tileValue: Int)(x: Int, y: Int) {
value(y)(x) = tileValue
}
def addRows(other: TileMap): TileMap =
new TileMap(value ++ other.value,
width, // we add rows -> width stays the same
height + other.height
)
def addColumns(other: TileMap): TileMap =
new TileMap(
value zip other.value map {case (first, second) => first ++ second},
width + other.width,
height // we add columns -> height stays the same
)
}
This class has a private constructor, because we do not want the internal representation to leak out. In Scala, you can define a companion object which has access to all the private members. Basically, a companion object contains all methods that you would declare static in Java.
// companion object
object TileMap {
def empty(width: Int, height: Int): TileMap = {
val rows = new mutable.ArraySeq[mutable.ArraySeq[Int]](height)
for (row <- 0 until height) {
rows(row) = new mutable.ArraySeq[Int](width)
for (cell <- 0 until width) {
rows(row)(cell) = 0
}
}
new TileMap(rows, width, height)
}
}
The companion object will initialize an empty TileMap for us. This could most likely be written more elegant, but it does what it is supposed to: It creates an array of width x height and sets all elements to zero. Your Nullpointer exception probably happened because you didn't initialize the elements properly. When you tried to print them manually, they probably just printed 0
instead of null
because of autoboxing.
Edit
How to accumulate the map in a single 2d array:
The methods addRows
and addColumns
create a new TileMap
(i.e., your two-dimensional array) by combining either the inner or the outer array. Combining the outer array is easy: Simply add them using the method ++
which is defined on all scala collections. To combine the inner arrays, we can first zip
the two arrays together, which gives us a single array of tuples of arrays (Array[(Array[Int], Array[Int])]
). We can then map
this array to a new, regular two-dimensional array by applying ++
on each tuple.
We can now use those two functions to reduce a two dimensional array of tile maps (which is basically what we have stored by using val biomes: Map[Vector2, Biome]
) into a single tile map.
I should mention that, by design, this looks very fragile: I am imagining things to break once your tile maps are not of equal size. Also, messing up the order in which the rows/columns are combined could be nearly impossible to debug, and if your map of biomes isn't complete, computing the terrain may simply crash. Nevertheless, here's how I'd solve the problem with the current design:
def terrain(biomes: Map[Vector2, Biome]): TileMap = {
val (rows, _) = biomes.keys.unzip
rows.toList.sortBy{id => id} map {
row => terrainForRow(row, biomes)
} reduceLeft {
(accumulator: TileMap, nexTileMap: TileMap) => accumulator addColumns nexTileMap
}
}
def terrainForRow(column: Int, biomes: Map[Vector2, Biome]): TileMap =
biomes.filter{
case ((x, y), _) => x == column
}.toList.sortBy{
case ((_, y), _) => y
}.map{
case ((_, _), biome) => biome.tileMap
}.reduceLeft[TileMap]{
(accumulator, nextTileMap) => accumulator addRows nextTileMap
}
This certainly needs some explanation: terrainForRow
shall take a single row (i.e., all values in the map with the same value for x
) and concatenate them in the correct order. The order is determined by y
, because we want the order of the columns to be preserved.
First, we filter
all biomes that have the same value for x
Second, we add ordering to the collection by converting it to list, and sort the list according to the value of y
Third, we throw away the position (value of x
and y
), and extract the tile map. This is needed if we want to use reduceLeft
, because of its type signature.
Last, we accumulate the tile maps, calling addRows
which we defined earlier. Note that reduceLeft
is only defined for collections of size two and bigger
In the method terrain
, we call terrainForRow
for each row (that is, after we sorted the tile maps by row). Each result already is a TileMap
, so we can call reduceLeft
on the sequence of results and reduce them to a single TileMap
by using the function addColumns
that we defined.
I've put the updated code at http://pastie.org/9598459
I'd like to add some things you can do in scala, since you want to learn the language:
Currying and partially applied functions
Look at the signature of the method setTile
: It has two parameter lists. This is called currying and allows us to partially apply functions. For example, we can create a function like this:
// let us partially apply the setTile function
val setGrassTileAt: (Int, Int) => Unit =
biomesTileMap.setTile(GrassTileValue)
// now tileBuilder will set the value of GrassTile at each position we provide
setGrassTileAt(0, 0)
setGrassTileAt(3, 1)
setGrassTileAt(0, 1)
The function setGrassTile
(stored in a variable, because in scala, functions are first-class citizens), takes two integer values (the coordinates) and sets the value of the biome at that coordinate to the GrassTileValue
.
Uniform access principle and case classes
abstract class Biome
{
// Scala supports the uniform access princple
// A client is indifferent whether this is def, val or var
// A subclass can implement width and height as val
def width: Int
def height: Int
def tileMap: TileMap
}
// Case classes automatically define the methods equals, hashCode and toString
// They also have an implicit companion object defining apply and unapply
// Note that equals is now defined on width and height, which is probably not what
// we want in this case.
case class SkyBiome(width: Int, height: Int) extends Biome {
val tileMap: TileMap = TileMap.empty(width, height)
}
case class GrasslandBiome(width: Int, height: Int) extends Biome {
val tileMap: TileMap = TileMap.empty(width, height)
}
In Scala, clients should not distinguish between variables and methods when accessing a member of an object. For example, it does not matter whether width
is a def
in the supertype or a val
in the subtype.
Case classes are classes that override equals
, hashCode
and toString
. Also, they provide an extractor unapply
(which you can use when match
ing) and an apply
method (which is applyied when you look at the object as a function). Case classes are most beneficial for value objects.
I hope I could help you a bit. Edit: Code before my updated answer: http://pastie.org/9586136