본문 바로가기

OOP, FP/스칼라로 만드는 테트리스

08. [스칼라 테트리스] 04. 4개의 블록을 표현하는 테트로미노

이번에는 테트로미노를 구현해보자.

블록과 테트로미노가 헷갈릴 수 있기 때문에 다시 한번 정리하고 넘어가겠다.


블록

블록은 하나의 정사각형을 의미한다.

블록은 T, Z, S 등 7가지의 종류와 EMPTY라는 빈 블록으로 분류된다.


테트리스라는 게임에서 블록을 회전시켜 모양을 변경할 수 있기 때문에 각각의 블록은 4가지 다른 모양을 가져야 한다.

블록이 가진 모양은 4개의 튜플(점)으로 구현한다. (T의 경우 (0, 0), (1, 0), (-1, 0), (0, -1))  


테트로미노

테트로미노는 4개의 정사각형(블록)으로 이루어진다. 

4개의 블록으로 구성되지만 블록의 종류는 같아야 한다.

하나의 테트로미노가 L, T, EMPTY, T 이런 식으로 구성되는 것은 말이 안되기 떄문이다.

따라서 하나의 테트로미노 객체에는 하나의 블록 객체만 있으면 된다.


블록이 회전된 방향에 따라 다른 좌표에 표시되므로 현재 방향을 의미하는 orientation 속성이 필요하다.

추가적으로, 블록이 처음 생성될 때 그려지는 위치값을 갖는다.

보통 테트리스에서 테트로미노가 생성될 때, 화면 중앙부터 시작하기 때문이다.


테트로미노 구현

1
2
3
class Tetromino(val block: Block.Value,
                val position: Tuple2[IntInt= (Board.Width / 21), 
                val orientation: Int = 0)
cs

앞서 설명한 세 개의 속성을 갖는 테트로미노 클래스 코드.

각각의 속성은 블록, 생성 위치값, 회전값을 의미한다.

테트리스에서 다음에 생성될 블록은 랜덤으로 생성된다.


1
2
3
def this() {
  this(Block.nextBlock)
}
cs

블럭을 랜덤으로 생성하는 기능은 Block.nextBlock()에 구현되어 있다.

이를 호출하는 생성자를 만들어주자.


테트로미노의 좌표

기본적인 준비가 끝났다.

테트로미노의 블록의 좌표는 4개의 정사각형의 좌표와 회전값으로 구한다.

Block.getPositions(block) 호출하면 현재 블록의 모양에 맞는 좌표를 반환한다.

하지만, 이 좌표는 회전된 모양을 표현하기 위해 2차원 배열을 반환한다.

이를 위해 orientation 속성이 필요한데, Block.getPositions(block)(orientation) 와 같이 호출하여

(1) 현재 테트로미노의 회전방향에 맞는 좌표값을 구할 수 있다.


또 다른 속성인 position도 좌표를 구할 때 쓰인다.

(1) 에서 얻은 좌표값은 원점이 (0, 0)을 기준으로 한다.

새로 생성되는 테트로미노는 화면의 중앙부터 시작해야 하므로 (1)의 결과로 얻은 각 좌표에 position.x, position.y를 더해서 (2) 실제 화면에 표시되는 좌표값을 구해야 한다.


1
2
3
4
5
def getBlockPositions: Array[(IntInt)] = {
  Block.getPositions(block)(orientation).map {
    blockPosition => (blockPosition._1 + position._1, blockPosition._2  + position._2)
  }
}
cs

스칼라는 함수를 호출할 때 () 대신 {} 중괄호를 사용할 수 있다.

단, 인자가 하나일 경우에만 가능하다.

Tetromino.getBlockPositions 메소드는 실제 화면에 표시되는 좌표값을 반환한다.


테트로미노의 움직임

테트로미노가 움직이는 방법은 회전, 좌로 이동, 우로 이동, 아래로 이동과 같이 네가지가 있다.

제일 먼저 필요한 메소드는 테트로미노를 복사하는 메소드다.

함수형 프로그래밍은 불변을 지향한다. 때문에 스칼라에서도 var 키워드 대신 val 키워드를 사용하길 권장한다고 한다.

테트로미노는 움직일 수 있어야 하는데, val로 선언된 속성을 변경할 수 없기 때문에 복사 기능을 제공해야한다.


1
2
3
4
def copy(block: Block.Value = block,
         position: Tuple2[IntInt= position,
         orientation: Int = orientation): Tetromino = 
   new Tetromino(block, position, orientation)
cs

구현은 간단하다. 생성자와 똑같은 인자를 받으면서 기본 값을 정해놓는다.

copy(position = (1, 2))과 원본과 같이 다른 속성에만 인자를 전달해주면 된다.


이제 진짜 움직임을 구현해야한다.


1
2
3
4
5
def withMoveLeft: Tetromino = copy(position = (position._1 - 1, position._2))
  
def withMoveRight: Tetromino = copy(position = (position._1 + 1, position._2))
  
def withMoveDown: Tetromino = copy(position = (position._1, position._2 + 1))
cs

좌, 우, 아래 이동을 구현한 코드이다.

앞서 준비한 복사메소드에 바꾸고자 하는 좌표값을 전달하면서 호출하면된다.

2차원 좌표평면에서 한 점의 x, y값이 증가 혹은 감소할 경우 이동하는 방향으로 생각하면된다.


1
def withRotation: Tetromino = copy(orientation = (orientation + 1) % Block.getPositions(block).size)
cs

회전의 경우에만 조금 다르다.

원본의 orientation을 1만큼 증가시키고 이를 4로 나눈 나머지를 전달한다.

테트로미노는 4개의 모양을 갖고 있다. 즉, 4번 회전하면 다시 처음의 상태로 돌아가야 한다.

orientation은 0~3의 값만 들어갈 수 있으며, 3에서 1이 증가할 경우 다시 0으로 돌아간다.


테트로미노 테스트

1
2
3
4
def fixture =
  new {
    val tetromino = new Tetromino(block = Block.T)
  }
cs

테스트에 사용할 테트로미노의 블록은 T


1
2
3
4
5
6
it should "have one block" in {
  for (i <- 0 until 10) {
    val block = new Tetromino().block
    Block.values should contain (block)
  }
}
cs

테트로미노는 하나의 블록을 가진다.


1
2
3
4
it should "have four position" in {
  
  fixture.tetromino.getBlockPositions should have size (4)
}
cs

테트로미노는 네 개의 좌표값을 갖는다.


1
2
3
4
5
6
7
8
9
10
11
"withMoveLeft" should "return copied Tetromino with position.x decreased by 1 " in {
  val origin = fixture.tetromino
    val copedWithXdc1 = origin.copy(position = (origin.position._1 - 1, origin.position._2))
  val withLeft = fixture.tetromino.withMoveLeft
  
  withLeft.position should not equal (origin.position)
  withLeft.block should equal (origin.block)
  withLeft.orientation should equal (origin.orientation)
  
  copedWithXdc1.position should equal (withLeft.position)
}
cs

withMoveLeft 메소드는 x가 1만큼 감소된 복사본을 반환한다.


1
2
3
4
5
6
7
8
9
10
11
"withMoveRight" should "return copied Tetromino with position.x increased by 1 " in {
  val origin = fixture.tetromino
  val copedWithXic1 = origin.copy(position = (origin.position._1 + 1, origin.position._2))
  val withRight = fixture.tetromino.withMoveRight
  
  withRight.position should not equal (origin.position)
  withRight.block should equal (origin.block)
  withRight.orientation should equal (origin.orientation)
  
  copedWithXic1.position should equal (withRight.position)
}
cs

withMoveRight 메소드는 x가 1만큼 증가된 복사본을 반환한다.


1
2
3
4
5
6
7
8
9
10
11
"withMoveDown" should "return copied Tetromino with position.y increased by 1 " in {
  val origin = fixture.tetromino
  val copedWithYic1 = origin.copy(position = (origin.position._1, origin.position._2 + 1))
  val withDown = fixture.tetromino.withMoveDown
  
  withDown.position should not equal (origin.position)
  withDown.block should equal (origin.block)
  withDown.orientation should equal (origin.orientation)
  
  copedWithYic1.position should equal (withDown.position)
}
cs

withMoveDown 메소드는 y가 1만큼 증가된 복사본을 반환한다.


1
2
3
4
5
6
7
8
9
10
11
"withRotation" should "return copied Tetromino with rotated orientation value" in {
  val origin = fixture.tetromino
  val copedWithOrientMod = origin.copy(orientation = origin.orientation + 1 % Block.getPositions(origin.block).size)
  val withRotation = fixture.tetromino.withRotation
  
  withRotation.position should equal (origin.position)
  withRotation.block should equal (origin.block)
  withRotation.orientation should not equal (origin.orientation)
  
  copedWithOrientMod.orientation should equal (withRotation.orientation)
}
cs

withRotation 메소드는 회전된 방향값을 갖는 복사본을 반환한다.