Moss Circuit: Mini Game Ular Buatan Sendiri dalam 839 Baris Pascal

Hampir semua orang yang belajar pemrograman pernah menulis program pertama yang sama: ular yang bergerak di layar dan makan titik yang berpindah tempat. Yang biasanya langsung dibuang setelah tiga hari.
Moss Circuit adalah program itu juga, tapi ditulis ulang dengan sengaja lalu dibagikan ke publik. Bukan game snake yang dibaca lalu ditinggalkan — ini kode yang selesai dalam keadaan jalan, dan seluruh isinya bisa dibaca dari atas sampai bawah dalam satu duduk.
Yang membuatnya menarik bukan visualnya, melainkan strukturnya. Mari kita bedah.
Dibangun tanpa engine, dan itu memang pilihan yang tepat
Moss Circuit ditulis dalam Object Pascal dan dikompilasi dengan Free Pascal Compiler. Pustaka yang dipakai: tidak ada. Seluruh game digambar dengan fungsi WinAPI GDI.
Untuk game snake, ini keputusan yang tepat. Yang dibutuhkan hanya empat operasi:
| Kebutuhan | Fungsi GDI |
|---|---|
| Menggambar satu petak | Rectangle / FillRect |
| Menggambar lingkaran | Ellipse |
| Menggambar teks | TextOut |
| Meminta frame baru | InvalidateRect + timer |
Tidak ada texture yang perlu dimuat, tidak ada loop render yang harus dijadwalkan, tidak ada aset eksternal. Justru karena itulah program ini bisa punya seluruh source-nya di 839 baris dan tetap terbaca manusia.
Kalau ditulis dengan engine seperti Godot atau Unity, proyek dengan fitur serupa bisaeasily 3.000 baris — dan sebagian besar baris itu adalah metadata yang tidak perlu dibaca siapa pun.
Tata letak papan
Window bisa di-resize. CalculateBoard bekerja begini:
Padding := 48;
BoardSize := Min(ClientWidth - Padding * 2, ClientHeight - 286);
BoardSize := Max(240, BoardSize);
BoardLeft := (ClientWidth - BoardSize) div 2;
BoardTop := 190;
CellSize := BoardSize div BoardCells;
BoardSize := CellSize * BoardCells;
BoardLeft := (ClientWidth - BoardSize) div 2;
Tiga hal menarik di sini.
BoardSize dihitung ulang dua kali. Betul, itu bukan salah baca. Hasil pertama dari ClientWidth dan ClientHeight hanyalah tebakan kasar. Lalu CellSize membagi ruang itu ke BoardCells, dan baris kelima mengalikan lagi. Hasilnya papan yang benar-benar pas di kelipatan BoardCells, tanpa sisa pixel yang membuat tepi papan terlihat miring.
Angka 286 adalah konstanta magis. Itu sisa ruang untuk header, kartu skor, dan footer. Rapuh — kalau nanti kartu skor berubah tinggi, angka ini harus diubah manual. Kode yang lebih rapi akan menyimpan tinggi bagian atas sebagai named constant.
Minimum 240 pixel. Di window kecil, papan tidak pernah menyusut sampai tidak bisa dimainkan.
Palet warna di balik fungsi
Kebanyakan program WinAPI kecil punya fungsi warna yang berantakan. RGB(20, 30, 40) tercecer di empat puluh tempat, dan begitu mau ganti tema, kamu harus edit empat puluh baris.
Moss Circuit tidak begitu. Semua warna lewat dua belas helper:
function RGBColor(R, G, B: Byte): COLORREF;
function ThemeColor(DarkR, DarkG, DarkB, LightR, LightG, LightB: Byte): COLORREF;
function PageColor: COLORREF;
function PanelColor: COLORREF;
function BorderColor: COLORREF;
function TextColor: COLORREF;
function MutedColor: COLORREF;
function AccentColor: COLORREF;
function SnakeColor: COLORREF;
function FoodColor: COLORREF;
function CanvasColor: COLORREF;
function GridColor: COLORREF;
ThemeColor adalah inti dari semuanya. Namanya sudah menjelaskan: fungsi ini dipanggil dengan enam angka, tiga untuk tema gelap dan tiga untuk terang.
function ThemeColor(DarkR, DarkG, DarkB, LightR, LightG, LightB: Byte): COLORREF;
begin
if LightTheme then
Result := RGBColor(LightR, LightG, LightB)
else
Result := RGBColor(DarkR, DarkG, DarkB);
end;
Jadi PanelColor memanggil ThemeColor dengan angka gelap lebih dulu, lalu angka terang. Sekarang tombol T cukup mengubah satu variabel boolean:
procedure ToggleTheme;
begin
LightTheme := not LightTheme;
if LightTheme then
SaveSetting('theme.dat', 'light')
else
SaveSetting('theme.dat', 'dark');
InvalidateRect(MainWindow, nil, True);
end;
Dua baris terakhir melakukan dua hal sekaligus: menyimpan preferensi, lalu meminta repaint. Parameter True di InvalidateRect berarti seluruh area dihapus ulang — persis yang dibutuhkan agar warna lama tidak tertinggal di layar.
Ini pola yang layak ditiru. Kalau kamu menulis aplikasi dengan palet gelap, sembunyikan warna di balik fungsi, bukan di balik konstanta global. Fungsi memungkinkan keputusan diambil di satu tempat. Konstanta memaksa kamu menyentuh semua call site setiap kali tema berubah.
Penyimpanan preferensi
Semua state yang bertahan disimpan di dua file teks biasa, bukan registry dan bukan database:
%APPDATA%\MossCircuit\theme.dat
%APPDATA%\MossCircuit\best.dat
LoadSettings dimulai dengan aman:
DataDirectory := GetEnvironmentVariable('APPDATA');
if DataDirectory = '' then
DataDirectory := ExtractFilePath(ParamStr(0));
DataDirectory := IncludeTrailingPathDelimiter(DataDirectory) + 'MossCircuit';
ForceDirectories(DataDirectory);
Baris kedua itu yang penting. Kalau environment variable APPDATA kosong — yang bisa terjadi di Wine, atau saat program dijalankan dari lingkungan non-Windows — program tidak crash. Ia jatuh ke folder executable. ForceDirectories memastikan folder-nya ada sebelum pertama kali menulis.
tentang file I/O, kode ini memakai {$I-} di sekitar operasi file, yang mematikan exception I/O, lalu memeriksa IOResult secara manual:
{$I-}
Reset(FileHandle);
if IOResult = 0 then
begin
ReadLn(FileHandle, Value);
CloseFile(FileHandle);
if IOResult = 0 then
LightTheme := Value = 'light';
end;
{$I+}
Setiap operasi file diperiksa dengan IOResult. Tidak ada try/except yang menutupi masalah. Ini kode yang benar, bukan kode yang kebetulan bekerja.
Mekanik dan kecepatan
Konstanta kuncinya pendek:
BoardCells = 24;
MaxSegments = BoardCells * BoardCells;
StartSpeed = 145;
MinimumSpeed = 66;
MaxSegments = 24 * 24 = 576 adalah batas yang benar. Ular tidak bisa menempati lebih dari semua petak yang ada, dan array tidak perlu lebih besar dari itu.
Tapi ada satu detail di PlaceFood yang sering terlewat di program snake:
function PlaceFood: Boolean;
var
I: Integer;
Occupied: Boolean;
begin
if SnakeLength >= MaxSegments then
begin
Result := False;
Exit;
end;
repeat
FoodCell.X := Random(BoardCells);
FoodCell.Y := Random(BoardCells);
Occupied := False;
for I := 0 to SnakeLength - 1 do
if (Snake[I].X = FoodCell.X) and (Snake[I].Y = FoodCell.Y) then
begin
Occupied := True;
Break;
end;
until not Occupied;
Result := True;
end;
Baris if SnakeLength >= MaxSegments then Exit adalah pengaman yang sering terlewat. Kalau papan penuh — ular mengisi semua 576 petak dan pemain masih hidup — tanpa baris ini, fungsi masuk ke repeat tanpa henti karena setiap petak sudah ditempati. Program hang: tidak error, tidak crash, hanya tidak merespons.
Dengan baris itu, PlaceFood mengembalikan False dan game tahu untuk mengakhiri sesi. Satu baris, dan tanpa itu game bisa menggantung.
Kecepatan dihitung ulang tiap tick:
function CurrentSpeed: UINT;
begin
Result := Max(MinimumSpeed, StartSpeed - Score * 3);
end;
145 milidetik per frame di awal, turun 3 milidetik per biji, berhenti di 66. Angka MinimumSpeed yang 66 itu disengaja — ada batas bawah supaya ular tidak jadi mustahil dikendalikan setelah 27 biji.
Perhitungannya satu baris, tapi tanpa Max tersebut game akan menjadi tidak bisa dimainkan terlalu cepat.
Yang membuatnya terasa dipoles
Tiga hal kecil yang jarang ada di program snake buatan sendiri.
Papan dihitung ulang setiap resize, bukan hanya di awal. Kalau CalculateBoard dipanggil setiap WM_SIZE, papan selalu mengikuti ukuran window tanpa perlu tombol refresh.
Tombol bisa diklik. Tombol start bukan gambar statis, tapi hit-testing rectangle dengan koordinat mouse. Program yang hanya menerima keyboard terasa seperti alat, bukan seperti aplikasi.
Rekor disimpan saat sesi selesai. SaveBestIfNeeded dipanggil dari FinishGame dan CompleteGarden, jadi skor terbaikmu sudah tersimpan begitu permainan berakhir — bukan harus menunggu aplikasi ditutup dengan rapi.
Yang belum ada, dan kenapa itu wajar
Ini bukan game lengkap, dan tidak berpura-pura menjadi game lengkap. Tidak ada audio. Tidak ada highscore online. Tidak ada mode campaign dengan difficulty bertahap.
Untuk game snake, semua itu memang opsional. Yang membuat program ini bertahan adalah hal lain: kodenya pendek dan jujur. Tidak ada aset yang hilang, tidak ada dependensi yang harus dipasang, tidak ada dokumentasi yang perlu dibaca untuk menjalankan. Ada file .pas, ada fpc, dan ada MossCircuit.exe.
Untuk siapa
Kalau kamu sedang belajar Pascal atau WinAPI, ini contoh yang layak dibaca dari atas sampai bawah. 839 baris cukup untuk menunjukkan:
- Struktur program WinAPI yang benar: message loop, window procedure, penanganan WM_SIZE
- Palet warna terpusat yang bisa di-tema
- Penanganan I/O yang aman tanpa
try/except - Guard condition untuk edge case yang bisa menggantungkan program
Kalau kamu sedang mencari game untuk dimainkan sekarang, ini ringan. Executable kecil, tanpa runtime, tanpa installer, langsung jalan.
Kalau kamu sedang belajar menulis game dari nol, bagian paling berguna ada di CalculateBoard dan CurrentSpeed. Keduanya sederhana, tapi keduanya menunjukkan hal yang sama: sebagian besar keputusan sulit dalam game bukan soal algoritma, tapi soal memilih batas yang aman.
Coba sendiri
Source code lengkapnya ada di GitHub:
- Repository: github.com/ayahzahra/moss-circuit
- Kompilasi:
fpc -WG -O2 -Xs MossCircuit.pas - Penulis: Nuril Ayahzahra
Tidak ada dependensi runtime, tidak ada aset yang perlu diunduh, tidak ada akun yang perlu dibuat.
Penutup
Program snake pertama setiap orang punya hasil yang sama: berhasil jalan, lalu dihapus. Moss Circuit adalah kasus yang sedikit lebih menarik. Program yang dipoles ulang dengan tema yang benar, penyimpanan preferensi yang aman, dan sebuah guard condition yang mencegah hang. Mungkin tidak banyak orang yang berpikir untuk contrabandakannya ke publik, tapi hasilnya tetap berguna: kode yang bisa dipelajari, yang jalan di Windows tanpa minta satu pun dependency.
Kalau kamu sedang belajar pemrograman dan butuh sesuatu yang kecil untuk dibaca dari depan sampai belakang, ini tempat yang baik untuk mulai.
Artikel ini ditulis dengan bantuan AI. Semua klaim teknis di atas diverifikasi langsung terhadap kode sumber di repository.