Skip to content

Commit dd8c23f

Browse files
authored
Merge pull request #285 from spoukke/chore/spoukke/update-polite-ping-pong
update polite ping pong with new protocol
2 parents ce0f0b3 + c575bc1 commit dd8c23f

2 files changed

Lines changed: 94 additions & 163 deletions

File tree

examples/eventBased/politePingPong/politeAlice.py

Lines changed: 57 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,35 @@
11
"""
22
Polite Ping-Pong — Alice (client).
33
4-
Alice's behaviour is defined as a finite state machine. She always
5-
initiates the exchange by sending "ping" before entering the event loop.
6-
The event loop then reads one message at a time, looks up the
7-
(current_state, message) pair in the dispatch table, and calls the
8-
corresponding handler. The handler performs an action and returns the
9-
next state.
4+
Alice waits for Bob's "READY" signal before sending each "PING". After
5+
NUM_ROUNDS pings she sends "BYE" instead, ending the exchange.
106
117
Alice's state diagram
128
---------------------
139
14-
┌─ (connect) ─────────────────────────────┐
15-
│ send "ping" │
16-
▼ │
17-
──────────────► WAITING_PONG (start)
18-
19-
recv "pong"
20-
send "thank you"
21-
22-
23-
WAITING_YOURE_WELCOME
24-
25-
recv "you're welcome"
26-
27-
28-
DONE
29-
30-
As a transition table (this maps directly to ALICE_DISPATCH below):
31-
32-
Current state │ Message received │ Action │ Next state
33-
───────────────────────┼────────────────────┼──────────────────┼──────────────────────
34-
WAITING_PONG │ "pong" │ send "thank you" │ WAITING_YOURE_WELCOME
35-
WAITING_YOURE_WELCOME │ "you're welcome" │ (none) │ DONE
36-
37-
Any (state, message) pair NOT in the table is rejected with a warning;
38-
the state does not change and the loop continues.
39-
40-
Note: the initial "ping" is sent before the loop starts — it is not a
41-
state transition but simply Alice's opening move as the initiator.
10+
┌─ (connect) ──────────────────────────────────────────┐
11+
│ │
12+
▼ │
13+
WAITING_FOR_READY ──[recv "READY"]──► WAITING_FOR_PONG (start)
14+
15+
recv "PONG"
16+
17+
18+
WAITING_FOR_READY (next round)
19+
... after NUM_ROUNDS ...
20+
21+
recv "READY" (last)
22+
send "BYE"
23+
24+
DONE
25+
26+
Transition table:
27+
28+
Current state │ Message │ Action │ Next state
29+
────────────────────┼──────────┼─────────────────────────────┼──────────────────
30+
WAITING_FOR_READY │ "READY" │ send "PING" (or "BYE") │ WAITING_FOR_PONG
31+
│ │ │ (or DONE)
32+
WAITING_FOR_PONG │ "PONG" │ — │ WAITING_FOR_READY
4233
"""
4334
from asyncio import StreamReader, StreamWriter
4435
from pathlib import Path
@@ -49,92 +40,70 @@
4940
from simulaqron.settings.network_config import NodeConfigType
5041

5142

43+
NUM_ROUNDS = 5
44+
5245
# ── States ───────────────────────────────────────────────────────────────────
5346

54-
STATE_WAITING_PONG = "WAITING_PONG" # noqa: E221
55-
STATE_WAITING_YOURE_WELCOME = "WAITING_YOURE_WELCOME"
56-
STATE_DONE = "DONE" # noqa: E221
47+
STATE_WAITING_FOR_READY = "WAITING_FOR_READY"
48+
STATE_WAITING_FOR_PONG = "WAITING_FOR_PONG" # noqa: E221
49+
STATE_DONE = "DONE" # noqa: E221
5750

51+
# ── Mutable round counter ─────────────────────────────────────────────────────
5852

59-
# ── Handlers ─────────────────────────────────────────────────────────────────
53+
rounds_left = NUM_ROUNDS
6054

61-
async def handle_pong(writer: StreamWriter) -> str:
62-
"""
63-
Transition: WAITING_PONG ──[recv "pong"]──► WAITING_YOURE_WELCOME
6455

65-
Alice receives a pong and politely says thank you.
66-
"""
67-
reply = "thank you"
68-
print(f"Alice: sending '{reply}'")
69-
writer.write(reply.encode("utf-8"))
70-
await writer.drain()
71-
return STATE_WAITING_YOURE_WELCOME
56+
# ── Handlers ─────────────────────────────────────────────────────────────────
7257

58+
async def handle_ready(writer: StreamWriter) -> str:
59+
global rounds_left
60+
if rounds_left > 0:
61+
rounds_left -= 1
62+
round_num = NUM_ROUNDS - rounds_left
63+
writer.write(b"PING\n")
64+
print(f"Alice [round {round_num}]: sent PING")
65+
return STATE_WAITING_FOR_PONG
66+
else:
67+
writer.write(b"BYE\n")
68+
print("Alice: sent BYE, done.")
69+
return STATE_DONE
7370

74-
async def handle_youre_welcome(writer: StreamWriter) -> str:
75-
"""
76-
Transition: WAITING_YOURE_WELCOME ──[recv "you're welcome"]──► DONE
7771

78-
Alice receives the final courtesy and the exchange is complete.
79-
No reply is needed.
80-
"""
81-
print("Alice: received 'you're welcome' — exchange complete.")
82-
return STATE_DONE
72+
async def handle_pong(writer: StreamWriter) -> str:
73+
print("Alice: received PONG")
74+
return STATE_WAITING_FOR_READY
8375

8476

8577
# ── Dispatch table ────────────────────────────────────────────────────────────
86-
# Maps (current_state, message) → handler.
87-
# This table IS the state machine: every valid transition is listed here,
88-
# and anything not listed is automatically an invalid transition.
8978

9079
ALICE_DISPATCH = {
91-
(STATE_WAITING_PONG, "pong"): handle_pong, # noqa: E241
92-
(STATE_WAITING_YOURE_WELCOME, "you're welcome"): handle_youre_welcome,
80+
(STATE_WAITING_FOR_READY, "READY"): handle_ready,
81+
(STATE_WAITING_FOR_PONG, "PONG"): handle_pong, # noqa: E241
9382
}
9483

9584

96-
# ── Event loop ────────────────────────────────────────────────────────────────
97-
98-
async def run_alice(reader: StreamReader, writer: StreamWriter):
99-
"""
100-
Alice's event loop.
85+
# ── Event loop ───────────────────────────────────────────────────────────────
10186

102-
Alice initiates by sending "ping", then enters the state machine loop:
103-
1. Read the next message from Bob.
104-
2. Look up (current_state, message) in ALICE_DISPATCH.
105-
3. If found, call the handler and move to the returned next state.
106-
4. If not found, log a warning and stay in the current state.
107-
Loop exits when the state reaches STATE_DONE or the connection drops.
108-
"""
109-
# Initial action: Alice always opens the exchange with a ping.
110-
# This happens before the loop — it is not a state transition.
111-
opening = "ping"
112-
print(f"Alice: sending '{opening}'")
113-
writer.write(opening.encode("utf-8"))
114-
await writer.drain()
87+
async def run_alice(reader: StreamReader, writer: StreamWriter) -> None:
88+
global rounds_left
89+
rounds_left = NUM_ROUNDS
11590

116-
state = STATE_WAITING_PONG
91+
state = STATE_WAITING_FOR_READY
11792

11893
while state != STATE_DONE:
119-
# 1. Wait for the next event (message from Bob)
120-
data = await reader.read(255)
94+
data = await reader.readline()
12195
if not data:
12296
print(f"Alice [{state}]: connection dropped unexpectedly.")
12397
break
12498
msg = data.decode("utf-8")
12599
print(f"Alice [{state}]: received '{msg}'")
126100

127-
# 2. Look up the transition
128101
handler = ALICE_DISPATCH.get((state, msg))
129102

130-
# 3a. Invalid transition — warn and stay in current state
131103
if handler is None:
132-
print(
133-
f"Alice [{state}]: no transition for message '{msg}' — ignoring."
134-
)
104+
print(f"Alice [{state}]: no transition for '{msg}' — ignoring.")
135105
continue
136106

137-
# 3b. Valid transition — execute handler, advance state
138107
state = await handler(writer)
139108

140109
print(f"Alice: event loop finished (final state: {state}).")
Lines changed: 37 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,27 @@
11
"""
22
Polite Ping-Pong — Bob (server).
33
4-
Bob's behaviour is defined as a finite state machine. The event loop
5-
reads one message at a time, looks up the (current_state, message) pair
6-
in the dispatch table, and calls the corresponding handler. The handler
7-
performs an action (typically sending a reply) and returns the next state.
4+
Bob sends "READY" immediately on connection and after each "PONG", signalling
5+
to Alice that he is ready for the next round. He handles "PING" (reply PONG
6+
then READY) and "BYE" (close).
87
98
Bob's state diagram
109
-------------------
1110
12-
recv "ping" send "pong"
13-
WAITING_PING ─────────────────────────────────────► WAITING_THANKS
14-
15-
recv "thank you"
16-
send "you're welcome"
17-
18-
19-
DONE
11+
┌─ (connect) ──────────────────────────────────────────┐
12+
│ send "READY" │
13+
▼ │
14+
WAITING_FOR_PING_OR_BYE (start)
15+
16+
recv "PING" → send "PONG", send "READY" → (stay)
17+
recv "BYE" → DONE
2018
21-
As a transition table (this maps directly to BOB_DISPATCH below):
19+
Transition table:
2220
23-
Current state │ Message received │ Action │ Next state
24-
─────────────────┼──────────────────┼────────────────────────┼──────────────────
25-
WAITING_PING │ "ping" │ send "pong" │ WAITING_THANKS
26-
WAITING_THANKS │ "thank you" │ send "you're welcome" │ DONE
27-
28-
Any (state, message) pair NOT in the table is rejected with a warning;
29-
the state does not change and the loop continues.
21+
Current state │ Message │ Action │ Next state
22+
────────────────────────┼─────────┼──────────────────────────┼───────────────────────
23+
WAITING_FOR_PING_OR_BYE │ "PING" │ send "PONG", send "READY"│ WAITING_FOR_PING_OR_BYE
24+
WAITING_FOR_PING_OR_BYE │ "BYE" │ — │ DONE
3025
"""
3126
from asyncio import StreamReader, StreamWriter
3227
from pathlib import Path
@@ -38,93 +33,60 @@
3833

3934

4035
# ── States ───────────────────────────────────────────────────────────────────
41-
# Each constant names a state in Bob's state machine.
42-
# The string value is used in log output so keep it human-readable.
4336

44-
STATE_WAITING_PING = "WAITING_PING" # noqa: E221
45-
STATE_WAITING_THANKS = "WAITING_THANKS"
46-
STATE_DONE = "DONE" # noqa: E221
37+
STATE_WAITING_FOR_PING_OR_BYE = "WAITING_FOR_PING_OR_BYE"
38+
STATE_DONE = "DONE" # noqa: E221
4739

4840

4941
# ── Handlers ─────────────────────────────────────────────────────────────────
50-
# One handler per valid transition. A handler receives the writer (to send
51-
# a reply) and returns the next state. It does NOT need to validate the
52-
# current state — that is guaranteed by the dispatch table.
5342

5443
async def handle_ping(writer: StreamWriter) -> str:
55-
"""
56-
Transition: WAITING_PING ──[recv "ping"]──► WAITING_THANKS
57-
58-
Bob receives a ping and replies with a pong.
59-
"""
60-
reply = "pong"
61-
print(f"Bob: sending '{reply}'", flush=True)
62-
writer.write(reply.encode("utf-8"))
63-
await writer.drain()
64-
return STATE_WAITING_THANKS
65-
66-
67-
async def handle_thank_you(writer: StreamWriter) -> str:
68-
"""
69-
Transition: WAITING_THANKS ──[recv "thank you"]──► DONE
70-
71-
Bob receives a thank you and replies politely before finishing.
72-
"""
73-
reply = "you're welcome"
74-
print(f"Bob: sending '{reply}'", flush=True)
75-
writer.write(reply.encode("utf-8"))
76-
await writer.drain()
44+
writer.write(b"PONG\n")
45+
print("Bob: sent PONG", flush=True)
46+
writer.write(b"READY\n")
47+
print("Bob: sent READY", flush=True)
48+
return STATE_WAITING_FOR_PING_OR_BYE
49+
50+
51+
async def handle_bye(writer: StreamWriter) -> str:
52+
print("Bob: received BYE, closing.", flush=True)
7753
return STATE_DONE
7854

7955

8056
# ── Dispatch table ────────────────────────────────────────────────────────────
81-
# Maps (current_state, message) → handler.
82-
# This table IS the state machine: every valid transition is listed here,
83-
# and anything not listed is automatically an invalid transition.
8457

8558
BOB_DISPATCH = {
86-
(STATE_WAITING_PING, "ping"): handle_ping, # noqa: E241
87-
(STATE_WAITING_THANKS, "thank you"): handle_thank_you,
59+
(STATE_WAITING_FOR_PING_OR_BYE, "PING"): handle_ping,
60+
(STATE_WAITING_FOR_PING_OR_BYE, "BYE"): handle_bye, # noqa: E241
8861
}
8962

9063

9164
# ── Event loop ────────────────────────────────────────────────────────────────
9265

93-
async def run_bob(reader: StreamReader, writer: StreamWriter):
94-
"""
95-
Bob's event loop.
96-
97-
Repeatedly:
98-
1. Read the next message from Alice.
99-
2. Look up (current_state, message) in BOB_DISPATCH.
100-
3. If found, call the handler and move to the returned next state.
101-
4. If not found, log a warning and stay in the current state.
102-
Loop exits when the state reaches STATE_DONE or the connection drops.
103-
"""
66+
async def run_bob(reader: StreamReader, writer: StreamWriter) -> None:
10467
print("Bob: Alice connected.", flush=True)
105-
state = STATE_WAITING_PING
68+
69+
writer.write(b"READY\n")
70+
print("Bob: sent READY", flush=True)
71+
state = STATE_WAITING_FOR_PING_OR_BYE
10672

10773
while state != STATE_DONE:
108-
# 1. Wait for the next event (message from Alice)
109-
data = await reader.read(255)
74+
data = await reader.readline()
11075
if not data:
11176
print(f"Bob [{state}]: connection dropped unexpectedly.", flush=True)
11277
break
113-
msg = data.decode("utf-8")
78+
msg = data.decode().strip()
11479
print(f"Bob [{state}]: received '{msg}'", flush=True)
11580

116-
# 2. Look up the transition
11781
handler = BOB_DISPATCH.get((state, msg))
11882

119-
# 3a. Invalid transition — warn and stay in current state
12083
if handler is None:
12184
print(
122-
f"Bob [{state}]: no transition for message '{msg}' — ignoring.",
85+
f"Bob [{state}]: no transition for '{msg}' — ignoring.",
12386
flush=True,
12487
)
12588
continue
12689

127-
# 3b. Valid transition — execute handler, advance state
12890
state = await handler(writer)
12991

13092
print(f"Bob: event loop finished (final state: {state}).", flush=True)
@@ -141,5 +103,5 @@ async def run_bob(reader: StreamReader, writer: StreamWriter):
141103
server = SimulaQronClassicalServer(sockets_config, "Bob")
142104
server.register_client_handler(run_bob)
143105

144-
print("Bob: starting server...")
106+
print("Bob: starting server...", flush=True)
145107
server.start_serving()

0 commit comments

Comments
 (0)