How I decided to relearn programming
In the age of vibe coding, I decided to swim against the tide and remember what it means to program for real. I'm talking about compiled languages, because for the last 20 years I've written code exclusively for web development in PHP and JS.
Why did I make this decision? Mainly so I don't forget. Beyond that, in a world where code is generated by agents and more and more people have no idea what's happening inside their code at runtime, it becomes critically important to understand how a computer works and what code is actually for. So I decided to start from scratch, so to speak, and recreate a classic game in C for my MacBook.
Vibe coding and programming
Before we begin, I want to remind you that agents don't create new code — they solve problems using standard patterns they were trained on. In practice it looks roughly like this: the agent finds code in some virtual repository that solves your problem and adapts it to your implementation. This is why choosing the right technology stack matters enormously (and it would be nice to actually know what that means), because LLMs are trained on whatever is abundant — and there's a lot of JS out there, for example, which is why agents handle frontend tasks well. My experiments trying to build a working product in Swift showed that Claude Code knows that language significantly worse than JS or C.
Formally speaking, agents play the role of a compiler, much like how people once wrote code in low-level languages such as assembly or raw machine code.
WOZ MONITOR — APPLE I ROM $FF00–$FFFF (256 bytes, 1976)
FF00: D8 58 A0 7F 8C 12 D0 A9
FF08: A7 8D 11 D0 8C 13 D0 C9
FF10: DF F0 13 C9 9B F0 03 C8
FF18: 10 0F A9 DC 20 EF FF A9
FF20: 8D 20 EF FF A0 01 B9 00
FF28: 02 09 80 20 EF FF C9 8D
FF30: D0 D4 8A 48 A9 AC 8D 11
FF38: D0 A9 A7 8D 11 D0 99 00
FF40: 02 0A 85 2B C8 B9 00 02
FF48: C9 8D F0 D4 C9 A3 90 F4
FF50: F0 F0 C9 BA F0 EB C9 D2
FF58: F0 3B 86 28 86 29 84 2A
FF60: B9 00 02 49 B0 C9 0A 90
FF68: 06 69 88 C9 FA 90 11 0A
FF70: 0A 0A 0A A2 04 0A 26 28
FF78: 26 29 CA D0 F8 C8 D0 E0
FF80: C4 2A F0 97 24 2B 50 10
FF88: A5 28 81 26 E6 26 D0 B5
FF90: E6 27 4C 44 FF 6C 24 00
FF98: 30 2B A2 02 B5 27 95 25
FFA0: 95 23 CA D0 F7 D0 14 A9
FFA8: 8D 20 EF FF A5 25 20 DC
FFB0: FF A5 24 20 DC FF A9 BA
FFB8: 20 EF FF A0 00 B1 24 20
FFC0: DC FF A5 24 29 07 10 02
FFC8: A9 8D 20 EF FF E5 24 A5
FFD0: 24 C5 28 A5 25 E5 29 B0
FFD8: 2B E6 24 D0 CE E6 25 4C
FFE0: B4 FF 48 4A 4A 4A 4A 20
FFE8: E5 FF 68 29 0F 09 B0 C9
FFF0: BA 90 02 69 06 2C 12 D0
FFF8: 30 FB 8D 12 D0 60 00 FF
FFFA: 00 00
FFFC: 00 FF
FFFE: 00 00
You had to type this by hand, by the way, just to start using the very first Apple computer. I hope you now understand why Steve Jobs was a genius — and it had nothing to do with the iPhone.
That way of interacting with a computer was extremely inconvenient, so high-level programming languages like C emerged. Now, instead of 100 lines of code, you could write something almost human-readable:
printf("Hello World!");That was progress. Instead of 100 lines, we started writing in something resembling a human language. Then even more advanced languages appeared, like Python, where a single line replaces hundreds of lines of C.
So programming with agents is simply a new type of compiler — one that understands loosely structured human text and turns it into code. And it works exactly like a compiler: it has no idea what it's doing. All responsibility lies with you.
No agent has managed to come up with code like this on its own — and yet this code was no less revolutionary, turning the entire gaming industry upside down in the 1990s:
float Q_rsqrt( float number )
{
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = * ( long * ) &y; // evil floating point bit level hacking
i = 0x5f3759df - ( i >> 1 ); // what the fuck?
y = * ( float * ) &i;
y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
// y = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed
return y;
}
So — to be able to read the code my agent produces, I decided to practice.
Writing Pac-Man
When I was a teenager, I programmed classic games: Snake, Arkanoid, Tetris. But I never got around to Pac-Man, and it turns out to be a genuinely interesting game, precisely because of its mechanics.
For one thing, I didn't know that the four ghosts all behave differently. They move differently relative to the player, attack at different times, and there's even one that's afraid of the player and runs away from him — you can only die from it by walking into its path, because ghosts can't turn around.
The original game also has a clever attack system: one enemy attacks head-on, another tries to predict where you're going and targets that spot. Very interesting overall.
The mechanics are described in various articles, so the only thing left to do was build it. And here things got interesting, because 20 years ago when I was still writing C, I did it on Windows and sometimes even DOS. That was relatively simple for me — I understood the hardware, I understood the OS, and I could just output graphics or text.
But now I have a MacBook, and I had no idea how to write simple code on it. Modern agents, however, have gotten users comfortable with the terminal, and they reminded me that a terminal is basically DOS.
So I started writing.
#include <ncurses.h>
#include <locale.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <ctype.h>
#include <math.h>
#include <string.h>
#define HEIGHT 36
#define WIDTH 28
#define SLEEP 60000
#define ENEMY_SPEED 2
#define ENEMY_PANIC 150
#define FRUIT_TIMER 150
Create a file pacman.c and begin. The hardest parts turned out to be rendering the map and implementing the enemy movement algorithm. The map problem is this: the original game was graphical, and the map was essentially square — 28 by 36 tiles, each 8 by 8 pixels. In a terminal, everything is rendered with characters, and each character occupies 8 by 16 pixels. If I map one tile to one character, the map becomes a tall rectangle stretched vertically, which looks wrong.
So I took a different approach: each map tile is represented by 3 characters side by side. This lets me use one character per actor and move them around the virtual 28×36 grid. The map doesn't stretch vertically, but it does become a bit wider.

The result looks roughly right but is far from the original. I could have replaced the block characters with filled Unicode squares, but the map becomes extremely wide — in the original, many walls are 2 tiles across, which in my scheme means 6 characters. That wasn't going to work.
I needed a different approach, and the answer was sitting right in front of me: instead of drawing each object as three adjacent blocks, draw it as one block surrounded by spaces.
Walls get narrower, but adjacent blocks create gaps. So I thought: what if walls are drawn using line-drawing characters — straight lines, corners, and so on — to make them resemble the original? But that required an algorithm, something like the fast inverse square root from earlier in this article.
An algorithm that understands: if a wall starts here, put a space before it; then draw a line; at a corner, place the correct arc. I spent a couple of days thinking about it but couldn't crack it, so I asked Claude Code for help and got the answer.
const char* get_wall_char(int y, int x) {
int idx = get_idx(y, x);
switch (idx) {
case 0: return " ";
case 1: return "─";
case 2: return "─";
case 3: return "─";
case 4: return "│";
case 5: return "╭";
case 6: return "╮";
case 7: {
int b = get_idx(y+1, x);
if (b == 11 || b == 15) return "─";
int ri = get_idx(y, x+1);
int li = get_idx(y, x-1);
if (li == 7 && ri == 7) return "─";
if (ri == 7) return "╮";
if (li == 7) return "╭";
return "┬";
}
case 8: return "│";
case 9: return "╰";
case 10: return "╯";
case 11: {
int a = get_idx(y-1, x);
if (a == 7 || a == 15) return "─";
int ri = get_idx(y, x+1);
int li = get_idx(y, x-1);
if (li == 11 && ri == 11) return "─";
if (ri == 11) return "╯";
if (li == 11) return "╰";
return "┴";
}
case 12: return "│";
case 13: {
int r = get_idx(y, x+1);
if (r == 14 || r == 15) return "│";
int bi = get_idx(y+1, x);
int ai = get_idx(y-1, x);
if (ai == 13 && bi == 13) return "│";
if (bi == 13) return "╰";
if (ai == 13) return "╭";
return "├";
}
case 14: {
int l = get_idx(y, x-1);
if (l == 13 || l == 15) return "│";
int bi = get_idx(y+1, x);
int ai = get_idx(y-1, x);
if (ai == 14 && bi == 14) return "│";
if (bi == 14) return "╯";
if (ai == 14) return "╮";
return "┤";
}
case 15: {
int ul = is_wall(y-1,x-1), ur = is_wall(y-1,x+1);
int dl = is_wall(y+1,x-1), dr = is_wall(y+1,x+1);
if (ul && ur && dl && dr) return " ";
if (ul && ur && !dl && dr) return "╮";
if (ul && ur && dl && !dr) return "╭";
if (dl && dr && !ul && ur) return "╯";
if (dl && dr && ul && !ur) return "╰";
return "─";
}
}
return " ";
}That turned the game into something actually playable.

The game is now clearly recognizable as the original, built entirely in text and running in a terminal. The only thing I'm missing are proper characters for Pac-Man and the ghosts, but at this point it doesn't matter.
I managed to write the game after not touching C code for 20 years. There were plenty of problems along the way — compiler errors, logic bugs, all sorts of things. For example, there was a bug with round completion. To advance to the next level, you need to collect all the dots on the map, and there's a counter that tracks them. If you died (all three lives gone) and chose to start over, the game would jump to the next level even if you'd only collected five dots. The cause was simple: I hadn't reset the dot counter when starting a new game, so it remembered how many dots you'd collected before you lost.
I'll close with this: vibe coders, study programming. It will help you spend fewer tokens, understand what your agent is actually writing, and notice when it's quietly handed you a pig — that is, a bug.
To say thank you and show support for future content.
50€/annually
To gain access to commentary and content, please consider subscribing.
If you're already a customer, just log in.
we do not store your email, only the encrypted hash, which increases the security of your email.