Week 4: When Planning Pays Off
Week 4: When Planning Pays Off
Monday was MLK Day. We had plans to go to Oceanside at 10am.
I got up at 6am and went to Starbucks anyway.
The question wasn't "should I skip?" It was "what time do I need to get up?"
Week 1 me would have skipped. Week 3 me barely made it through Friday. Day 16 me adapted.
Twenty days of showing up. It's starting to feel automatic.
This is Week 4 of my 6-month commitment to learn real-time networked systems from scratch.
The Plan
Week 4's goal: build a multi-client chat server with clean architecture.
Multiple clients connecting simultaneously. Username system. Broadcast messaging. Direct messages to specific users. Commands like /users and /help. Graceful disconnects.
Monday morning at Starbucks, I opened my notebook and drew boxes.
Server in the center. Arrows coming in from multiple clients. Arrows going back out to broadcast. A separate box for message parsing. Another for client management.
I wasn't drawing every function. I was drawing the flow. How does a message get from one client, through the server, and out to everyone else? Where does username storage fit? What happens when someone disconnects?
Fifteen minutes on core decisions. Linked list for clients (easy to add and remove). Poll-based I/O (comfortable with this from Week 2). Separate utility files for client management and poll descriptor handling. Message parsing happens before routing. Parse once, route correctly.
The plan wasn't detailed. I didn't know exactly how I'd implement direct messaging yet. But I knew where it would fit in the architecture.
Week 3 taught me that starting without a plan leads to tangled code. Week 4 was proving I could plan enough without over-planning.
After sketching the architecture, I spent the rest of the session reading Gaffer's articles on snapshot interpolation and compression. Not for this week's project. For Month 2. Understanding how games pack state efficiently into packets, how they handle large amounts of data and packet loss. I took notes on the concepts I'd need later: delta compression, bandwidth optimization, interpolation between updates.
Then I documented Month 1 learnings. Writing down what I'd actually built each week, what I understood versus what I'd just copied, what surprised me. The act of writing it down revealed gaps I hadn't noticed. Week 1's TCP patterns were solid. Week 2's UDP concepts were there but shallow. Week 3's bitmap logic still felt fuzzy.
By 3:30pm, I had an architecture sketch, notes on game networking, and a clear view of what Month 1 had actually taught me.
Monday was preparation, not building. And it felt right.
Day 17: Foundation
Tuesday I started coding.
The socket setup came automatically now. socket(), bind(), listen(). I've written this sequence five times. It's muscle memory.
But the accept loop for multiple clients required thinking. How do I structure this cleanly?
I went back to my architecture sketch. Poll array here. Client linked list here. Separate functions for adding clients, removing clients, finding clients by socket descriptor.
I created client.c and client.h for client management. pfds.c and pfds.h for poll descriptor utilities. main.c would just orchestrate. Accept connections, read data, route messages.
By 3pm I had a working echo server. Two telnet sessions connected, both echoing back their own messages. Not exciting, but clean.
The difference from Week 3 was immediate. No tangled dependencies. No "wait, where should this function go?" Everything had a place because I'd planned the places.
I tested with three clients. All echoing. Disconnected one. The others kept working. Reconnected. It worked.
Solid foundation. Tomorrow: real functionality.
Day 18: Making It Real
Wednesday I built the chat features.
First challenge: usernames. Where do they get stored? In the client struct. When do they get set? On first message after connection. What if they don't send a username? Default to "user_1", "user_2" based on order.
I added a username field to the client struct. Added a get_client_by_name() function. Modified the broadcast function to prepend usernames to messages.
Around 2:30pm, I tested it. Opened two telnet sessions. First one sent "hello". Second one's terminal showed "user_1: hello".
Not quite. I wanted them to set their own usernames.
I modified the flow: first message from a client gets treated as their username. Subsequent messages are chat messages.
3pm. Tested again. First client types "ethan", second types "bobby". Then ethan types "hello guys!"
Bobby's terminal showed "ethan: hello guys!"
That felt real.
Not copying from a tutorial. Not following a pattern from Beej. I was designing a system, making decisions about how it should work, then implementing those decisions.
The problems that came up were interesting. How do I parse messages to detect commands versus normal messages? If someone types /users, that should trigger a command, not broadcast to everyone. Where does that logic go?
I added a message parser. Check first character. If it's '/', parse as command. Otherwise, parse as message. Simple, but it required thinking about the architecture.
Parsing in C is unforgiving. One buffer overflow and you get a segfault. One missing null terminator and you're printing garbage. I was careful. Used strncpy instead of strcpy. Checked buffer sizes before writing. Each decision deliberate instead of improvised.
By Wednesday evening: working chat server with usernames and broadcast messaging. Clean code. No tangled logic.
I looked at the file structure. main.c, client.c, client.h, pfds.c, pfds.h. Each file under 200 lines. Each function doing one thing.
Week 3's server was a single 400-line file with functions doing three things each. This was different.
Day 19: Shipped
Thursday morning I had three things left: direct messaging, commands, disconnect handling.
Direct messaging first. The user types @bobby hey there and only bobby sees it.
Parse for '@'. If present, extract the username that follows. Look up that client. Send only to them.
The parsing was tricky. Find the '@', scan until whitespace, that's the username. What if they type @bobby with no message? Handle that edge case. What if the username doesn't exist? Send an error back to the sender.
First attempt: the username parsing included the '@' symbol in the lookup. Messages were addressed to '@bobby' instead of 'bobby', so the lookup failed every time. Took me fifteen minutes staring at the output before I realized I needed to skip the first character when extracting the name. Simple fix, but that's C. One off-by-one error and nothing works.
I worked through the other edge cases. Test: @bobby hey. Bobby receives it. Test: @nobody hey. Sender gets "User not found." Test: @bobby with no message. Sender gets "Message cannot be empty."
Commands next. /help, /users, /myname.
I added a command handler. Check the command, call the appropriate function. /users loops through clients and sends back a list. /help sends available commands. /myname sends back their current username.
Each one took 10-15 minutes to implement and test.
Disconnect handling last. When poll() detects a closed connection, remove that client from the list and from the poll descriptors. I'd already written remove_client() and remove_from_pfds() on Tuesday. Just needed to call them in the right place.
4:22pm. Eight minutes left in my session.
I opened three telnet windows. Connected all three. Set usernames: ethan, bobby, sarah.
Tested everything. Broadcasts worked. Direct messages went through. Commands responded. Disconnections handled cleanly.
Everything worked.
I felt satisfaction. Real satisfaction.
Not relief that it was over. Not exhausted disconnection like Week 3's Friday. Just clean satisfaction from building something that works, from solving problems I found interesting.
Week 3 was walking through fog. Unclear where I was going, uncertain if I'd get there, tired of trying to see through it.
Week 4 was different. Clear direction, clean problems, engaged the whole way.
Day 20: The Finish
Friday I documented everything.
README explaining how to compile, how to run the server, how to connect clients. Available commands. Architecture overview showing the separation between client management and poll handling.
I added comments to the utility files. Not every line. Just the non-obvious parts. Why I'm using strncpy here. Why the poll timeout is set to zero. Why clients are stored in a linked list instead of an array.
Writing the README revealed one gap in my understanding. I'd been using poll() with a timeout of zero, but I couldn't clearly explain why. I looked it up: zero timeout means non-blocking. It checks for ready sockets and returns immediately, doesn't wait.
I updated the README with that explanation.
Documentation isn't cleanup. It's the final act of ownership. It's what turns working code into something someone else—or future me—can actually use.
That's when I realized: I haven't shipped something until I can explain how it works.
What Week 4 Proved
Week 3: No plan, tangled architecture, tedium, incomplete.
Week 4: Clear plan, clean architecture, engagement, shipped.
Planning helped. But it wasn't just planning.
It was scope. Week 3's reliable UDP server required learning too many concepts simultaneously. Bitmaps for ACK tracking, byte shifting for packet headers, sequence numbers, retransmission timers, all while trying to build a working server. The project got lost in the learning.
Week 4's chat server pushed me, but stayed within reach. I could focus on architecture decisions and problem-solving instead of constantly learning fundamentals I didn't have.
It was also confidence. By Day 19, when I realized I'd built a working server without constantly referencing Beej or tutorials, something clicked.
I can build this. Not just understand it. Build it.
The AI Shift
I barely used AI this week.
When I did, it was like Stack Overflow. Quick questions: "Why is this buffer showing artifacts from previous messages?" "How do telnet line endings work?" Get the answer, understand the problem, implement the fix myself.
I stopped using AI as a crutch and started using it as a consultant. I'm no longer asking it to build the car. I'm asking it why the engine is knocking.
The relationship changed. Week 1 through 3, AI helped me get things working. Week 4, AI helped me get things working better. That matters.
Physical Discipline
Over the weekend, I joined the rowing team.
6am to 8am, six days a week. First practice was Wednesday morning.
I joined to get outside early. To connect with people. To start my day having already done something hard before most people are awake.
I'm objectively more tired. Classes are rougher. I almost fell asleep in discrete math twice this week. My body's still adjusting to 5:30am wakeups.
But I'm less tired mentally. I feel more present during the day. More energized in a way that's hard to explain.
I haven't had a morning yet where I dread practice. That's still riding on newness and excitement. The real test comes in February.
Month 1 Complete
Twenty days straight. Zero missed.
It doesn't feel like much time. But it's more than I've ever given anything on my own conviction.
Week 2 felt amazing because I broke through the initial resistance. Now it's normal, and that feels even better.
The impact of habit surprised me most. Making this routine—actually routine, not aspirationally routine—changed everything.
It's not a decision anymore whether I show up. It's just what happens at 2pm.
The hardest part of doing anything isn't starting. It's continuing. Habits solve that problem.
I'm most proud of my consistency. Not the projects I built or the concepts I learned. The fact that every single day for 2.5 hours, I was entirely focused on one thing.
No phone. No multitasking. No 'productive' task-switching.
Just the work.
That was hard. But it got easier every day. And now it's just how I work.
What Month 1 Built
Week 1: TCP fundamentals. Copying from Beej, learning patterns.
I spent hours reading code I didn't understand, typing it out, seeing if it worked. Around Day 3, commenting Beej's code line by line. Explaining to myself what bind() actually does, why listen() comes before accept(), what the backlog parameter means.
It was tedious. But it built the foundation. By Day 5, I could write a basic TCP server from memory.
Week 2: UDP basics. Packet loss, pushing through exhaustion.
The week UDP clicked and almost broke me. Building a server that randomly drops packets to simulate real network conditions. Understanding why games use UDP instead of TCP.
3:01pm on Friday. Five hours of lecture before session. Completely drained. Opened Claude: "I feel so tired and bored right now, this is a lot."
But I stayed. Finished the session. That was the week I learned continuing is harder than starting.
Week 3: Reliable UDP. Incomplete, but taught me planning matters.
The week everything got tangled. Trying to build reliability on top of UDP. ACK bitmaps, sequence numbers, retransmission logic, all without planning the architecture first.
By Friday I was disconnected. Going through the motions. The code worked partially, but I didn't care anymore. Just wanted it to be over.
That was the low point. But it taught me the most important lesson: plan before you code.
Week 4: Chat server. Complete, clean architecture, shipped.
The week I proved Week 3's lesson. Planned the architecture on Monday. Built cleanly Tuesday through Thursday. Shipped something complete on Friday.
Engaged the whole way. Problems were interesting, not tedious. 4:22pm Thursday, everything working, genuine satisfaction.
I went from copying code I didn't understand to building systems from understanding.
The technical fundamentals are there now. TCP/UDP at a low level. Poll-based I/O. Client/server architecture. Protocol design. When to use which approach and why.
But the bigger lessons were about process. I can push through exhaustion. I can push through disconnection. Planning prevents tedium. Using AI strategically works. Showing up daily compounds.
Habits matter more than motivation. Intentionality makes time feel abundant, not scarce. Focus makes work engaging, not draining.
By filling my time with deliberate things (rowing, coding, classes, relationships), I'm less rushed and more present than when I had 'more free time.'
I don't need more free time. I need more intentional time.
Month 2 Starts Monday
Month 1 was fundamentals. Learning how things work. Proving I could show up.
Month 2 is building things that solve problems. Using what I know to create tools. Moving toward game networking.
Week 5's project: simple multiplayer movement server. Multiple clients controlling players, seeing each other move in real-time. State synchronization. Client-side interpolation.
I'm excited. And careful not to overload myself. Week 3 taught me what happens when scope exceeds foundation.
I want to maintain realism. Build things that push my limits without exceeding them. Take time to learn deeply instead of rushing to finish.
Get comfortable struggling without looking for shortcuts.
Twenty days down. One month complete.
Same table. 2pm. Day 21.
Related Posts
Week 5: From Copying to Creating
Making something beautiful out of simple fundamentals
Week 3: When Planning Matters
Building a reliable UDP protocol. Discovering why architecture matters. Friday's disconnection.
Week 1: Proving I Can Stick With Hard Things
Five days of deep work learning TCP networking from scratch. What it takes to become more than a professional beginner.