Skip to content

Development Journey

BrandonRobare edited this page Jun 2, 2026 · 1 revision

Development Journey

netPong went from a single-player game to a networked one in three phases. Each phase has a report in docs/. This page walks through how the design changed and why.

Starting point: single-player Pong

The baseline was the course Pong assignment: one curses process, a fixed court, a paddle hard-coded on the right, and a ball driven by an interval timer. The timer fired a SIGALRM, the handler called bounce_or_lose(), and the ball bounced off all four edges. A miss decremented a ball counter and immediately served a new ball locally. That code already separated vertical motion, horizontal motion, paddle motion, and serving, which made it a good base to split rather than rewrite.

Phase 1: analysis

In Phase 1 I read the baseline and worked out what had to change. I found three assumptions baked into the single-player code:

  • the paddle is always on the right,
  • the far edge always bounces the ball,
  • the same process always serves after a miss.

The plan was to replace each one with runtime state. The fixed court constants would become fields in a court structure so one engine could draw either side. The far-edge bounce would become a network handoff. The local re-serve after a miss would become a message to the other player. Phase 1 also decided that the local physics ticker only needs to run while the local process owns the ball, which set up the play/wait split that came later. The full analysis is in docs/phase1/Phase1_Report.md.

Phase 2: protocol and state design

Phase 2 turned the analysis into a concrete design around two states inside the RFC's PLAYBALL phase. The key decision was ownership: at any moment exactly one process owns the paddle input, the timer, the ball, and the next serve. I named the states PLAY and WAIT.

In PLAY the local machine simulates the ball, bounces it off its walls and paddle, and either sends BALL when the ball crosses the net or sends MISS when the player misses. In WAIT the local machine stops the ticker, ignores the keyboard, and blocks on the socket for BALL, MISS, DONE, or QUIT. I also worked out the BALL field set here: net position, the two time-to-move counters, the vertical direction, and an optional character were enough for the other side to continue the ball. Scoring became symmetric, with both sides stepping a shared balls_left down together. The design document is in docs/phase2/Phase2_Report.md.

Phase 3: the C implementation

Phase 3 is the working program. I added three modules and refactored pong.c around the state machine.

New modules:

  • court.h replaced the four hard-coded edge constants with a struct court and configure_side(), so the same engine renders the left or the right court.
  • net.c / net.h wrapped the socket calls: make_server_socket for socket/bind/listen, wait_for_client for accept, and connect_to_server for socket/connect.
  • protocol.c / protocol.h implemented SPPBTP: send_* helpers that write CRLF lines with dprintf, and recv_msg that reads one line and fills a tagged message struct.

Changes to existing code:

  • paddle_init() took a column argument instead of a hard-coded 70, so the paddle can sit on either side.
  • main() read the argument count to start as server or client and run the HELO/NAME/SERV handshake.
  • The far-edge branch in bounce_or_lose() stopped reversing the ball and started sending BALL plus entering WAIT.
  • The miss branch stopped re-serving locally and started sending MISS plus entering WAIT.
  • run_play_loop() and run_wait_loop() replaced the single unconditional loop.

The Phase 3 report in docs/phase3/Phase3_Report.md records the build verification, a telnet conformance test, and the limitations.

Things I learned

Two details stood out. First, the server opens the conversation but the client serves the first ball, which is a role reversal from the usual "connector acts first" pattern. Second, pausing the physics needed no new mechanism: the existing set_ticker() already stopped the timer when passed zero, so WAIT reused it directly.

Clone this wiki locally