프로그래밍의 흑마법, 코드골프 등에 관심이 있는 사람들이라면 한번쯤은 IOCCC에 대해 들어봤을 것이다. 아니면 IOCCC을 들어본 적은 없더라도 ASCII fluid dynamics 라는 영상은 봤을 지도 모른다.
그리고 이 프로그램의 소스 코드는 다음과 같다.
# include<stdio.h>// .IOCCC Fluid- #
# include <unistd.h> //2012 _Sim!_ #
# include<complex.h> //|||| ,____. IOCCC- #
# define h for( x=011; 2012/* #
# */-1>x ++;)b[ x]//-' winner #
# define f(p,e) for(/* #
# */p=a; e,p<r; p+=5)// #
# define z(e,i) f(p,p/* #
## */[i]=e)f(q,w=cabs (d=*p- *q)/2- 1)if(0 <(x=1- w))p[i]+=w*/// ##
double complex a [ 97687] ,*p,*q ,*r=a, w=0,d; int x,y;char b/* ##
## */[6856]="\x1b[2J" "\x1b" "[1;1H ", *o= b, *t; int main (){/** ##
## */for( ;0<(x= getc ( stdin) );)w=x >10?32< x?4[/* ##
## */*r++ =w,r]= w+1,*r =r[5]= x==35, r+=9:0 ,w-I/* ##
## */:(x= w+2);; for(;; puts(o ),o=b+ 4){z(p [1]*/* ##
## */9,2) w;z(G, 3)(d*( 3-p[2] -q[2]) *P+p[4 ]*V-/* ##
## */q[4] *V)/p[ 2];h=0 ;f(p,( t=b+10 +(x=*p *I)+/* ##
## */80*( y=*p/2 ),*p+=p [4]+=p [3]/10 *!p[1]) )x=0/* ##
## */ <=x &&x<79 &&0<=y&&y<23?1[1 [*t|=8 ,t]|=4,t+=80]=1/* ##
## */, *t |=2:0; h=" '`-.|//,\\" "|\\_" "\\/\x23\n"[x/** ##
## */%80- 9?x[b] :16];;usleep( 12321) ;}return 0;}/* ##
#### ####
###############################################################################
**###########################################################################*/
소스와 문제에 대한 설명은 ioccc 페이지 에서 찾을 수 있다. SPH 알고리즘을 사용하여 만든 액체 시뮬레이션이지만, 저 짧고 괴랄한 코드로 어떻게 이게 가능한지 궁금하여 코드를 나름 분석해보았고, 이게 굉장히 재밌어서 공유해본다.
복호화
일단 소스 코드에서 주석을 지우고 인덴트만 맞춰보자. 그러면 다음과 같이 된다.
# include <stdio.h>
# include <unistd.h>
# include <complex.h>
# define h for(x=011;2012-1>x++;)b[x]
# define f(p,e) for(p=a;e,p<r;p+=5)
# define z(e,i) f(p,p[i]=e)f(q,w=cabs(d=*p-*q)/2-1)if(0<(x=1-w))p[i]+=w*
double complex a [97687],*p,*q,*r=a,w=0,d;
int x,y;char b[6856]="\x1b[2J" "\x1b" "[1;1H ",*o=b,*t;
int main(){
for(;0<(x=getc(stdin));)
w=x>10?32<x?4[*r++=w,r]=w+1,*r=r[5]=x==35,r+=9:0,w-I:(x=w+2);;
for(;;puts(o),o=b+4) {
z(p[1]*9,2)w;
z(G,3)(d*(3-p[2]-q[2])*P+p[4]*V-q[4]*V)/p[2];
h=0;
f(p,(t=b+10+(x=*p*I)+80*(y=*p/2),*p+=p[4]+=p[3]/10*!p[1]))
x=0<=x&&x<79&&0<=y&&y<23?1[1[*t|=8,t]|=4,t+=80]=1, *t|=2:0;
h=" '`-.|//,\\" "|\\_" "\\/\x23\n"[x%80-9?x[b]:16];
usleep(12321);
}
return 0;
}
여기서 매크로까지 전부 치환시켜보자.
# include <stdio.h>
# include <unistd.h>
# include <complex.h>
double complex a [97687],*p,*q,*r=a,w=0,d;
int x,y;char b[6856]="\x1b[2J" "\x1b" "[1;1H ",*o=b,*t;
int main() {
for(;0<(x=getc(stdin));)
w=x>10?32<x?4[*r++=w,r]=w+1,*r=r[5]=x==35,r+=9:0,w-I:(x=w+2);;
for(;;puts(o),o=b+4) {
for(p=a;p[2]=p[1]*9,p<r;p+=5)
for(q=a;w=cabs(d=*p-*q)/2-1,q<r;q+=5)
if(0<(x=1-w))p[2]+=w*w;
for(p=a;p[3]=G,p<r;p+=5)
for(q=a;w=cabs(d=*p-*q)/2-1,q<r;q+=5)
if(0<(x=1-w))p[3]+=w*(d*(3-p[2]-q[2])*P+p[4]*V-q[4]*V)/p[2];
for(x=011;2012-1>x++;)
b[x]=0;
for(p=a;(t=b+10+(x=*p*I)+80*(y=*p/2),*p+=p[4]+=p[3]/10*!p[1]),p<r;p+=5)
x=0<=x&&x<79&&0<=y&&y<23?1[1[*t|=8,t]|=4,t+=80]=1, *t|=2:0;
for(x=011;2012-1>x++;)
b[x]=" '`-.|//,\\" "|\\_" "\\/\x23\n"[x%80-9?x[b]:16];
usleep(12321);
}
return 0;
}
코드에서 9번째 라인의 정체불명의 w=x>10? ...
부분을 분석해보자. 삼항연산자와 콤마연산자로 복잡하게 연결되있는 코드이다. 일단 삼항 연산자부터 분리해보자. 중요한건 콤마연산자의 우선순위이다.
if (x > 10)
w = 32<x?4[*r++=w,r]=w+1,*r=r[5]=x==35,r+=9:0,w-I;
else
w = x = w + 2;
한단계 더 분리할 수 있다.
if (x > 10) {
if (32 < x)
w = 4[*r++=w,r]=w+1,*r=r[5]=x==35,r+=9,w-I;
else
w = 0, w-I
}
else
w = x = w + 2;
여기서 콤마 연산자도 늘어놓을 수 있다.
if (x > 10) {
if (32 < x)
4[*r++=w,r]=w+1;
*r=r[5]=x==35;
r+=9;
w = w-I;
else
w = 0, w-I
}
else
w = x = w + 2;
충분히 읽을 수 있는 코드가 되었다. 4[*r++=w,r]=w+1
는 다시 다음의 두 문장으로 분리 가능하다.
*r++=w;
4[r]=w+1;
4[r]
은 r[4]
와 같으므로 (C언어의 배열 신태틱 슈거 참고) 바꿔 쓸 수 있다.
그리고 x
는 stdin
으로부터 한글자씩 가져오는 변수이므로 10보다 작은 문자중 출력가능한 '\n'
만이 첫번째 분기문에서 else문으로 빠질 수 있다. 그렇다면 코드는 다음과 같이 조금 더 보기 좋게 바꿀 수 있다.
while ((x=getc(stdin)) != EOF) {
if (x != '\n') {
if (x > 32) {
*(r++) = w;
r[4] = w + 1;
if (x == '#')
r[0] = r[5] = 1;
else
r[0] = r[5] = 0;
r += 9;
}
w = w - I;
}
else {
w = x = w + 2;
}
}
흥미로운 코드는 첫 분기문에서 else문으로 빠진 부분의 코드이다. w = x = w + 2
는 그냥 w += 2
코드로 치환할 수 있어 보이지만, 그렇지 않다. 변수 w
의 타입은 double complex
이고 x
의 타입은 int
이다. 즉 x = w + 2
의 값은 w + 2
의 실수부가 되며, 그래서 w
에는 w
의 실수부에 2를 더한 값이 들어가게 된다. 즉 w = x = w + 2
는 w = 2 + creal(2)
로 치환가능하다.
하지만 여전히 이해하기는 쉽지 않은 코드이다. stdin
으로부터 입력을 받아와 배열 a
를 초기화하는 듯 보인다. 내부에서 x
가 '#'
임을 비교하는 코드는 아마 사용되는 맵에서 '벽' 임을 확인하는 코드일테다. 그리고 복소수인 w
는 개행문자를 만나면 (w + 2, 0i)
의 복소수로 이동하고 그렇지 않으면 i
를 하나씩 감소함으로 보아 현재 좌표를 복소수로 저장하는 변수임을 알 수 있다. (x, y)
는 y - xi
꼴로 저장이 된다. 이렇게 이해가 갔다면 좀더 readable 하게 코드를 바꿔보자.
while ((x=getc(stdin)) != EOF) {
if (x == '\n') {
// w 에 좌표 (0, y + 2) 넣어서 개행
w = creal(w + 2);
continue;
}
if (x > ' ') {
r[0] = w;
r[5] = w + 1;
if (x == '#')
r[1] = r[6] = 1;
else
r[1] = r[6] = 0;
r += 10;
}
w -= I; // w의 x좌표 증가
}
인덱스 변수처럼 사용되는 포인터 변수 r
을 사용한 배열 a
의 초기화는 조금 있다 살펴보자. 이제 for(;;puts(o),o=b+4) {
으로 시작하는 실제 알고리즘 코드를 살펴볼 차례다.
첫번째 큰 루프는 일단 스킵하고 두개의 중첩된 반복문을 살펴보자.
for(p=a;p[2]=p[1]*9,p<r;p+=5)
for(q=a;w=cabs(d=*p-*q)/2-1,q<r;q+=5)
if(0<(x=1-w))p[2]+=w*w;
for(p=a;p[3]=G,p<r;p+=5)
for(q=a;w=cabs(d=*p-*q)/2-1,q<r;q+=5)
if(0<(x=1-w))p[3]+=w*(d*(3-p[2]-q[2])*P+p[4]*V-q[4]*V)/p[2];
비슷한 방법으로 난독화된 이런 코드는 콤마 오퍼레이터만 늘어놓기만 해도 코드가 훨씬 이해하기 쉬워진다.
for (p=a; p<r; p+=5) {
p[2] = p[1] * 9;
for (q=a; q<r; q+=5) {
w = cabs(d=p[0]-q[0])/2-1;
if(0<(x=1-w))
p[2] += w * w;
}
}
for(p=a; p<r; p+=5) {
p[3] = G;
for(q=a;q<r;q+=5) {
w = cabs(d=p[0]-q[0])/2-1;
if(0<(x=1-w))
p[3]+=w*(d*(3-p[2]-q[2])*P+p[4]*V-q[4]*V)/p[2];
}
}
두 반복문에서 똑같이 보이는 w
를 사용한 if(0<(x=1-w))
코드는 아까 말했던 것처럼 정수 변수를 사용하여 복소수의 실수부를 가져오는 코드이다. 그래서 일반 double 로 다음과 같이 바꿔줘도 문제 없다. 그리고 귀찮은 선언문도 앞으로 빼주자.
for (p=a; p<r; p+=5) {
p[2] = p[1] * 9;
for (q=a; q<r; q+=5) {
double i = cabs(p[0]-q[0])/2-1;
if(0<(int)(1-i))
p[2] += i * i;
}
}
for(p=a; p<r; p+=5) {
p[3] = G;
for(q=a;q<r;q+=5) {
d=p[0]-q[0];
double i = cabs(d)/2-1;
if(0<(int)(1-i))
p[3]+=i*(d*(3-p[2]-q[2])*P+p[4]*V-q[4]*V)/p[2];
}
}
여기서 알 수 있는 중요한 것은 배열 a
에서 5씩 증가시켜서 값을 계산한다는 것이다. 초기화 코드를 보면 10을 주기로 초기화를 하지만 r[1] = r[6] = 1
같은 코드는 다음 인덱스에 대해서 초기화를 동시에 해주는 것으로 보인다. 배열 a
는 여기서 사용된 알고리즘의 핵심인 SPH 파티클들의 배열이란것을 눈칠챌 수 있다.
다시 초기화 코드로 넘어가서 다음 코드만 보면 된다.
r[0] = w;
r[5] = w + 1;
if (x == '#')
r[1] = r[6] = 1;
5개의 복소수 값을 가지는 구조체 정도로 생각을 해주면, 0번째 복소수는 좌표를 담고 1번째 복소수는 현재 좌표가 벽인지 확인하는 0혹은 1의 플래그 값임을 알 수 있다. 그리고 위 두 반복문을 보면 p[2]
는 처음 벽의 유무에 따라 0
혹은 9
가 되고 그 다음 모든 원소에 대해 거리의 제곱에 비례하는 값이 더해진다. 바로 현재 파티클이 얼마나 다른 원소들로 밀집되어 있는지 확인하는 밀도이다. 그리고 p[3]
는 처음은 중력을 의미하는 매크로 G
로 초기화 되고 복잡한 값들로 초기화가 되는데 여기서는 크게 신경쓰지 않아도 된다.
마지막 남은 코드들을 보자.
for(x=011;2012-1>x++;)
b[x]=0;
for(p=a;(t=b+10+(x=*p*I)+80*(y=*p/2),*p+=p[4]+=p[3]/10*!p[1]),p<r;p+=5)
x=0<=x&&x<79&&0<=y&&y<23?1[1[*t|=8,t]|=4,t+=80]=1, *t|=2:0;
for(x=011;2012-1>x++;)
b[x]=" '`-.|//,\\" "|\\_" "\\/\x23\n"[x%80-9?x[b]:16];
usleep(12321);
역시 같은 방법으로 적당히 보기 쉽게 바꿔주자. 다만 첫 반복문의 011
은 8진법이므로 실제 값은 9
임을 헷갈리지 말아야 한다.
for(x=10; x < 2011; x++)
b[x]=0;
for(p=a; p<r; p+=5) {
if (p[1] == 0) {
p[4] += p[3] / 10;
p[0] += p[4];
}
x = p[0] * I;
y = p[0] / 2;
t = b + 10 + x + 80 * y;
if (0<=x && x<79 && 0<=y && y<23) {
t[0] |= 8;
t[1] |= 4;
t[80] |= 2;
t[81] = 1;
}
}
for(x=10; x < 2011; x++) {
if (x % 80 == 9)
b[x] = '\n';
else
b[x]=" '`-.|//,\\|\\_\\/\x23"[b[x]];
}
첫 반복문은 문자열의 뒷부분을 0으로 초기화하는 코드이다. 그리도 두번째 반복문의 첫 분기문을 보자.
if (p[1] == 0) {
p[4] += p[3] / 10;
p[0] += p[4];
}
p[1]
이 0이면, 즉 벽이 아니면 p[4]
에 p[3]/10
을 더하고 그리고 좌표에 해당하는 p[0]
에 p[4]
를 더해준다. 물리엔진, 미적분을 조금 접해본 사람들은 바로 눈치챘을 코드인데, p[3]
는 속도이고 p[4]
는 힘에 해당한다. 그렇다면 파티클의 배열인 배열 a
는 0번째 인덱스부터 차례대로, 좌표, 벽 플래그, 밀도, 속도, 힘을 나타내는 변수들로 이루어진 구조체의 배열로 볼 수 있다.
이 다음 코드는 보면 괴상하지만, 다음 반복문과 함께 보면 그렇게 어렵지 않다.
일단 다음 반복문부터 보도록 하자. 10부터 2011까지, b[x]
값을 지정한 문자열로 치환한다. 그리고, 전체 코드를 슬쩍 보고 오면 알 수 있듯이 이 큰 반복문은 한번 돌았을 때 puts(o), o+=4
코드를 실행하고 o
는 b
의 일부임으로 문자열 b
는 출력 버퍼이다. 그리고 이 출력 버퍼에 t
라는 인덱스 포인터를 사용하여 모든 배열 a
의 원소들을 80*24의 콘솔창 크기에 맞게 출력 버퍼에 씌우는 코드가 t[0] |= 8; ..
이다.
여기서 or 연산자를 사용하여 버퍼의 값을 변경해주는 것은 자연스러운 액체의 모양을 만들기 위함이다. 간단한 예를 들어 생각해보자.
비어있는 버퍼에 대해, x, y
에 액체가 있어서 출력을 해야 한다. 그렇다면 버퍼는 다음과 같은 값이 저장된다.
8 4
2 1
이는 다음과 같이 치환될 것이다.
,.
`'
그리고 x + 1, y
에도 액체가 있어서 출력을 해야 한다면 버퍼는 다음 모양이 된다.
8 12 4
2 3 2
이는 다음과 같이 치환될 것이다. 여기서 3과 12 둘 다 가로선임을 봤을 때 정말 단순하지만 잘 작동하는 메카니즘이란걸 알 수 있다.
,_.
`-`
그리고 화면을 잠시 멈추는 usleep
과 메인 반복문의 증가문에 해당하는 코드까지 적절하게 정리해주면 다음과 같은 코드를 볼 수 있다.
# include <stdio.h>
# include <unistd.h>
# include <complex.h>
double complex a[97687],*p,*q,*r=a,w=0,d;
int x,y;
char b[6856]="\x1b[2J" "\x1b" "[1;1H ",*o=b,*t;
int main() {
while ((x=getc(stdin)) != EOF) {
if (x == '\n') {
// w 에 좌표 (0, y + 2) 넣어서 개행
w = creal(w + 2);
continue;
}
if (x > ' ') {
r[0] = w;
r[5] = w + 1;
if (x == '#')
r[1] = r[6] = 1;
else
r[1] = r[6] = 0;
r += 10;
}
w -= I; // w의 x좌표 증가
}
while (1) {
for (p=a; p<r; p+=5) {
p[2] = p[1] == 1 ? 9 : 0;
for (q=a; q<r; q+=5) {
double i = cabs(p[0]-q[0])/2-1;
if(0<(int)(1-i))
p[2] += i * i;
}
}
for(p=a; p<r; p+=5) {
p[3] = G;
for(q=a;q<r;q+=5) {
d=p[0]-q[0];
double i = cabs(d)/2-1;
if(0<(int)(1-i))
p[3]+=i*(d*(3-p[2]-q[2])*P+p[4]*V-q[4]*V)/p[2];
}
}
for(x=10; x < 2011; x++)
b[x]=0;
for(p=a; p<r; p+=5) {
if (p[1] == 0) {
p[4] += p[3] / 10;
p[0] += p[4];
}
x = p[0] * I;
y = p[0] / 2;
t = b + 10 + x + 80 * y;
if (0<=x && x<79 && 0<=y && y<23) {
t[0] |= 8;
t[1] |= 4;
t[80] |= 2;
t[81] = 1;
}
}
for(x=10; x < 2011; x++) {
if (x % 80 == 9)
b[x] = '\n';
else
b[x]=" '`-.|//,\\|\\_\\/\x23"[b[x]];
}
usleep(12321);
puts(o);
o=b+4;
}
return 0;
}
마지막의 puts(0); o = b + 4
는 반복문의 처음 실행시에만 "\x1b[2J"
을 출력하고 그 다음부터는 이 다음인 "\x1b[1;1H"
부터 출력을 하는 것인데 각각 화면을 지우고, 커서를 맨왼쪽상단으로 옮기는 터미널 이스케이프 코드이다.
결론
너무 어려웠지만 나름 며칠간 분석해보니 재밌었다. 나도 언젠가 이런 코드를 작성하는 멋진 코드 골퍼가 되고 싶다.