Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 57 additions & 88 deletions examples/eventBased/politePingPong/politeAlice.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,35 @@
"""
Polite Ping-Pong — Alice (client).

Alice's behaviour is defined as a finite state machine. She always
initiates the exchange by sending "ping" before entering the event loop.
The event loop then reads one message at a time, looks up the
(current_state, message) pair in the dispatch table, and calls the
corresponding handler. The handler performs an action and returns the
next state.
Alice waits for Bob's "READY" signal before sending each "PING". After
NUM_ROUNDS pings she sends "BYE" instead, ending the exchange.

Alice's state diagram
---------------------

┌─ (connect) ─────────────────────────────┐
│ send "ping" │
▼ │
──────────────► WAITING_PONG (start)
recv "pong"
send "thank you"
WAITING_YOURE_WELCOME
recv "you're welcome"
DONE

As a transition table (this maps directly to ALICE_DISPATCH below):

Current state │ Message received │ Action │ Next state
───────────────────────┼────────────────────┼──────────────────┼──────────────────────
WAITING_PONG │ "pong" │ send "thank you" │ WAITING_YOURE_WELCOME
WAITING_YOURE_WELCOME │ "you're welcome" │ (none) │ DONE

Any (state, message) pair NOT in the table is rejected with a warning;
the state does not change and the loop continues.

Note: the initial "ping" is sent before the loop starts — it is not a
state transition but simply Alice's opening move as the initiator.
┌─ (connect) ──────────────────────────────────────────┐
│ │
▼ │
WAITING_FOR_READY ──[recv "READY"]──► WAITING_FOR_PONG (start)
recv "PONG"
WAITING_FOR_READY (next round)
... after NUM_ROUNDS ...
recv "READY" (last)
send "BYE"
DONE

Transition table:

Current state │ Message │ Action │ Next state
────────────────────┼──────────┼─────────────────────────────┼──────────────────
WAITING_FOR_READY │ "READY" │ send "PING" (or "BYE") │ WAITING_FOR_PONG
│ │ │ (or DONE)
WAITING_FOR_PONG │ "PONG" │ — │ WAITING_FOR_READY
"""
from asyncio import StreamReader, StreamWriter
from pathlib import Path
Expand All @@ -49,92 +40,70 @@
from simulaqron.settings.network_config import NodeConfigType


NUM_ROUNDS = 5

# ── States ───────────────────────────────────────────────────────────────────

STATE_WAITING_PONG = "WAITING_PONG" # noqa: E221
STATE_WAITING_YOURE_WELCOME = "WAITING_YOURE_WELCOME"
STATE_DONE = "DONE" # noqa: E221
STATE_WAITING_FOR_READY = "WAITING_FOR_READY"
STATE_WAITING_FOR_PONG = "WAITING_FOR_PONG" # noqa: E221
STATE_DONE = "DONE" # noqa: E221

# ── Mutable round counter ─────────────────────────────────────────────────────

# ── Handlers ─────────────────────────────────────────────────────────────────
rounds_left = NUM_ROUNDS

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

Alice receives a pong and politely says thank you.
"""
reply = "thank you"
print(f"Alice: sending '{reply}'")
writer.write(reply.encode("utf-8"))
await writer.drain()
return STATE_WAITING_YOURE_WELCOME
# ── Handlers ─────────────────────────────────────────────────────────────────

async def handle_ready(writer: StreamWriter) -> str:
global rounds_left
if rounds_left > 0:
rounds_left -= 1
round_num = NUM_ROUNDS - rounds_left
writer.write(b"PING\n")
print(f"Alice [round {round_num}]: sent PING")
return STATE_WAITING_FOR_PONG
else:
writer.write(b"BYE\n")
print("Alice: sent BYE, done.")
return STATE_DONE

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

Alice receives the final courtesy and the exchange is complete.
No reply is needed.
"""
print("Alice: received 'you're welcome' — exchange complete.")
return STATE_DONE
async def handle_pong(writer: StreamWriter) -> str:
print("Alice: received PONG")
return STATE_WAITING_FOR_READY


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

ALICE_DISPATCH = {
(STATE_WAITING_PONG, "pong"): handle_pong, # noqa: E241
(STATE_WAITING_YOURE_WELCOME, "you're welcome"): handle_youre_welcome,
(STATE_WAITING_FOR_READY, "READY"): handle_ready,
(STATE_WAITING_FOR_PONG, "PONG"): handle_pong, # noqa: E241
}


# ── Event loop ────────────────────────────────────────────────────────────────

async def run_alice(reader: StreamReader, writer: StreamWriter):
"""
Alice's event loop.
# ── Event loop ───────────────────────────────────────────────────────────────

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

state = STATE_WAITING_PONG
state = STATE_WAITING_FOR_READY

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

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

# 3a. Invalid transition — warn and stay in current state
if handler is None:
print(
f"Alice [{state}]: no transition for message '{msg}' — ignoring."
)
print(f"Alice [{state}]: no transition for '{msg}' — ignoring.")
continue

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

print(f"Alice: event loop finished (final state: {state}).")
Expand Down
112 changes: 37 additions & 75 deletions examples/eventBased/politePingPong/politeBob.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,27 @@
"""
Polite Ping-Pong — Bob (server).

Bob's behaviour is defined as a finite state machine. The event loop
reads one message at a time, looks up the (current_state, message) pair
in the dispatch table, and calls the corresponding handler. The handler
performs an action (typically sending a reply) and returns the next state.
Bob sends "READY" immediately on connection and after each "PONG", signalling
to Alice that he is ready for the next round. He handles "PING" (reply PONG
then READY) and "BYE" (close).

Bob's state diagram
-------------------

recv "ping" send "pong"
WAITING_PING ─────────────────────────────────────► WAITING_THANKS
recv "thank you"
send "you're welcome"
DONE
┌─ (connect) ──────────────────────────────────────────┐
│ send "READY" │
▼ │
WAITING_FOR_PING_OR_BYE (start)
recv "PING" → send "PONG", send "READY" → (stay)
recv "BYE" → DONE

As a transition table (this maps directly to BOB_DISPATCH below):
Transition table:

Current state │ Message received │ Action │ Next state
─────────────────┼──────────────────┼────────────────────────┼──────────────────
WAITING_PING │ "ping" │ send "pong" │ WAITING_THANKS
WAITING_THANKS │ "thank you" │ send "you're welcome" │ DONE

Any (state, message) pair NOT in the table is rejected with a warning;
the state does not change and the loop continues.
Current state │ Message │ Action │ Next state
────────────────────────┼─────────┼──────────────────────────┼───────────────────────
WAITING_FOR_PING_OR_BYE │ "PING" │ send "PONG", send "READY"│ WAITING_FOR_PING_OR_BYE
WAITING_FOR_PING_OR_BYE │ "BYE" │ — │ DONE
"""
from asyncio import StreamReader, StreamWriter
from pathlib import Path
Expand All @@ -38,93 +33,60 @@


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

STATE_WAITING_PING = "WAITING_PING" # noqa: E221
STATE_WAITING_THANKS = "WAITING_THANKS"
STATE_DONE = "DONE" # noqa: E221
STATE_WAITING_FOR_PING_OR_BYE = "WAITING_FOR_PING_OR_BYE"
STATE_DONE = "DONE" # noqa: E221


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

async def handle_ping(writer: StreamWriter) -> str:
"""
Transition: WAITING_PING ──[recv "ping"]──► WAITING_THANKS

Bob receives a ping and replies with a pong.
"""
reply = "pong"
print(f"Bob: sending '{reply}'", flush=True)
writer.write(reply.encode("utf-8"))
await writer.drain()
return STATE_WAITING_THANKS


async def handle_thank_you(writer: StreamWriter) -> str:
"""
Transition: WAITING_THANKS ──[recv "thank you"]──► DONE

Bob receives a thank you and replies politely before finishing.
"""
reply = "you're welcome"
print(f"Bob: sending '{reply}'", flush=True)
writer.write(reply.encode("utf-8"))
await writer.drain()
writer.write(b"PONG\n")
print("Bob: sent PONG", flush=True)
writer.write(b"READY\n")
print("Bob: sent READY", flush=True)
return STATE_WAITING_FOR_PING_OR_BYE


async def handle_bye(writer: StreamWriter) -> str:
print("Bob: received BYE, closing.", flush=True)
return STATE_DONE


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

BOB_DISPATCH = {
(STATE_WAITING_PING, "ping"): handle_ping, # noqa: E241
(STATE_WAITING_THANKS, "thank you"): handle_thank_you,
(STATE_WAITING_FOR_PING_OR_BYE, "PING"): handle_ping,
(STATE_WAITING_FOR_PING_OR_BYE, "BYE"): handle_bye, # noqa: E241
}


# ── Event loop ────────────────────────────────────────────────────────────────

async def run_bob(reader: StreamReader, writer: StreamWriter):
"""
Bob's event loop.

Repeatedly:
1. Read the next message from Alice.
2. Look up (current_state, message) in BOB_DISPATCH.
3. If found, call the handler and move to the returned next state.
4. If not found, log a warning and stay in the current state.
Loop exits when the state reaches STATE_DONE or the connection drops.
"""
async def run_bob(reader: StreamReader, writer: StreamWriter) -> None:
print("Bob: Alice connected.", flush=True)
state = STATE_WAITING_PING

writer.write(b"READY\n")
print("Bob: sent READY", flush=True)
state = STATE_WAITING_FOR_PING_OR_BYE

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

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

# 3a. Invalid transition — warn and stay in current state
if handler is None:
print(
f"Bob [{state}]: no transition for message '{msg}' — ignoring.",
f"Bob [{state}]: no transition for '{msg}' — ignoring.",
flush=True,
)
continue

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

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

print("Bob: starting server...")
print("Bob: starting server...", flush=True)
server.start_serving()