본문 바로가기

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

07. [스칼라 테트리스] 03. 블록 클래스 구현, 테스트

스칼라 테트리스 github


이제 본격적으로 테트리스 코드를 작성하고, 테스트 케이스를 만든다.

지금까지 코드를 작성하고 일일히 메소드를 실행하면서 테스트를 진행했다.

하지만, 이는 나 혼자만 알고있는 테스트 과정으로, 남들은 이 코드가 올바르게 동작하는 지 알 수 없다.


테스트 케이스를 작성해 본 적이 없고, 영어가 많이 부족하기 때문에 힘들겠지만...

이번에는 하나의 클래스를 만들고 메소드마다 테스트 케이스를 작성하는 방식으로 진행하려고 한다.


스칼라는 scalatest 라는, 자바의 JUnit과 같은 테스트 라이브러리가 존재한다.

이 라이브러리는 여러가지 테스팅 스타일을 제공한다.

이 테스팅 스타일 중 나는 FlatSpec라는 스타일을 선택했다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.scalatest.FlatSpec
 
class SetSpec extends FlatSpec {
 
  "An empty Set" should "have size 0" in {
    assert(Set.empty.size == 0)
  }
 
  it should "produce NoSuchElementException when head is invoked" in {
    assertThrows[NoSuchElementException] {
      Set.empty.head
    }
  }
}
 
cs

위처럼 하나의 영어 문장으로 무엇을 위한 테스트인지 설명하고,

내부에서 테스트 코드를 작성하는 스타일이다.

자바 JUnit은 메소드명을 이용해서 테스트의 용도를 구분해야 함에 비해,

마치 주석을 보는 것처럼 영어 문장으로 용도를 설명할 수 있어서 선택하게 되었다.


정사각형으로 이루어진 블록과 관련된 Block 객체

블록은 이전에 분석관련 포팅에서 설명했듯이, 7가지의 종류로 구분한다.

우리는 빈 블록을 추가해서 8가지의 종류로 구분하는데, 이를 위해서 Eumeration을 상속해야한다.

1
object Block extends Enumeration
cs


Enumeration을 상속하면 열거형을 구현할 수 있다.

1
2
3
4
object Block extends Enumeration {
  type Block = Value
  val T, S, Z, O, I, L, J, EMPTY = Value
}
cs

위 처럼 7개의 종류를 구분하는 열거형을 구현한다.


우선 정사각형만을 표현하는 블록은 어떤 속성과 기능을 가져야 할까?

우리는 화면에 블록을 그릴 것이기 때문에, 하나의 정사각형의 크기, 종류별 색상와 종류별로 4개의 정사각형이 그려질 위치값이 필요하다.

그리고 랜덤한 종류의 블록을 계속해서 생성하기 위한 기능이 필요하다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  // Size about one side of square
  val BlockSize = 31
  
  /**
    * Create a random block that is not empty
    */
  def nextBlock: Block = Block.apply(Random.nextInt(Block.values.size - 1))
  
  /**
    * Return color of block
    */
  def getBlockColor(block: Block): Color = block match {
    case T     => Color.red
    case S     => Color.yellow
    case Z     => Color.green
    case O     => Color.blue
    case I     => Color.magenta
    case L     => Color.orange
    case J     => Color.lightGray
    case EMPTY => Color.black
  }
cs


블록의 크기는 속성으로 저장한다.

랜덤으로 다음 블록을 생성하는 nextBlock 메소드는 Block.EMPTY를 제외한 랜덤 블록을 생성하기 위해 Random과 Block.values를 이용한다.

인자로 전달한 Block의 색상을 구하기 위한 getBlockColor 메소드는 java.awt.Color 중 하나를 반환한다.


위의 코드들은 쉽기 때문에 특별히 더 설명할 필요는 없을 것 같다.


1
2
3
4
5
6
7
def getBlockImage(block: Block): BufferedImage = {
  val img = new BufferedImage(BlockSize, BlockSize, BufferedImage.TYPE_INT_ARGB)
  val graphics = img.createGraphics()
  graphics.setPaint(getBlockColor(block))
  graphics.fillRect(00, img.getWidth, img.getHeight)
  img
}
cs

블록을 패널에 그리기 위해 이미지를 생성해서 반환하는 함수이다.

BufferedImage(int width, int height, int imageType) 생성자에 블록의 사이즈와 8-bit RGBA color 타입을 인자로 넘겨서 이미지를 생성한다.

마지막으로 생성한 이미지에 블록의 색깔을 넓이와 높이만큼 채운 뒤, 이미지를 반환한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
  /**
    * Return positions correct by each type
    */
  def getPositions(block: Block): Array[Array[Tuple2[Int, Int]]] = block match {
    case T => Array(Array((00), (10), (-10), (0-1)),
      Array((00), (01), (0-1), (-10)),
      Array((00), (-10), (10), (01)),
      Array((00), (0-1), (01), (10)))
    case Z => Array(Array((00), (10), (0-1), (-1-1)),
      Array((00), (01), (10), (1-1)),
      Array((00), (10), (0-1), (-1-1)),
      Array((00), (01), (10), (1-1)))
    case S => Array(Array((00), (-10), (0-1), (1-1)),
      Array((00), (01), (-10), (-1-1)),
      Array((00), (-10), (0-1), (1-1)),
      Array((00), (01), (-10), (-1-1)))
    case O => Array(Array((00), (10), (0-1), (1-1)),
      Array((00), (10), (0-1), (1-1)),
      Array((00), (10), (0-1), (1-1)),
      Array((00), (10), (0-1), (1-1)))
    case I => Array(Array((00), (-10), (10), (20)),
      Array((00), (0-1), (01), (02)),
      Array((00), (-10), (10), (20)),
      Array((00), (0-1), (01), (02)))
    case J => Array(Array((00), (10), (-10), (-1-1)),
      Array((00), (0-1), (01), (-11)),
      Array((00), (-10), (10), (11)),
      Array((00), (01), (0-1), (1-1)))
    case L => Array(Array((00), (-10), (10), (1-1)),
      Array((00), (01), (0-1), (-1-1)),
      Array((00), (10), (-10), (-11)),
      Array((00), (0-1), (01), (11)))
    case EMPTY => Array()
  }
cs

마지막으로 내가 제일 이해하기 어려웠던 부분이다.

나중에 이해하고 나서는 별 거 아니라고 후회하긴 했지만....


이 메소드의 구현에 대한 자세한 내용은 02. [스칼라 테트리스] 자료 구조 분석 에서 설명했다.

  1. 기본 블록들은 4개의 정사각형의 좌표를 튜플로 관리하여 모양을 표시한다.
    1. T는 중앙(0,0), 중앙왼쪽(-1,0), 중앙오른쪽(1,0), 중앙아래(0,1)로 시작한다.

    2. S는 중앙(0,0), 중앙왼쪽(-1,0), 중앙위쪽(0,-1), 중앙오른쪽위(1,-1)로 시작한다.

    3. 블록은 4개의 회전된 모양을 가지며, 왼쪽 방향키를 클릭할 시 각각 아래와 같이 회전한다

      1. T = 중앙(0,0), 중앙왼쪽(-1,0), 중앙오른쪽(1,0), 중앙아래(0,1) 로 시작

      2. 회전 1번 = 중앙(0,0), 중앙아래(0,1), 중앙위(0,-1), 중앙왼쪽(-1,0) 로 회전

      3. 회전 2번 = 중앙(0,0), 중앙왼쪽(-1,0), 중앙오른쪽(1,0), 중앙위(0,-1)로 회전

      4. 회전 3번 = 중앙(0,0), 중앙위(0,-1), 중앙아래(0,1), 중앙오른쪽(1,0)로 회전

      5. 다시 1로 돌아간다.

  2. 블록이 표현할 수 있는 4가지의 모양을 2차원 배열로 관리한다.

    1. 1. 을 해석하면 하나의 블록은 4개의 (x,y) 좌표값으로 이루어진다

    2. 스칼라에서 (x,y)는 Tuple2라는 컬렉션에 저장할 수 있다.

    3. 그러므로, 하나의 블록은 1차원 배열에 저장된다. ( := Array(Tuple2, Tuple2, Tuple2, Tuple2)

    4. T라는 블록이 4개의 모양을 가지므로 각 모양은 2차원 배열에 저장된다. ( T := Array(Array(Tuple2, Tuple2, Tuple2, Tuple2))


위의 설명을 이해했다면, getPositions함수의 반환값이 Array[Array[Tuple2[Int, Int]]]인 이유를 이해할 수 있다.
기본적인 좌표는 (x,y)라는 튜플로 이루어진다. 이 좌표값은 화면의 어느 부분에 정사각형 하나가 그려지는지를 의미한다.
하나의 테트로미노는 4개의 정사각형으로 이루어져있으므로 4개의 좌표값을 가져야한다. (:= Array[Tuple2[Int, Int]])
하나의 테트로미노는 방향에 따라 4개의 모양을 갖는다. (:= Array[ Array[Tuple2[Int, Int] ])

그러므로, getPositions함수는 인자로 받은 블록의 종류에 따라 다른 2차원 배열을 반환한다.
이 2차원 배열에 (0), (1), (2), (3)와 같은 회전값을 전달하면 블록의 방향에 맞는 4개의 정사각형 좌표 Array[Tuple[Int, Int])를 반환한다.
이 4개의 좌표를 화면에 그리는데 이용한다.

Block 테스트

1
2
3
4
it should "have color" in {
        Block.getBlockColor(Block.T) should not equal (null)
        Block.getBlockColor(Block.T) shouldBe a [Color]
    }
cs
블록은 색상을 가져야 한다.

1. 반환값이 null이 아니여야 한다.

2. 반환값이 java.awt.Color의 인스턴스여야 한다.


1
2
3
4
5
6
"nextBlock" should "return a random element in Block.values that not Block.EMPTY" in {
        for(i <- 0 until 10) {
            Block.values should not be (Block.EMPTY)
            Block.values should contain (Block.nextBlock)
        }
    }
cs

nextBlock 메소드는 Block의 종류 중 EMPTY를 제외한 랜덤 요소를 반환한다.

1. 10번 반복
2. 반환값이 Block.EMPTY가 아니여야 한다.
3. 반환값이 Block.values의 종류 중 하나여야 한다.

1
2
3
4
"Block.EMPTY instance positions" should "be empty array" in {
        val emptyBlock = Block.EMPTY
        Block.getPositions(emptyBlock) shouldBe empty
    }
cs

Block.EMPTY의 positions은 빈 배열이다.

1. Block.EMPTY의 getPositions의 결과값은 empty여야 한다.