-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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.
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 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 is the working program. I added three modules and refactored pong.c around the state machine.
New modules:
-
court.hreplaced the four hard-coded edge constants with astruct courtandconfigure_side(), so the same engine renders the left or the right court. -
net.c/net.hwrapped the socket calls:make_server_socketfor socket/bind/listen,wait_for_clientfor accept, andconnect_to_serverfor socket/connect. -
protocol.c/protocol.himplemented SPPBTP:send_*helpers that write CRLF lines withdprintf, andrecv_msgthat reads one line and fills a tagged message struct.
Changes to existing code:
-
paddle_init()took a column argument instead of a hard-coded70, so the paddle can sit on either side. -
main()read the argument count to start as server or client and run theHELO/NAME/SERVhandshake. - The far-edge branch in
bounce_or_lose()stopped reversing the ball and started sendingBALLplus enteringWAIT. - The miss branch stopped re-serving locally and started sending
MISSplus enteringWAIT. -
run_play_loop()andrun_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.
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.