본문으로 건너뛰기

© 2026 Molayo

HN분석2026. 06. 15. 09:18

5k C 코드로 구현한 curses용 Klondike Solitaire 게임

요약

IOCCC(국제 난해한 C 코드 콘테스트)를 위해 5KB 미만의 제한된 크기로 구현된 curses 기반 Klondike Solitaire 게임 개발기를 소개합니다. C 언어의 구문을 극도로 활용하여 난해하게 작성된 코드의 특징과 구현 과정을 다룹니다.

핵심 포인트

  • IOCCC의 목적과 난해한 C 코드(Obfuscated C)의 개념 설명
  • 4993바이트 제한 내에서 구현된 Klondike Solitaire 게임
  • curses 라이브러리를 활용한 터미널 기반 텍스트 인터페이스 구현
  • C 언어 문법을 극한으로 활용한 코드 작성 방식 소개

5k C 코드로 구현한 curses용 Klondike Solitaire

작성자: Oscar Toledo G. 2026년 6월 7일

여유 시간이 생겨서 제29회 IOCCC가 시작되었다는 것을 알게 되었습니다. 그래서 이번 콘테스트에 제출할 작품을 코딩하기로 결정했습니다. 만약 International Obfuscated C Code Contest (국제 난해한 C 코드 콘테스트)에 대해 들어본 적이 없다면, 이는 1984년부터 시작되어 Landon Curt Noll에 의해 만들어진 콘테스트입니다. 이 콘테스트의 목적은 규칙에 명시된 특정 크기 제한 내에서 난해한 (obfuscated) C 프로그램을 작성하는 것입니다.

Contests 섹션에서 제가 이전에 IOCCC에서 수상했던 작품들을 확인하실 수 있습니다.

올해의 최대 크기는 4993 바이트 (5킬로바이트보다 약간 적음)이며, 출력 가능한 문자의 수는 2503개입니다. 출력 가능한 문자에 대한 몇 가지 특별한 규칙이 있기 때문에, 현재 이를 확인하기 위한 iocccsize라는 도구가 있습니다.

제가 난해한 (obfuscated) C라고 말할 때마다, 이는 목적은 달성하지만 명확하지 않은 형태로 C 코드를 작성하는 방식을 의미합니다. 예를 들어, 1부터 10까지 숫자를 세는 간단한 루프는 다음과 같이 작성됩니다:

#include <stdio.h>
int main(void) {
int c;
for (c = 1; c <= 10; c++)
printf("%d\n", c);
}

하지만 C 구문 (syntax)의 힘을 아주 조금만 사용하면 다음과 같이 작성할 수 있습니다:

#include <stdio.h>
int main(void) {int c=1;while(printf("%d\n",c),11>++c);}

이것은 난해한 (obfuscated) C가 무엇인지에 대한 작은 아이디어를 제공합니다. 매년 IOCCC의 최고의 한 줄 코드 (one liner)를 해독하는 것은 하나의 도전이며, 여러분의 C 지식을 테스트하는 방법이기도 합니다!

아이디어

여러 아이디어를 고민한 끝에, 저는 C 언어로 Klondike Solitaire 게임을 만들기로 결정했습니다. 저는 경력을 쌓아오면서 여러 개의 솔리테어 게임을 코딩해 왔기 때문입니다. 약 20년 전 저만의 운영체제(OS)를 위해 만든 것(이에 대해 더 많이 써야 합니다)과, 최근에는 Intellivision 콘솔용 Klondike를 만든 적이 있습니다.

혹시 모르실 수도 있겠지만, Solitaire(솔리테어)는 Minesweeper (지뢰찾기)와 함께 Windows 3.1에 포함되었던 게임입니다. 당시 Windows용 게임 환경이 거의 전무했기 때문에 두 게임 모두 엄청난 성공을 거두었습니다. Windows 3.1을 사용하던 지루해하던 사람들이 Minesweeper를 플레이하기 시작했고, 그다음에는 Solitaire를 플레이하게 되었는데, 이것은 중독성이 매우 강했습니다! 저도 직접 플레이해보고 규칙을 이해하기 전까지는 몰랐습니다. 또한 patience라고도 알려진 여러 가지 솔리테어 게임이 있다는 사실도 발견했는데, 이것이 바로 Klondike (클론다이크) 변형 방식이었습니다. 저는 모든 게임을 이길 수 있는 것은 아니라는 사실을 깨달았을 때 완전히 그만두었습니다. 실제로 수학자들은 카드를 한 장씩 돌리는 게임의 경우 약 43%만이 승리할 수 있다고 추정하며, 세 장씩 돌리는 경우에는 이 수치가 18%로 급격히 떨어집니다. 모든 카드를 볼 수 있다고 해도 승리하는 것이 그리 쉽지는 않습니다.

코딩하기!

저는 2026년 2월 6일에 코딩을 시작했습니다. C 언어로 주요 로직을 코딩하는 데 3일이 걸렸고, 카드 디스플레이를 만들고 색상을 사용하며 카드 심볼을 표시하기 위해 Unicode (유니코드) 문자를 추가하고자 curses 라이브러리를 사용하기로 결정했습니다. curses 라이브러리를 사용하면 사용 중인 터미널 유형에 대해 걱정할 필요 없이 텍스트 인터페이스를 만들기 위한 화면 제어가 가능합니다.

물론, 이 게임의 첫 번째 버전은 콘테스트 제한 범위를 넘어서 너무 컸기 때문에 코드를 최적화하기 시작했습니다. 또한 카드를 선택하고 내려놓는 사용자 인터페이스(UI) 문제를 해결해야 했습니다.

Klondike의 원래 인터페이스는 커서를 카드 위로 이동시켜 카드를 선택하고 다른 곳에 내려놓는 방식에 맞춰져 있습니다. 사실, 전설에 따르면 Windows 3.1이 이 게임을 포함한 이유는 단지 사용자들에게 마우스로 드래그 앤 드롭 (drag&drop) 하는 법을 가르치기 위해서였다고 합니다.

콘테스트의 공간 제한에 맞추기 위해, 제 사용자 인터페이스는 카드를 선택할 때는 Tab 키를, 카드를 내려놓을 때는 Space 키를 사용하는 방식으로 크게 단순화되어야 했습니다.

curses 사용법을 모릅니다

사악한 마법사가 되려는 것이라면 curses를 배우는 것이 꽤 괜찮을 겁니다... 아 죄송합니다, curses 라이브러리를 배우는 것이라는 뜻이었습니다 :P 게임에서 이를 사용하는 학습 곡선(learning curve)은 마치 영겁의 시간처럼 느껴졌습니다. 저는 curses의 여러 버전이 있다는 것을 발견했으며, 모든 Linux 및 Mac 배포판이 어떤 형태로든 이를 포함하고 있다는 것을 경험을 통해 알고 있습니다.

curses의 주요 문제는 버전이 너무나도 많다는 것입니다! 그리고 각 운영 체제는 약간씩 다른 버전의 라이브러리를 통합하여 제공합니다. 또 다른 흔한 문제는 카드 문양(suits)에 UTF-8이 필요하다는 점인데, 일부 curses 라이브러리는 이를 지원하지 않습니다 (비고(Remarks) 참조). 오직 ASCII만을 사용한다면 curses를 사용하기에 무리가 없을 것입니다.

적어도,

저는 상자(카드)를 만들기 위한 문자 정의를 찾아냈고, Unicode는 카드 기호들을 제공해 주었습니다.

또한 빨간색 카드를 위해 색상을 사용할 수도 있었습니다. 수년 전에는 ANSI/VT-52 이스케이프 시퀀스(escape sequences)를 사용하는 것이 훨씬 더 일반적이었지만, curses는 통합된 인터페이스를 통해 이러한 문제를 해결해 줍니다.

My Klondike game for curses in action

실행 중인 curses용 Klondike 게임

다듬기 (Polishing)

옵션으로 3장 나누기(3-card deal)를 구현해냈고, Las Vegas 점수 모드도 추가했습니다. 프로그램을 실행할 때 입력하는 인자(arguments)의 개수에 따라 이 옵션들이 선택됩니다 (비고(Remarks) 참조).

백미(The cherry in the cake)는 시간당 보너스를 포함하여 Windows와 정확히 일치하는 점수 시스템을 추가한 것이었습니다.

소스 코드

여러 번의 수정과 며칠간의 개발 끝에, curses용 Klondike Solitaire 게임의 소스 코드를 공개합니다.

소스 코드

여러 번의 수정과 며칠간의 개발 끝에, curses용 Klondike Solitaire 게임의 소스 코드를 공개합니다.

#include <stdlib.h>
#include <locale.h>
#include <time.h>
#include <curses.h>
#define H addch
#define L(z) for(z=0;z<52;z++)
#define _ H(ACS_CKBOARD);
#define O 255
#define I if(
#define E else
#define J H(ACS_HLINE);
int W(int *,int *);
int k[O ], x[ O ],
b[O], v [ O ],i,s, m,a ,z,e,
w,n,d,t,c, h,g,j,f,l,y; void F(void)
{d=c[k]; e= c[x]; f=c[b]; for(g=c;g<51;g
[k]=k[1+g],g[x] =x[g+1],g[b]=b[g+1],g++);g[k]=
d; g[x]=e; g[b]=f; c=g; } void U(void) { d = O;
L(c) I !c[x]) d = c; c = d; I d - O) { c[x] =1;
b[c] = 0; F(); d = c; } } void A(void) { m = 1;
t = O; l =0; d = O; L(c) I !c[x] & b[c]) d
= c; I d - O) v[l++] = d; e = O;
L(c) I x [c] == 1 && !c[b])
e = c ;I e - O ) { I
d == O ) v[l++] =
d; v[l++ ] = e;
}
L(c) I c[x]

31 && !c[b])
v[l++ ] = c; I l>0)
I v[0] == O) j = 0;
E j=x[v[0]]; qsort(
v,l,sizeof(m), &W);}
void K (void ) { t = c; d =
0; I c[x] > 31) { L(e) I (e[x]
& 7) == (c[x] & 7)) { I x[e] > x [c]) d =
1; } } } void M(void) { I c[x] ==e) return;
I n & 2) { I 1 == c[x]) { I a > 1) a -- ; } } I
n & 1) { I e > 2 & e < 7) s +=
5; } E { I e > 2 & e <

  1. s += 10; E I x
    [c] == 1) s +=5;
    } g = O ; L(f) {
    I x[f] == c[x]

{
I b[f]) { I ~n & 1) s += 5; b[f] = 0; g = f; } }
} } do { h = c[x] + 8;
c[x] = e; F(); e = c[x] + 8; c = O; L(f) I x[f] == h) c = f; } while
(c - O) ; t = O; }
int W(int *e, int *h) { return x[*e]-x[*h]; }
int main(int r) { char *S;time_t D=time(0);setlocale(LC_CTYPE, S=

x[*e]-x[*h]; }\nint main(int r) { char *S;time_t D=time(0);setlocale(LC_CTYPE, S=L(e) { c = x[e]; g = (c & 7) * 6; I c == 1) { I a > 2 & k[e] == z) g += 4; I a > 1 & k[e] == y) g += 2; }h = k[e] / 13; i = k[e] % 13; for(d=0;d<4;d++){ move(c / 8 * 2 + d + 1, g);if (!d) { H(ACS_ULCORNER); J J J H(ACS_URCORNER); }E if (d == 3) {H(ACS_LLCORNER); J J J H(ACS_LRCORNER); }E { H(ACS_VLINE);if (b[e]) { _ _ _ }E { I h < 2) attron(COLOR_PAIR(1));E attron(COLOR_PAIR(2)); if (d == 2) { H(32); H(32); H(32); }E { I !i)H(65);E I i < 9) H(49 + i);E I i == 9) { H(49); H(48); }E { H("JQK"[i - 10]); }addstr(&"\xe2\x99\xa5\0\xe2\x99\xa6\0\xe2\x99\xa0\0\xe2\x99\xa3"[h * 4]);I i - 9) H(32); }I h < 2) attroff(COLOR_PAIR(1));E attroff(COLOR_PAIR(2));}H(ACS_VLINE);} } } attron(A_NORMAL);w = j % 8 * 6;I j == 1) I n & 2) w += (a - 1) * 2;move(j / 8 * 2 + 1, w);H(42);I t - O) {c = x[t];w = c % 8 * 6;I c == 1) {I n & 2) w += (a - 1) * 2;}move(c / 8 * 2 + 1, w);H(38);} mvprintw(0, 0, "%sScore: %d ", S, s);refresh(); c = getch();I c == 10) break;I c == 32) {I m) {I !j) {I v[0] == O) {do { d = O; L(c) I c[x] == 1)d = c;c = d;I c - O) {c[x] = 0; b[c] = 1; F(); } }while (c != O); }E {I n & 2) {a = 0; U();I d - O) a++;U();I d != O) a++, y = k[d];U();I d != O) a++;z = k[d];}E U();d = O;L(c) I !c[x]) d = c; }A(); }E {L(d) I j == x[d]) c = d;K(); m = 0; l = 0;I !d) { for (e = 3; e < 7; e++) { g = O; L(f) I x[f] == e) g = f; I (g == O & k[c] % 13 == 0) | (g != O & k[c] % 13 == k[g] % 13 + 1 & k[c] / 13 == k[g] / 13)) { v[l++] = e; } } } for (h = 0; h < 7; h++) { f = h + 32; g = O; L(e) I (x[e] & 7) == h & x[e] > 31 & x[e] + 8 > f) { f = x[e] + 8; g = e; }I (g == O & k[c] % 13 == 12) | (g != O & k[g] % 13 - 1 == k[c] % 13 & ((k[g] / 13) & 2) != ((k[c] / 13) & 2))) v[l++] = f; } I l) j = v[0]; } }E { c = t; e = j; M(); d = 0; L(c) d+=c[x] < 3 | c[x] > 6; I !d) S="Won!",s+=n&1?0:700000/difftime(time(0),D);A(); clear();} }E I c == 9 && l) { I m) { for (c = 0; c < l; c++) I j == ((d = v[c]) - O ?

x[d] : 0)) break; j = (d = v[++c % l]) - O ? x[d] : 0; } E { I l)
{ for (c = 0; c<l&j!=v[c]; c++) ; j = v[++c % l]; } } } } endwin(); }

비고 (Remarks)

Klondike Solitaire

빌드 방법 (Building it)

gcc prog.c -o prog

실행 방법 (Executing it)

prog
prog a
prog a b
prog a b c

상세 정보 (About it)

이제 이 프로그램으로 Klondike solitaire (클론다이크 솔리테어) 게임을 즐길 수 있습니다.

빌드를 위해서는 ncurses가 필요하며, UTF-8 지원이 필요합니다. 최신 운영체제라면 무엇이든 가능합니다 (Terminal 및 macOS 12.7.6에서 테스트 완료).

Fedora 21 (2014년이라 매우 오래되었습니다!)에서는 카드 그래픽이 나타나지 않으며, 링커 옵션 -lcurses-lcursesw로 변경해야 합니다.

macOS 10.15 (Catalina)에서도 테스트를 진행했습니다. Homebrew가 설치되어 있어 brew install ncurses를 입력했고, Github에서 533MB를 자동으로 업데이트하며 설치되었습니다. 기호(symbols)는 나타나지만, 카드 테두리와 배경을 포함하는 폰트가 없습니다. 2020년의 운영체제가 2014년부터 Linux가 보유해 온 기호 지원 기능을 갖추지 못했다는 점이 흥미롭습니다.

macOS 10.11 (El Capitan)에서도 확인해 보았으나, 기본 ncurses v5는 UTF-8을 지원하지 않습니다. Homebrew 설치를 시도했으나 Github와 통신할 수 없었습니다.

터미널 줄 수를 36줄 또는 40줄로 맞추어 창 크기를 조정할 것을 권장합니다.

사용자를 혼란스럽게 하기 위해 컴파일 시 경고(warnings)를 생성하도록 설계되었습니다.

플레이 방법 (Playing it)

인자(argument) 없이 실행하면 보너스 시간 점수를 포함한 Windoze 점수 체계의 Klondike solitaire 게임이 시작됩니다.

인자를 하나 추가하면 Las Vegas 점수 체계로 플레이할 수 있습니다.

인자를 두 개 추가하면 3장의 카드를 나누는 Windoze 점수 체계로 플레이합니다.

인자를 세 개 추가하면 3장의 카드를 나누는 Las Vegas 점수 체계로 플레이합니다.

게임 키 (Game keys)

Tab 키를 사용하여 커서(별표)를 이동할 수 있으며, Space bar를 사용하여 이동하거나 놓을 카드를 선택할 수 있습니다.

왼쪽 상단에서 Space bar를 누르면 카드를 나누기(deal) 시작합니다.

지루해지면 Enter를 눌러 게임을 종료하세요.

난독화 기법 (Obfuscation tricks)

!= 연산자는 C 언어에서 불필요하다는 사실은 모두가 알고 있습니다. 마이너스(-) 연산자만 사용하세요.

>= 연산자도 마찬가지입니다. >만 사용하세요.

오늘 중괄호({) 하나를 아끼기 위해 for를 사용하세요.

또한 배열 접근을 작성할 때, 그 느낌을 살려 배열과 인덱스를 무작위로 교체하세요.

상수에는 숫자 0처럼 보이도록 알파벳 O를 사용하세요.

분위기를 더하기 위해 삼항 연산자 (trinary operators)를 몇 개 던져 넣읍시다.

가장 훌륭한 난독화 (obfuscation)는 보너스 시간 점수를 주기 위한 시계 코드를 추가했을 때 이루어졌습니다. 실제 소프트웨어처럼 말이죠.

My Klondike game for curses being played

실행 중인 curses용 Klondike 게임

다운로드 (Download)

IOCCC 출품작은 여기서 다운로드할 수 있습니다:

사후 분석 (Post Mortem)

나중에 콘테스트가 종료된 후, 코드에 난독화할 수 있는 기회와 최적화할 수 있는 경로가 더 남아 있다는 것을 깨달았습니다.

우승자 발표 생중계를 매우 즐겁게 시청했지만, 안타깝게도 제 출품작은 우승하지 못했습니다.

여러분이 텍스트 콘솔에서 Klondike 게임을 즐기며 재미를 느끼실 수 있도록 제 출품작을 공유하게 되어 기쁩니다. 그리고 물론, 이것이 C 언어로 만든 가장 작은 솔리테어 게임이라고 주장할 수도 있겠네요 ;)

이 기사가 즐거우셨나요? 공유해 주시거나, ko-fi에서 커피 한 잔을 대접해 주시거나, 월간 후원자가 되어 주세요. 여러분의 후원은 제가 이와 같은 기사를 쓰는 데 더 많은 시간을 할애할 수 있게 해줍니다.

링크 (Links)

최종 수정일: 2026년 6월 8일

AI 자동 생성 콘텐츠

본 콘텐츠는 HN AI Posts의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0