-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
netPong runs as one binary in two roles. The server takes a port; the client takes a host and a port. The argument count in main() decides the role. After the connection is up, both processes run the same loop and the same physics. The difference between them is which side of the court each defends, which court.h captures in a struct court.
flowchart TD
main["pong.c (main loop and game state machine)"]
paddle["paddle.c (paddle module)"]
net["net.c (BSD socket setup)"]
proto["protocol.c (SPPBTP wire format)"]
court["court.h (per-side court geometry)"]
main -->|moves and tests| paddle
main -->|opens connection| net
main -->|sends and parses lines| proto
main -->|configures geometry| court
proto -->|writes to / reads from| net
-
pong.cholds the main loop, the game state machine, the ball physics inbounce_or_lose(), and the curses drawing. It is the only module that knows about game rules. -
paddle.cis a small object-style module. It owns one static paddle, draws it, moves it within bounds, and answerspaddle_contact(y, x).paddle_init()takes a column argument so the same code works on the left or the right. -
net.cwraps the BSD socket calls.make_server_socket()does socket/bind/listen,wait_for_client()does accept, andconnect_to_server()does socket/connect. Nothing above this layer touchessockaddr_in. -
protocol.cis the wire format. Thesend_*helpers write CRLF-terminated SPPBTP lines withdprintf, andrecv_msg()reads one line and fills a taggedstruct sppbtp_msg. -
court.hdeclares the geometry struct andconfigure_side(). I pulled the four hard-coded edge constants out of the single-player version and made them runtime fields, so one engine renders either court.
I kept the protocol layer above the socket layer. protocol.c writes to a plain integer fd and reads from a FILE*; it never calls a socket function. That made it possible to test the protocol with telnet and dprintf without involving game logic.
flowchart LR
subgraph ClientProc["Client process (left court)"]
C_main["pong.c main loop"]
C_pad["paddle.c"]
C_proto["protocol.c"]
C_net["net.c"]
C_main --> C_pad
C_main --> C_proto
C_proto --> C_net
end
subgraph ServerProc["Server process (right court)"]
S_net["net.c"]
S_proto["protocol.c"]
S_pad["paddle.c"]
S_main["pong.c main loop"]
S_main --> S_pad
S_main --> S_proto
S_proto --> S_net
end
C_net <-->|TCP, SPPBTP text lines| S_net
The server defends the right court (SIDE_RIGHT) and the client defends the left (SIDE_LEFT). Each process draws its own paddle and the net wall on its far edge. The connection is a single TCP stream. After accept() the server closes its listening socket, so one launch serves one game.
The connection is client/server. The gameplay is peer-symmetric: once HELO/NAME/SERV finish, neither side is privileged. Each side simulates the ball on its own court and trusts the BALL line the other side sends.
stateDiagram-v2
[*] --> INTRO
INTRO --> PLAY: client serves first ball
INTRO --> WAIT: server waits for client serve
PLAY --> WAIT: ball crosses net (send BALL)
PLAY --> WAIT: paddle missed (send MISS)
WAIT --> PLAY: receive BALL
WAIT --> PLAY: receive MISS, balls_left > 0 (re-serve)
PLAY --> FINISHED: press Q (send QUIT)
WAIT --> FINISHED: receive DONE (send QUIT)
WAIT --> FINISHED: receive QUIT
WAIT --> FINISHED: last ball missed (send DONE)
FINISHED --> [*]
pong.c defines enum game_state { STATE_INTRO, STATE_PLAY, STATE_WAIT, STATE_FINISHED }. The whole design rests on one rule: only the process in PLAY owns the ball and runs the physics ticker. I implemented that with enter_play() and enter_wait(), which flip the state and start or stop the interval timer through set_ticker().
-
INTRO runs the handshake. The server sends
HELO, readsNAME, sendsSERV, then drops intoWAIT. The client readsHELO, sendsNAME, readsSERV, then serves and entersPLAY. -
PLAY runs
run_play_loop(). ASIGALRMevery 50 ms callsbounce_or_lose()to move the ball. The loop readsk/j/Qfrom the keyboard. It also peeks the socket with a zero-timeoutselect()so it can catch an opponentQUITwithout blocking. -
WAIT runs
run_wait_loop(). The ticker is off. The loop blocks onrecv_msg()and dispatches:BALLre-entersPLAY,MISSeither re-serves or sendsDONE,DONEsendsQUIT, andQUITfinishes. - FINISHED breaks the outer loop, tears down curses, closes the socket, and prints the final score.
In the single-player baseline the interval timer ran the whole time. For netPong that wastes signals, because for half the game the ball is on the other court. I made set_ticker(TICK_MSECS) start it on entry to PLAY and set_ticker(0) stop it on entry to WAIT. The on_alarm() handler also guards on state != STATE_PLAY and returns early, so a stray late signal cannot move a ball that the local process no longer owns.
I read protocol lines with fgets, which wants a FILE*, but I write with dprintf on the raw fd. To keep both safe I call fdopen(dup(sock_fd), "r"). The duplicate gives the read stream its own file descriptor, so closing the FILE* at shutdown does not close the fd I still write through, and the two directions never share a buffer. This follows the socklib pattern from Molay's Understanding Unix/Linux Programming, chapter 12.