U E D R , A S I H C RSS

Spiral Array/Leonardong

피라미드 오르기

2h 20m 28s
아이디어는 JuNe 선배가 말했던 것이다.(저번 자바 컨퍼런스에서 였던가..) 한 번 나선형으로 진행되는 것을 같은 층으로 본다. 그러면 가장 바깥쪽은 1층, 다음 안쪽은 2층 이런 식으로 안쪽으로 갈수록 높이가 높아진다. 한 사람이 피라미드를 한 바퀴 돌고 다음 층으로 올라가면서 자신이 들렀던 곳이 몇 번째인지, 좌표는 무엇인지 기억한다. 한 층을 다 돌면 시작했던 자리로 돌아오기 때문에 중복해서 기억한 좌표는 지우고 다음 층으로 이동한다.

하지만 여지껏 그러한 접근법을 알고서도 TDD로 풀지를 못했었다. 매번 나선형 "행렬"에 어떻게 숫자를 새길지만 생각했기 때문이다. 그러다 보니 2차원 배열의 인덱스를 조작하는 수준에서 생각이 벗어나질 못했다. 하지만 사실은 움직임(이전의 인덱스 조작), 움직인 점들, 행렬을 따로 생각할 수 있었다. 아! 이렇게 테스트 하면 되겠구나!

TDD로 풀었다는 점이 기쁘다. 처음부터 너무 메서드를 어디에 속하게 할 지 고민하지 않고 시작한 것이 유용했다. 그 결과로 예전 같으면 생각하지 못했을 Direction클래스와 그 하위 클래스가 탄생했다. 또한 행렬은 최종 결과물을 저장하고 보여주는 일종의 뷰처럼 쓰였다.

현재는 행렬 구성이 비효율적이다. 움직였던 기록을 가지고 행렬을 구성하기를 반복한다.이것을 수정할 때 좀더 효율적으로 작동하게 만들어야겠다. Mover클래스, Array클래스의 종료검사, 테스트 케이스는 확실히 Refactoring이 필요하다.


~cpp
import unittest

ROW = 0
COL = 1

class Direction:
    def move(self, coordinate, board):
        if ( board.isWall( self.next(coordinate) ) ):    
            return coordinate
        return self.next(coordinate)

class Down(Direction):
    def next(self, coordinate):
        return (coordinate[ROW] + 1, coordinate[COL])
    
class Up( Direction ):
    def next(self, coordinate):
        return (coordinate[ROW] - 1, coordinate[COL])

class Right(Direction):
    def next(self, coordinate):
        return (coordinate[ROW], coordinate[COL] + 1 )

class Left(Direction):
    def next(self, coordinate):
        return (coordinate[ROW], coordinate[COL] - 1 )


class Board:
    def __init__(self):
        self.size = 0
    def setBoundary( self, start, end ):
        self.start = start
        self.end = end
    def isWall( self, coordinate ):
        if ( coordinate[ROW] < self.start ):
            return True
        elif ( coordinate[ROW] >= self.end ):
            return True
        elif ( coordinate[COL] < self.start ):
            return True
        elif ( coordinate[COL] >= self.end ):
            return True
        return False
    def decreaseBoundary(self, amount):
        self.setBoundary( self.start+amount, self.end-amount )
    

class Mover:
    def __init__(self):
        self.coordinate = (0,0)
        self.moveCount = 0
        self.history = []
    def position(self):
        return self.coordinate
    def goStraight(self, direction, board):
        while ( not self.coordinate == direction.move(self.coordinate, board) ):
            self.history.append( Point( self.coordinate, self.moveCount+1 ) )
            self.moveCount += 1
            self.coordinate = direction.move( self.coordinate, board )
        self.history.append( Point( self.coordinate, self.moveCount+1 ) )
    def mark(self, point, value):
        point.value = value
    def _setMoveCount(self, count):
        self.moveCount = count
    def _setPosition(self, coordinate):
        self.coordinate = coordinate
    def getHistory(self):
        return self.history
    def turnRound(self, board):
        self.goStraight( Right(), board )
        self.goStraight( Down(), board )
        self.goStraight( Left(), board )
        self.goStraight( Up(), board )
        board.decreaseBoundary(1)
        self.eraseLastMovement()
        self._setPosition( self.nextFloor() )
    def nextFloor(self):
        return (self.coordinate[ROW]+1, self.coordinate[COL]+1 )
    def eraseLastMovement(self):
        self._setMoveCount( self.history.pop().value - 1 )
        
class Point:
    def __init__(self, coordinate, value):
        self.coordinate = coordinate
        self.value = value

class Array:
    def __init__(self, size):
        self.matrix = []
        for i in range(size):
            self.matrix.append( [Point((-1,-1),-1)] * size )
    def isAllFilled(self):
        for row in self.matrix:
            for point in row:
                if ( point.value < 0 ):
                    return False
        return True
    def _fillAll(self):
        for row in self.matrix:
            for point in row:
                point.value = 1
    def get(self, row, col):
        return self.matrix[row][col]
    def construct(self, pointList):
        for p in pointList:
            self.matrix[p.coordinate[ROW]][p.coordinate[COL]] = p
    def printValues(self):
        for row in self.matrix:
            for point in row:
                print point.value,
                print "\t",
            print

class SpiralArrayTest(unittest.TestCase):
    def setUp(self):
        self.board = Board()
        self.size = 10
        self.board.setBoundary(start=0, end=self.size)
        self.mover = Mover()
        self.array = Array( self.size )
    def testMoveDown(self):
        self.assertEquals( (1,0), Down().move( (0,0), self.board ) )
        self.assertEquals( (self.size-1,0), Down().move( (self.size-1,0), self.board ) )
    def testMoveUp(self):
        self.assertEquals( (0,0), Up().move( (0,0), self.board ) )
        self.assertEquals( (self.size-1,0), Up().move( (self.size,0), self.board ) )
    def testIsWall(self):
        self.assertEquals( False, self.board.isWall( (0,0) ) )
        self.assertEquals( True, self.board.isWall( (-1,0) ) )
        self.assertEquals( True, self.board.isWall( (0,-1) ) )
        self.assertEquals( True, self.board.isWall( (0,self.size) ) )
        self.assertEquals( True, self.board.isWall( (self.size,0) ) )
    def testGoStraightRight(self):
        self.mover.goStraight(Right(), self.board)
        self.assertEquals( (0,self.size-1), self.mover.position() )        
    def testMark(self):
        point = Point((0,0),0)
        self.mover._setMoveCount(1)
        self.mover.mark( point, self.mover.moveCount )
        self.assertEquals( 1, point.value )
    def testStorePointCoordinate(self):
        self.mover.goStraight( Down(), self.board )
        points = self.mover.getHistory()
        self.assertEquals( (self.size-1,0), points.pop().coordinate )
        self.assertEquals( (self.size-2,0), points.pop().coordinate )
    def testStoreMoveCount(self):
        self.mover._setPosition((self.size-1, self.size-1))
        self.mover.goStraight( Left(), self.board )
        points = self.mover.getHistory()
        self.assertEquals( self.size, points.pop().value )
        self.assertEquals( self.size-1, points.pop().value )
    def testTurnRound(self):
        self.mover._setPosition( (0,0) )
        self.mover.turnRound(self.board)
        self.assertEquals( (1,0), self.mover.getHistory().pop().coordinate )
    def testIsFinished(self):
        self.assertEquals( False, self.array.isAllFilled() )
        self.array._fillAll()
        self.assertEquals( True, self.array.isAllFilled() )        
    def testConstructArray(self):
        self.mover._setPosition( (0,0) )
        self.mover.turnRound(self.board)
        self.array.construct( self.mover.getHistory() )
        self.assertEquals( (0,0), self.array.get(0,0).coordinate )
        self.assertEquals( (self.size-1, self.size-1),
                           self.array.get(self.size-1, self.size-1).coordinate )
if __name__ == "__main__":
##    unittest.main()
    inputSize = input()
    board = Board()
    board.setBoundary(0, inputSize )
    array = Array(inputSize)
    mover = Mover()
    mover._setPosition((0,0))
    while not ( array.isAllFilled() ):
        mover.turnRound(board)
        array.construct(mover.getHistory())
    array.printValues()

피라미드 오르기 Refacotring

1h 58m 26s

지난 번 리팩토링 대상이었던 Mover클래스, Array클래스의 종료검사, 테스트 케이스를 리팩토링 했다. 테스트 케이스와 Array클래스는 쉽게 리팩토링 할 수 있었다. 하지만 Mover클래스를 손대는데 오래 걸렸다.

goStraight 전 버전은 Direction클래스를 이용해서 이동한(벽을 만나면 이동하지 않음) 위치를 얻어내고, 이동한 기록을 저장하는 형식이었다. 벽에 대한 검사가 겹치는 것 같아 mover에서 바로 벽을 검사하고 벽에 들어서면 종료하는 것으로 만들었다. 그러고 보니 따로 카운트 할 필요가 없어 moveCount변수를 없앴다. mover가 종료 조건도 검사하는데 board 넓이만큼 이동했으면 끝나는 것이기 때문이다.

그런데 벽에 들어서야 종료하다 보니까 mover를 벽에 들어가기 전에 위치로 되돌려놓아야 했다. 그래서 direction에 모두 previous 메서드가 생겼다. 한데 다음 번 goStraight를 할 때는 이미 이동했던 기록이 남아있게 되었다. 그래서 매번 goStraight를 할 때마다 마지막 이동 기록을 삭제했다. 그러다보니 board크기가 1일 경우는 이동한 기록이 모두 지워져버리는 것이 아닌가. 조잡하지만 예외 처리를 해주었다.

경계조건이 참 미묘하다는 것을 느꼈다. 시작과 끝을 어떻게 할 것인가? 한참을 헤매다보니 더 나은 방법이 있는데도 찾질 못하는 것 같다.

코드 안에서 헤매기 보다는 정확히 생각을 정리해서 구현해야 한다. 이것 해보고 저것 해보는 사이에 시간이 너무 많이 흘러갔다. 결국에 답은 나왔지만, 이보다 빨리 할 수 있을 것이다.


~cpp

import unittest

ROW = 0
COL = 1

class Direction:
    pass
class Down(Direction):
    def next(self, coordinate):
        return (coordinate[ROW] + 1, coordinate[COL])
    def previous(self, coordinate):
        return Up().next( coordinate )
    
class Up( Direction ):
    def next(self, coordinate):
        return (coordinate[ROW] - 1, coordinate[COL])
    def previous(self, coordinate):
        return Down().next( coordinate )

class Right(Direction):
    def next(self, coordinate):
        return (coordinate[ROW], coordinate[COL] + 1 )
    def previous(self, coordinate):
        return Left().next( coordinate )

class Left(Direction):
    def next(self, coordinate):
        return (coordinate[ROW], coordinate[COL] - 1 )
    def previous(self, coordinate):
        return Right().next( coordinate )


class Board:
    def __init__(self, initialSize):
        self.size = initialSize
        self.setBoundary( 0, self.size )
    def setBoundary( self, start, end ):
        self.start = start
        self.end = end
    def isWall( self, coordinate ):
        if ( coordinate[ROW] < self.start ):
            return True
        elif ( coordinate[ROW] >= self.end ):
            return True
        elif ( coordinate[COL] < self.start ):
            return True
        elif ( coordinate[COL] >= self.end ):
            return True
        return False
    def decreaseBoundary(self, amount):
        self.setBoundary( self.start+amount, self.end-amount )
    

class Mover:
    def __init__(self):
        self.coordinate = (0,0)
        self.history = []
    def position(self):
        return self.coordinate
    def goStraight(self, direction, board):
        while ( not board.isWall( self.coordinate ) ):
            self.history.append( Point( self.coordinate, self.moveCount() ) )
            self.coordinate = direction.next( self.coordinate )
        self.coordinate = direction.previous( self.coordinate )
    def mark(self, point, value):
        point.value = value
    def moveCount(self):
        return len( self.history ) + 1
    def setPosition(self, coordinate):
        self.coordinate = coordinate
    def getHistory(self):
        return self.history
    def turnRound(self, board):
        for direction in [Right(), Down(), Left()]:
            self.goStraight( direction, board )
            self.eraseLastMovement()
        self.goStraight( Up(), board )
        if ( board.end - board.start != 1 ):
            self.eraseLastMovement()
        self.setPosition( self.nextFloor() )
    def nextFloor(self):
        return (self.coordinate[ROW]+1, self.coordinate[COL]+1 )
    def eraseLastMovement(self):
        self.history.pop()
    def isFinished( self, board ):
        return board.size * board.size < self.moveCount()
    def lastHistory( self ):
        return self.history[-1]
            
        
class Point:
    def __init__(self, coordinate, value):
        self.coordinate = coordinate
        self.value = value

class Array:
    def __init__(self, size):
        self.matrix = []
        for i in range(size):
            self.matrix.append( [Point((-1,-1),-1)] * size )
    def get(self, row, col):
        return self.matrix[row][col]
    def construct(self, pointList):
        for p in pointList:
            self.matrix[p.coordinate[ROW]][p.coordinate[COL]] = p
    def printValues(self):
        for row in self.matrix:
            for point in row:
                print point.value,
                print "\t",
            print

class SpiralArrayTest(unittest.TestCase):
    def setUp(self):
        self.size = 10
        self.board = Board(self.size)
        self.board.setBoundary(start=0, end=self.size)
        self.mover = Mover()
        self.array = Array( self.size )


class DirectionTest(SpiralArrayTest):
    def testNext(self):
        self.assertEquals( (1,0), Down().next( (0,0) ) )

    def testPrevious(self):
        self.assertEquals( (0,0), Down().previous( (1,0) ) )

class BoardTest(SpiralArrayTest):
    def testIsWall(self):
        self.assertEquals( False, self.board.isWall( (0,0) ) )
        self.assertEquals( True, self.board.isWall( (-1,0) ) )
        self.assertEquals( True, self.board.isWall( (0,-1) ) )
        self.assertEquals( True, self.board.isWall( (0,self.size) ) )
        self.assertEquals( True, self.board.isWall( (self.size,0) ) )

class MoverTest(SpiralArrayTest):
    def testGoStraightRight(self):
        self.mover.goStraight(Right(), self.board)
        self.assertEquals( (0,self.size-1), self.mover.position() )

    def testStorePointCoordinate(self):
        self.mover.goStraight( Down(), self.board )
        points = self.mover.getHistory()
        self.assertEquals( (self.size-1,0), points.pop().coordinate )
        self.assertEquals( (self.size-2,0), points.pop().coordinate )
    def testStoreMoveCount(self):
        self.mover.setPosition((self.size-1, self.size-1))
        self.mover.goStraight( Left(), self.board )
        points = self.mover.getHistory()
        self.assertEquals( self.size, points.pop().value )
        self.assertEquals( self.size-1, points.pop().value )
    def testStoreMovementWhenTurnRoundAndBoudaryNotOne(self):
        self.mover.setPosition( (0,0) )
        self.mover.turnRound(self.board)
        points = self.mover.getHistory()
        self.assertEquals( (self.size-1) * 4 , len( points ) )
    def testStoreMovementWhenTurnRoundAndBoudaryOne(self):
        self.board = Board( initialSize = 1 )
        self.mover.setPosition( (0,0) )
        self.mover.turnRound(self.board)
        points = self.mover.getHistory()
        self.assertEquals( 1 , len( points ) )
    def testTurnRoundWithBoundary0(self):
        self.mover.setPosition( (0,0) )
        self.mover.turnRound(self.board)
        self.assertEquals( (1,0), self.mover.lastHistory().coordinate )
    def testIsFinished(self):
        self.board = Board( initialSize = 1 )
        self.assertEquals( False, self.mover.isFinished( self.board ) )
        self.mover.turnRound( self.board )
        self.assertEquals( True, self.mover.isFinished( self.board ) )
    def testRun(self):
        self.board = Board( initialSize = 3 )
        self.mover.setPosition( (0,0) )
        self.mover.turnRound(self.board)
        self.assertEquals( (1,0), self.mover.lastHistory().coordinate )
        self.assertEquals( (1,1), self.mover.position() )

        self.board.decreaseBoundary(1)
        self.mover.turnRound(self.board)
        self.assertEquals( (1,1), self.mover.lastHistory().coordinate )

class ArrayTest(SpiralArrayTest):    
    def testConstructArray(self):
        self.mover.setPosition( (0,0) )
        self.mover.turnRound(self.board)
        self.array.construct( self.mover.getHistory() )
        self.assertEquals( (0,0), self.array.get(0,0).coordinate )
        self.assertEquals( (self.size-1, self.size-1),
                           self.array.get(self.size-1, self.size-1).coordinate )
        

    
if __name__ == "__main__":
    unittest.main()
    inputSize = input()
    board = Board(inputSize)
    mover = Mover()
    mover.setPosition((0,0))
    while ( not mover.isFinished( board ) ):
        mover.turnRound(board)
        board.decreaseBoundary(1)
    array = Array(inputSize)
    array.construct(mover.getHistory())
    array.printValues()

Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2009-05-27 07:09:19
Processing time 0.0996 sec