메뉴
HN
Hacker News 50일 전

5KB C언어로 만든 터미널용 카드놀이

IMP
4/10
핵심 요약

Oscar Toledo G.가 난독화 C언어 경진대회(IOCCC)를 위해 5KB 미만의 C언어와 curses 라이브러리를 활용해 터미널 기반의 클론다이크 솔리테어 게임을 개발한 후기를 소개합니다. 개발자는 제한된 용량 안에 게임을 구현하기 위해 UI와 코드를 극단적으로 최적화하고, 마우스 대신 키보드(Tab, Space) 조작 방식을 채택하는 등의 문제 해결 과정을 거쳤습니다.

번역된 본문

메인 페이지 인텔 8080 에뮬레이터 체스 프로그램 대회 스토어 레트로게임 FAQ 링크 내 소개 스페인어로 보기

curses용 5KB 크기의 클론다이크 솔리테어 게임 Oscar Toledo G. 작성. 2026년 6월 7일

약간의 여가 시간이 생겼고, 마침 29회 IOCCC가 시작된 것을 보게 되었습니다. 그래서 이 대회에 출품할 작품을 코딩하기로 결정했습니다. '국제 난독화 C 코드 대회(IOCCC)'를 처음 들어보셨다면, 이 대회는 1984년부터 열려온 Landon Curt Noll이 창설한 대회입니다. 이 대회의 목적은 규칙에 명시된 특정 크기 제한 내에서 난독화된 C 프로그램을 작성하는 것입니다. 내가 과거에 수상했던 IOCCC 출품작들은 대회(Contests) 섹션에서 확인할 수 있습니다.

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

내가 말하는 난독화된 C 코드란, 목적은 수행하지만 코드가 명확하게 읽히지 않는 형태로 C 코드를 작성하는 방식을 뜻합니다. 예를 들어, 1부터 10까지 세는 간단한 루프는 보통 이렇게 작성합니다:

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

하지만 C 문법의 강력함을 아주 약간만 이용하면, 다음과 같이 작성할 수 있습니다:

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

이 정도면 난독화된 C 코드가 대략 무엇인지 감을 잡으실 수 있을 것입니다. 해마다 열리는 IOCCC에서 최고의 원라이너(한 줄 짜리 코드)를 해독하는 것은 하나의 도전입니다! 그리고 이는 여러분의 C언어 지식을 테스트하는 좋은 방법이기도 합니다.

발상 여러 아이디어를 생각해 본 끝에, 내 경력 동안 여러 차례 코딩했던 클론다이크 솔리테어 게임을 C언어로 만들기로 결정했습니다. 약 20년 전에는 나만의 운영체제용으로 만들었고(이것에 대해서도 나중에 더 글을 써야겠네요), 최근에는 인텔리비전(Intellivision) 콘솔용 클론다이크를 코딩하기도 했습니다.

아시다시피 솔리테어는 지뢰찾기와 함께 윈도우 3.1에 포함된 게임이었습니다. 당시 윈도우용 게임 시장이 거의 전무했기 때문에 이 두 게임은 완전한 성공을 거두었습니다. 윈도우 3.1을 쓰며 지루했던 사람들은 지뢰찾기와 솔리테어를 시작했고, 그것은 중독성이 있었습니다! 직접 플레이해 보고 규칙을 이해하기 전까지는 그 사실을 몰랐습니다. 또한 '페이튼스(patience)'라고도 불리는 여러 종류의 솔리테어 게임이 있다는 것을 알게 되었고, 윈도우에 있던 것은 그중 '클론다이크' 변형이었습니다. 모든 게임에서 승리할 수 있는 것은 아니라는 사실을 알게 되었을 때 나는 이 게임을 완전히 접었습니다.

사실 수학자들은 한 장씩 카드를 뽑는 게임의 경우 단 43%만 승리할 수 있다고 추정하며, 세 장을 뽑을 때는 이 숫자가 18%로 급감한다고 합니다. 모든 카드를 볼 수 있다고 해도 이기는 것이 결코 쉽지 않습니다.

[윈도우 3.1의 클론다이크 솔리테어 (PCjs Machines에서 실행)]

코딩 시작! 나는 2026년 2월 6일에 코딩을 시작했습니다. C언어로 핵심 로직을 작성하는 데 3일이 걸렸고, 카드 화면을 구현하기 위해 'curses' 라이브러리를 사용하기로 결정했습니다. 컬러를 지원하고 카드 기호를 표시하기 위해 유니코드 문자도 추가했습니다. curses 라이브러리는 사용 중인 터미널의 종류에 구애받지 않고 텍스트 기반 인터페이스를 만들기 위해 화면을 제어할 수 있게 해줍니다.

물론 이 게임의 첫 번째 버전은 대회의 크기 제한에 비해 너무 컸기 때문에 코드 최적화를 시작해야만 했습니다. 또한 카드를 선택하고 놓는 사용자 인터페이스 문제도 해결해야 했습니다. 기존 클론다이크의 인터페이스는 카드 위로 커서를 옮기고, 집어 들고, 다른 곳에 놓는 방식에 초점이 맞춰져 있습니다. 전설에 따르면 윈도우 3.1이 이 게임을 포함시킨 이유는 사용자에게 마우스로 물건을 끌어다 놓는(drag & drop) 방법을 가르치기 위해서였다고 합니다.

대회의 용량 제한에 맞추기 위해, 내 사용자 인터페이스는 카드를 선택할 때는 Tab 키를, 카드를 놓을 때는 Space 키를 사용하는 방식으로 획기적으로 단순화할 수밖에 없었습니다.

curses 배우기 curses를 배우는 것은 당신이 사악한 마법사라면 아주 좋을 겁니다... 아, 죄송합니다. 제 말은 curses '라이브러리'를 배우는 것이라는 뜻입니다. :P 이 게임에 적용하기까지의 학습 곡선은 영원히 끝나지 않을 것처럼 느껴졌습니다. 이 라이브러리에는 여러 버전이 있다는 것을 알게 되었고, 경험상 모든 리눅스 및 Mac 배포판에 어떤 형태로든 이 라이브러리가 포함되어 있다는 것을 알고 있습니다. curses의 가장 큰 문제는 버전이 너무 많다는 것입니다! 그리고 각 운영 체제마다 미묘하게 다른 버전을 통합하고 있습니다...

원문 보기
원문 보기 (영어)
Main page Intel 8080 emulator Chess programs Contests Store Retrogaming FAQ Links About me Ver en español Klondike Solitaire for curses in 5k by Oscar Toledo G. Jun/07/2026 I had some free time, and I noticed the 29th IOCCC started. So I decided to code an entry for the contest. If you haven't heard about the International Obfuscated C Code Contest , it is a contest running since 1984 and created by Landon Curt Noll. The objective of this contest is writing an obfuscated C program under certain size limitations specified by the rules. You can see my previous winning entries for the IOCCC in the Contests section . This year the maximum size is 4993 bytes (a little less than 5 kilobytes), and the number of printable characters is 2503. As there are some special rules for printable characters, there is now a tool called iocccsize for checking this. Whenever I say obfuscated C, it is the way of writing C code in a form that does the objective but it isn't clear. For example, a simple loop counting from 1 to 10 is written like this: #include <stdio.h> int main(void) { int c; for (c = 1; c <= 10; c++) printf("%d\n", c); } But using a small fraction of the C syntax power, we can do it this way: #include <stdio.h> int main(void) {int c=1;while(printf("%d\n",c),11>++c);} This gives you a small idea of what obfuscated C is about. Deciphering the best one liner of any IOCCC year is a challenge! And a way of testing your C knowledge. The idea After pondering several ideas, I decided for a Klondike Solitaire game in C language as I have coded several over my career. One for my own operating system about twenty years ago (I need to write more about this), and recently Klondike for Intellivision consoles . In case you don't know, Solitaire was a game included in Windows 3.1 along Minesweeper. Both were a complete success because the gaming scene for Windows was almost null. Bored people using Windows 3.1 started playing Minesweeper and then Solitaire, and it was addictive! I didn't know until I played myself, and I understood the rules. I also discovered that there are several solitaire games, also known as patience, and this was the Klondike variation. I leaved it for good when I discovered that not all games could be won. In fact, mathematicians estimate that only 43% of the games with a single card deal can be won, and this number drops sharply to 18% if three cards are deal. Even if you can see all the cards it isn't so easy to win. Klondike Solitaire in Windows 3.1 (running in PCjs Machines ) Coding it! I started coding in Feb/06/2026, it took me three days to code the main logic in C, and I decided to use the curses library to create the cards display, use color, and added also Unicode characters to show the cards symbols. The curses library allows to control the screen for creating text interfaces without worrying about the terminal type being used. Of course, this first version of the game was too big for the contest's limits, so I started optimizing the code. Also I had to solve the problem of the user interface for selecting and dropping the cards. The original interface of Klondike is oriented towards moving a cursor over a card, picking it, and dropping it in another place. In fact, the legend says that Windows 3.1 included this game just to teach users how to drag&drop things with the mouse. In order to fit in the space for the contest, my user interface has to be greatly simplified using the Tab key for selecting cards, and the Space key for dropping cards. I don't know how to use curses Learning curses is pretty good if you are an evil wizard... Oh sorry, I mean learning the curses library :P It felt like an eternity the learning curve for using it in the game. I discovered there are several versions of it, and I know by experience that every Linux and Mac distribution includes it in some form. The main problem of curses is that there are way too many versions! And each operating system integrates a vaguely different version of the library. The other common problem is that the suits require UTF-8, and some curses libraries don't support it (see the Remarks). You are good with curses if you are using ASCII exclusively. At least I found character definitions for building boxes (the cards), and Unicode provided me the symbols for the cards. I also could use colors for red cards. Many years ago it was far more common using the ANSI/VT-52 escape sequences, but this is the type of thing that curses solves through an unified interface. My Klondike game for curses in action Polishing I managed to implement a 3-card deal as an option, and also a Las Vegas scoring mode. The number of arguments when running the program selects these options (see the Remarks) The cherry in the cake was adding a scoring system exactly like in Windows, including the bonus per time. The source code After several revisions and days in development, here is the source code of my Klondike Solitaire game for curses. #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 < 7) s += 10; E I x [c] == 1) s +=5; } g = O ; L(f) { I x[f] == c[x] - 8) { 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=""); srand(D); L(c) { for (; v[d = rand() % 52]; ) ; d[b]=d[v]=1; d[k]=c; } c = 24; for (d = 0; d < 7; d++) for (e = 0; e < d + 1; x[c++] = d + (e++ + 4) * 8) ; b[24] = b[26] = b[29] = b[33] = b[38] = b[44] = b[51] = 0; initscr(); raw(); noecho(); start_color(); init_pair(1, COLOR_RED, COLOR_WHITE); init_pair(2, COLOR_BLACK, COLOR_WHITE); A(); n = r - 1; I n & 1) s = -52; for(;;) { for (c = 1; c < 5; c++) { for (e = 0; e < 7; e++) { I e == 2) continue; move(c, e * 6); _ _ _ _ _ I e == 1) { H(32); H(32); H(32); H(32); } } } 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