diff --git a/examples/eventBased/politePingPong/politeAlice.py b/examples/eventBased/politePingPong/politeAlice.py index 6f6b2010..e510c88b 100644 --- a/examples/eventBased/politePingPong/politeAlice.py +++ b/examples/eventBased/politePingPong/politeAlice.py @@ -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 @@ -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}).") diff --git a/examples/eventBased/politePingPong/politeBob.py b/examples/eventBased/politePingPong/politeBob.py index aeaa1c77..a7ae53a5 100644 --- a/examples/eventBased/politePingPong/politeBob.py +++ b/examples/eventBased/politePingPong/politeBob.py @@ -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 @@ -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) @@ -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()