Skip to content

Commit d2116b3

Browse files
Merge pull request #286 from spoukke/fix/spoukke/ping-pong-state-machines
Fix/spoukke/ping pong state machines
2 parents 8bd1fff + 6ecc6cf commit d2116b3

4 files changed

Lines changed: 282 additions & 150 deletions

File tree

examples/eventBased/pingPong/pingpongAlice.py

Lines changed: 85 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,44 @@
11
"""
2-
Ping-Pong client (Alice).
2+
Ping-Pong Alice (client).
33
4-
Alice connects to Bob and sends a sequence of messages.
5-
After each message she waits for Bob's reply and prints it.
6-
7-
Try changing the messages list to see how Bob reacts!
4+
Alice connects to Bob and they exchange PING / PONG messages for NUM_ROUNDS
5+
rounds. Both sides know NUM_ROUNDS, so no BYE is needed — the connection
6+
simply closes after the last round.
87
98
This is a purely classical example — no quantum operations.
10-
It demonstrates the event-based programming pattern that we will
11-
later extend with quantum operations (teleportation, etc.).
9+
It demonstrates the event-based state-machine pattern used throughout
10+
SimulaQron examples.
11+
12+
Alice's state diagram
13+
---------------------
14+
15+
┌─ (connect) ──────────────────────────────────────┐
16+
│ │
17+
▼ │
18+
IDLE ──[send "PING"]──► PLAYING (start)
19+
20+
recv "PONG"
21+
22+
23+
IDLE (next round)
24+
... after NUM_ROUNDS ...
25+
26+
recv "PONG" (last)
27+
28+
DONE
29+
30+
Transition table:
31+
32+
State │ Event │ Action │ Next state
33+
─────────┼──────────────┼──────────────┼─────────────
34+
IDLE │ (entry) │ send "PING" │ PLAYING
35+
PLAYING │ recv "PONG" │ — │ IDLE
36+
PLAYING │ recv "PONG" │ (last round) │ DONE
37+
38+
IDLE → PLAYING is an *entry action*: Alice sends PING immediately on
39+
entering IDLE, before waiting for the next message.
1240
"""
41+
1342
from asyncio import StreamReader, StreamWriter
1443
from pathlib import Path
1544

@@ -19,27 +48,60 @@
1948
from simulaqron.settings.network_config import NodeConfigType
2049

2150

22-
async def run_alice(reader: StreamReader, writer: StreamWriter):
23-
"""
24-
Alice sends a sequence of messages and prints Bob's replies.
51+
NUM_ROUNDS = 5
52+
53+
# ── States ───────────────────────────────────────────────────────────────────
54+
55+
STATE_IDLE = "IDLE"
56+
STATE_PLAYING = "PLAYING"
57+
STATE_DONE = "DONE" # noqa: E221
58+
59+
60+
# ── Event loop ───────────────────────────────────────────────────────────────
61+
62+
async def run_alice(reader: StreamReader, writer: StreamWriter) -> None:
63+
rounds_done = 0
64+
65+
async def handle_pong(_writer: StreamWriter) -> str:
66+
"""Transition: PLAYING ──[recv "PONG"]──► IDLE (or DONE)"""
67+
nonlocal rounds_done
68+
print(f"Alice [round {rounds_done}]: received PONG")
69+
if rounds_done < NUM_ROUNDS:
70+
return STATE_IDLE
71+
return STATE_DONE
72+
73+
dispatch = {
74+
(STATE_PLAYING, "PONG"): handle_pong,
75+
}
76+
77+
state = STATE_IDLE
78+
79+
while state != STATE_DONE:
80+
# Entry action: IDLE → send PING → PLAYING
81+
if state == STATE_IDLE:
82+
rounds_done += 1
83+
writer.write(b"PING\n")
84+
print(f"Alice [round {rounds_done}]: sent PING")
85+
state = STATE_PLAYING
86+
87+
data = await reader.readline()
88+
if not data:
89+
print(f"Alice [{state}]: connection dropped unexpectedly.")
90+
break
91+
msg = data.decode("utf-8")
92+
93+
handler = dispatch.get((state, msg))
2594

26-
Try changing this list to see what Bob does with different messages!
27-
"""
28-
messages = ["ping", "ping", "hello", "ping"]
95+
if handler is None:
96+
print(f"Alice [{state}]: no transition for '{msg}' — ignoring.")
97+
continue
2998

30-
for msg in messages:
31-
# Send a message to Bob
32-
print(f"Alice: sending '{msg}'")
33-
writer.write(msg.encode("utf-8"))
34-
await writer.drain()
99+
state = await handler(writer)
35100

36-
# Wait for Bob's reply
37-
reply_data = await reader.read(255)
38-
reply = reply_data.decode("utf-8")
39-
print(f"Alice: received '{reply}'")
101+
print(f"Alice: event loop finished (final state: {state}).")
40102

41-
print("Alice: done, disconnecting.")
42103

104+
# ── Entry point ───────────────────────────────────────────────────────────────
43105

44106
if __name__ == "__main__":
45107
# Load configuration files — paths are relative to this script's location

examples/eventBased/pingPong/pingpongBob.py

Lines changed: 89 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,44 @@
11
"""
2-
Ping-Pong server (Bob).
2+
Ping-Pong Bob (server).
33
4-
Bob listens for messages from Alice. For each message he receives:
5-
- If the message is "ping", he replies with "pong".
6-
- For anything else, he replies with "no way!".
7-
8-
Bob keeps listening until Alice disconnects.
4+
Bob listens for Alice's PINGs and replies with PONGs.
5+
Both sides know NUM_ROUNDS, so no BYE is needed — Bob stops after
6+
sending the last PONG and the connection closes naturally.
97
108
This is a purely classical example — no quantum operations.
11-
It demonstrates the event-based programming pattern that we will
12-
later extend with quantum operations (teleportation, etc.).
9+
It demonstrates the event-based state-machine pattern used throughout
10+
SimulaQron examples.
11+
12+
Bob's state diagram
13+
-------------------
14+
15+
┌─ (connect) ──────────────────────────────────────┐
16+
│ │
17+
▼ │
18+
IDLE ──[recv "PING"]──► PLAYING (start)
19+
20+
send "PONG"
21+
22+
23+
IDLE (next round)
24+
... after NUM_ROUNDS ...
25+
26+
recv "PING" (last)
27+
28+
DONE
29+
30+
Transition table:
31+
32+
State │ Event │ Action │ Next state
33+
─────────┼──────────────┼──────────────┼─────────────
34+
IDLE │ recv "PING" │ — │ PLAYING
35+
PLAYING │ (entry) │ send "PONG" │ IDLE
36+
PLAYING │ (entry) │ (last round) │ DONE
37+
38+
PLAYING → IDLE/DONE is an *entry action*: Bob sends PONG immediately
39+
on entering PLAYING, before waiting for the next message.
1340
"""
41+
1442
from asyncio import StreamReader, StreamWriter
1543
from pathlib import Path
1644

@@ -20,40 +48,68 @@
2048
from simulaqron.settings.network_config import NodeConfigType
2149

2250

23-
async def run_bob(reader: StreamReader, writer: StreamWriter):
24-
"""
25-
Bob's event loop.
51+
NUM_ROUNDS = 5
52+
53+
# ── States ───────────────────────────────────────────────────────────────────
54+
55+
STATE_IDLE = "IDLE"
56+
STATE_PLAYING = "PLAYING"
57+
STATE_DONE = "DONE" # noqa: E221
58+
59+
60+
# ── Handlers ─────────────────────────────────────────────────────────────────
61+
62+
async def handle_ping(_writer: StreamWriter) -> str:
63+
"""Transition: IDLE ──[recv "PING"]──► PLAYING"""
64+
print("Bob: received PING", flush=True)
65+
return STATE_PLAYING
2666

27-
Each iteration:
28-
1. Wait for a message from Alice
29-
2. Decide on a reply based on the message content
30-
3. Send the reply back
31-
"""
32-
print("Bob: Alice connected, waiting for messages...", flush=True)
3367

34-
while True:
35-
# Wait until Alice sends something
36-
data = await reader.read(255)
68+
# ── Dispatch table ────────────────────────────────────────────────────────────
3769

38-
# If we get empty data, Alice has disconnected
70+
BOB_DISPATCH = {
71+
(STATE_IDLE, "PING"): handle_ping,
72+
}
73+
74+
75+
# ── Event loop ────────────────────────────────────────────────────────────────
76+
77+
async def run_bob(reader: StreamReader, writer: StreamWriter) -> None:
78+
print("Bob: Alice connected.", flush=True)
79+
rounds_done = 0
80+
state = STATE_IDLE
81+
82+
while state != STATE_DONE:
83+
# Entry action: PLAYING → send PONG → IDLE (or DONE)
84+
if state == STATE_PLAYING:
85+
rounds_done += 1
86+
writer.write(b"PONG\n")
87+
print(f"Bob [round {rounds_done}]: sent PONG", flush=True)
88+
state = STATE_IDLE if rounds_done < NUM_ROUNDS else STATE_DONE
89+
continue
90+
91+
data = await reader.readline()
3992
if not data:
40-
print("Bob: Alice disconnected.", flush=True)
93+
print(f"Bob [{state}]: connection dropped unexpectedly.", flush=True)
4194
break
95+
msg = data.decode("utf-8")
96+
print(f"Bob [{state}]: received '{msg}'", flush=True)
97+
98+
handler = BOB_DISPATCH.get((state, msg))
99+
100+
if handler is None:
101+
print(
102+
f"Bob [{state}]: no transition for '{msg}' — ignoring.",
103+
flush=True,
104+
)
105+
continue
42106

43-
message = data.decode("utf-8")
44-
print(f"Bob: received '{message}'", flush=True)
107+
state = await handler(writer)
45108

46-
# Decide on a reply
47-
if message == "ping":
48-
reply = "pong"
49-
else:
50-
reply = "no way!"
109+
print(f"Bob: event loop finished (final state: {state}).", flush=True)
51110

52-
# Send the reply
53-
print(f"Bob: sending '{reply}'", flush=True)
54-
writer.write(reply.encode("utf-8"))
55-
await writer.drain()
56111

112+
# ── Entry point ───────────────────────────────────────────────────────────────
57113

58114
if __name__ == "__main__":
59115
# Load configuration files — paths are relative to this script's location

0 commit comments

Comments
 (0)