marco cognetta theoretically good with computers

Advent of Code 퍼즐들을 아희로 해결한다

영어 번역본을 여기서 읽을 수 있고 이 포스트를 트위터에서도 볼 수 있다.

이번 달 2021년의 Advent of Code (AoC) 코딩 챌린지가 시작되었다. AoC에서 출제되는 퍼즐들은 재미있고 쉽게 풀 수 있어서 해마다 수많은 사람들이 새로운 프로그래밍언어를 배우거나 이미 사용중인 프로그래밍 언어를 연습하기 위해 참가한다.

첫날의 퍼즐을 해결하다가 아희 언어로 어떻게 해결할 수 있을지 방법을 떠올리게 되었다. 또한 Manim 애니메이션 라이브러리로 나의 아희 코드의 실행에 대한 애니메이션들을 만들었다!

이 포스트에서는, 먼저 아희에 대해서 소개한 이후에, 나의 해결법에 대해서 설명해 보겠다.

아희는 한글로 쓰는 난해한 프로그래밍 언어이다. 아래와 같이, 아희 언어 프로그램들은 한글 낱자를 2차원 배열로 나열한 형태인데, 각 낱자는 컴퓨터 명령을 의미한다. 예시로, 이 프로그램은 “Hello, world!”를 출력한다:

밤밣따빠밣밟따뿌
빠맣파빨받밤뚜뭏
돋밬탕빠맣붏두붇
볻뫃박발뚷투뭏붖
뫃도뫃희멓뭏뭏붘
뫃봌토범더벌뿌뚜
뽑뽀멓멓더벓뻐뚠
뽀덩벐멓뻐덕더벅

전체적인 아희 언어에 대한 설명은 여기에서 살펴볼 수 있지만, 아희에 대해 간략하게 소개해본다.

첫번째로, 각 낱자마다, 초성 중성 그리고 종성은 다른 쓰임새로 사용된다. 초성은 명령, 중성은 움직임, 그리고 종성은 parameter를 의미한다. 초성은 add, pop, compare, store 등과 같은 함수이고, 중성은 어떻게 grid에서 움직일지 (예를 들면 중성이 이면 다음 셀은 바로 오른쪽에 있는 셀일 것임) 그리고 종성은 함수에 주어질 값이 무엇인지를 알려준다.

처음으로 최상단 제일 왼쪽 셀(예시에서는 )에서 프로그램이 시작하고 낱자의 초성이 인 셀에 이를 때까지, 명령에 따라 함수를 실행시키고, 다음 위치로 이동하는 행동을 반복하는데, 모든 iteration에서는 위에 설명한 것과 같이 각 낱말의 초성(명령), 종성(변수)에 따라 동작이 실행된다.

아희 언어에서는 stack과 queue들을 사용할 수 있는데, 현재 사용중인 자료구조에 저장된 값들과 각 셀의 종성 parameter를 이용해서 함수를 실행한다.

하지만 parameter의 수가 부족하면 다른 행동을 하게 된다. 예를 들어 명령이 add 였고, 이 함수를 사용하기 위해서는, 변수를 2개 주어야 하는데, parameter 값에 한 개만 주어졌다면, 셀의 명령을 실행하지 않고 중성의 방향 값이 뒤바뀌게 된다. 만약 중성이 였다면, 로 뒤바뀌어서 오른쪽이 아닌 왼쪽으로 움직인다.

아희의 설명서를 살펴보면 standard-in/out으로 읽고 쓰는 방법에 대한 내용이 나와 있지만, 파일을 어떻게 읽을 수 있는지에 대해서는 언급하지 않고있다. AoC 퍼즐을 해결하기 위해서, 파일에서 input을 읽어와야 하므로 input을 읽을 수 있도록 아희 interpreter 코드를 수정해 주었다. 이후, 프로그램에서 input 파일의 end-of-file까지 모두 다 읽은 후 프로그램이 다시 읽도록 시도할 때 위에서 설명했던 "parameter가 모자란 경우"처럼 코드의 진행 방향이 뒤바뀐다.

AoC 첫날의 퍼즐은 다음과 같은 질문이었다: “수의 목록에서 숫자 바로 뒤에 더 큰 숫자가 오는 경우가 몇 번 발생되었니?”

예를 들면 숫자 목록이 1, 4, 2, 5, 7이면 1->4, 2->5, 5->6과 같이 세 차례 등장하므로 정답은 3이다.

나는 아희를 이용해서 아래와 같이 해결했다:

삼바상뱡숨방파빠파주
마르코하멍송더섬썸퍼

이 프로그램에서는 여러가지 명령어를 사용했다. 아래는 사용된 아희 명령어들에 대한 설명이다 (초성 지수에 따라):

  • 더 -> add

    • 현재 자료구조의 1번째 값을 pop해서 더한 후에 넣음

    • 종성은 영향이 없음

  • ㅁ -> print

    • 현재 자료구조의 1번째 값을 pop해서 출력함

    • 종성은 이면 숫자로 출력하지만 “ㅎ”이면 unicode로 출력함

  • ㅂ -> push

    • 종성이 /이면 파일을 읽어서 현재의 자료구조에 넣음

    • 종성이 없으면 0을 넣음

    • 다른 종성일 경우, 종성의 획 수에 해당하는 숫자를 자료구조에 넣음

      • ㄱ -> 2, ㅃ -> 8, 등

      • 1을 만들 종성이 없음

  • ㅃ -> duplicate

    • 현재 자료구조의 첫번째 값을 복사해서 해당 자료구조에 넣음

    • 종성은 영향이 없음

  • ㅅ -> 자료구조 선택함

    • 종성은 무슨 자료구조로 변화함

      • ㅁ -> queue

      • ㅁ 아닌 종성 -> stack

      • 종성에 따라 특정한 stack자료구조를 선택함

  • ㅆ -> transfer

    • 현재 자료구조의 1번째 값을 다른 자료구조로 보냄

    • 종성은 보내는 자료구조의 정보를 알려줌

  • ㅈ -> compare

    • 현재 자료구조의 1, 2번째 값들을 비교하고 1번째 값이 더 크면 1을 넣고 아니면 0을 넣음

    • 종성은 영향이 없음

  • ㅍ -> swap

    • 현재 자료구조의 1, 2번째 값들을 바꿈

    • 종성은 영향이 없음

  • ㅎ -> terminate

    • 프로그램이 종료됨

삼바상뱡숨방파빠파주
마르코하멍송더섬썸퍼

나의 프로그램은 4부분으로 나누어져 있다. 첫 번째는 삼바상뱡이다. 이 부분에서는, 프로그램을 초기화하기 위해 파일의 첫 숫자를 읽어서 queue에 넣는다. Stack-counter 값을 초기화하기 위해, 0을 stack에 미리 넣어준다. 이 후에 숫자를 읽을 때마다 각 숫자들을 비교한 결과에 따라 0 이나 1을 그 stack에 넣고 stack에 저장된 숫자들을 add 명령을 이용해서 더한다. 이 부분의 마지막 낱자 뱡은 중성을 갖고 있다. 이 중성은 셀을 2번 오른쪽으로 움직이라는 의미가 있어서 실행한 후 이 아닌 셀에 도달할 것이고 여기서부터 프로그램의 두 번째 부분이 시작된다.

두 번째 부분은 아래와 같다:

방파빠파주
송더섬썸퍼

이부분에서는, 반복해서 숫자를 읽고 그 전 숫자과 비교하고 stack counter를 갱신한다. 중성들은 cycle을 (방 -> 파 -> 빠 -> 파 -> 주 -> 퍼 -> 썸 -> 섬 -> 더 -> 송 -> 방 -> ...) 만든다. 그래서 숫자가 모두 읽혀져서 없어질 때까지 이 cycle의 명령들이 반복해서 실행된다.

더이상 읽을 숫자가 없어질 때 명령이 실행되지 못하고 방향이 뒤바껴서 바로 다음 셀은 이 되고 그 다음 셀은 이 되고 마지막으로 에서 프로그램이 종료된다. 즉, 이 숨/방/하 낱자들은 이 프로그램의 세번째 부분이다.

마지막 네 번째 부분은 마르코이다. 이 부분은 프로그램이 절대 도달할 수가 없는 부분이고, 프로그램을 직사각형 모양으로 유지하기 위해 나의 이름을 넣었다.

이 두 애니메이션으로 위에서 설명한 아희 프로그램이 어떻게 실행하고 종료되는지 알 수 있다.

실행함

종료됨


첫번째 날의 두 번째 퍼즐도 아희를 이용해서 해결했다. 각 숫자를 비교하는 대신, 이어진 세 숫자를 더한 합을 비교하기 때문에 첫 번째 퍼즐과 조금 다르다. Python을 이용하면 이렇게 해결할 수 있다:

if __name__ == '__main__':
    count = 0
    f = open('input.txt', 'r')
    x, y, z = int(f.readline()), int(f.readline()), int(f.readline())

    for line in f:
        a = int(line)
        if a > x: count += 1
        x, y, z = y, z, a
    print(count)

아래와 같이 아희로 해결했다:

삼바상방방방샨숨방빠쌍상싼산반분
마르코코그넷허멍손더섬썸저어더너

이 프로그램도 4부분으로 나누어져 있다. 처음으로, 삼바상방방방샨 부분에서는, 프로그램을 초기화 하기 위해 input에서 첫 3개의 숫자들을 읽어서 queue에 넣고 stack을 초기화 한다.

방빠쌍상싼산반분
손더섬썸저어더너

위에 있는 코드는 반복해서 다음 숫자를 읽고, 그 숫자와 3회 전에 등장한 숫자를 비교한다는 뜻이다.

비교를 의미하는 명령은 > 이 아닌 를 의미해서, 최신 숫자와 3회 전에 등장한 숫자를 비교하기 위해서는 최신 숫자에 1을 더해야 한다.

아희에서 (push) 명령어를 이용할 때, 1을 parameter로 줄 수 있는 방법이 없기 때문에 (자음 중에 한 획으로 이루어진 자음이 없고, 은 input 을 읽는데 쓰이기 때문에), 1을 만들기 위해 반/분/너 명령어들을 이용한다. 반/분은 숫자 2를 스택에, 총 2회 넣는 동작을 하고, (divide) 명령어를 이용해, 스택에서 pop을 2회 하고, 리턴된 숫자 두 개를 서로 나누는 방식으로 1을 생성한다.

다음 부분은:

....숨...
..허멍..

이고 프로그램의 결과를 출력한 후 프로그램을 종료시킨다.

마지막 부분인 마르코코그넷허는, 다시 나의 이름이다. 보통 내 이름을 한글로 쓸 때, 마르코 코그넷*터*로 쓰지만 이 프로그램의 모양을 유지하기 위해, 그리고 넷터넷허의 발음이 비슷하기 때문에 나의 이름을 조금 변형시켜서 코드에 적용했다.

이 애니메이션으로 나의 프로그램이 어떻게 실행하는지 알 수 있다.