diff --git a/examples/eventBased/pingPong/pingpongAlice.py b/examples/eventBased/pingPong/pingpongAlice.py index 0c382152..d54f67fa 100644 --- a/examples/eventBased/pingPong/pingpongAlice.py +++ b/examples/eventBased/pingPong/pingpongAlice.py @@ -1,15 +1,44 @@ """ -Ping-Pong client (Alice). +Ping-Pong — Alice (client). -Alice connects to Bob and sends a sequence of messages. -After each message she waits for Bob's reply and prints it. - -Try changing the messages list to see how Bob reacts! +Alice connects to Bob and they exchange PING / PONG messages for NUM_ROUNDS +rounds. Both sides know NUM_ROUNDS, so no BYE is needed — the connection +simply closes after the last round. This is a purely classical example — no quantum operations. -It demonstrates the event-based programming pattern that we will -later extend with quantum operations (teleportation, etc.). +It demonstrates the event-based state-machine pattern used throughout +SimulaQron examples. + +Alice's state diagram +--------------------- + + ┌─ (connect) ──────────────────────────────────────┐ + │ │ + ▼ │ + IDLE ──[send "PING"]──► PLAYING (start) + │ + recv "PONG" + │ + ▼ + IDLE (next round) + ... after NUM_ROUNDS ... + │ + recv "PONG" (last) + ▼ + DONE + +Transition table: + + State │ Event │ Action │ Next state + ─────────┼──────────────┼──────────────┼───────────── + IDLE │ (entry) │ send "PING" │ PLAYING + PLAYING │ recv "PONG" │ — │ IDLE + PLAYING │ recv "PONG" │ (last round) │ DONE + + IDLE → PLAYING is an *entry action*: Alice sends PING immediately on + entering IDLE, before waiting for the next message. """ + from asyncio import StreamReader, StreamWriter from pathlib import Path @@ -19,27 +48,60 @@ from simulaqron.settings.network_config import NodeConfigType -async def run_alice(reader: StreamReader, writer: StreamWriter): - """ - Alice sends a sequence of messages and prints Bob's replies. +NUM_ROUNDS = 5 + +# ── States ─────────────────────────────────────────────────────────────────── + +STATE_IDLE = "IDLE" +STATE_PLAYING = "PLAYING" +STATE_DONE = "DONE" # noqa: E221 + + +# ── Event loop ─────────────────────────────────────────────────────────────── + +async def run_alice(reader: StreamReader, writer: StreamWriter) -> None: + rounds_done = 0 + + async def handle_pong(_writer: StreamWriter) -> str: + """Transition: PLAYING ──[recv "PONG"]──► IDLE (or DONE)""" + nonlocal rounds_done + print(f"Alice [round {rounds_done}]: received PONG") + if rounds_done < NUM_ROUNDS: + return STATE_IDLE + return STATE_DONE + + dispatch = { + (STATE_PLAYING, "PONG"): handle_pong, + } + + state = STATE_IDLE + + while state != STATE_DONE: + # Entry action: IDLE → send PING → PLAYING + if state == STATE_IDLE: + rounds_done += 1 + writer.write(b"PING\n") + print(f"Alice [round {rounds_done}]: sent PING") + state = STATE_PLAYING + + data = await reader.readline() + if not data: + print(f"Alice [{state}]: connection dropped unexpectedly.") + break + msg = data.decode("utf-8") + + handler = dispatch.get((state, msg)) - Try changing this list to see what Bob does with different messages! - """ - messages = ["ping", "ping", "hello", "ping"] + if handler is None: + print(f"Alice [{state}]: no transition for '{msg}' — ignoring.") + continue - for msg in messages: - # Send a message to Bob - print(f"Alice: sending '{msg}'") - writer.write(msg.encode("utf-8")) - await writer.drain() + state = await handler(writer) - # Wait for Bob's reply - reply_data = await reader.read(255) - reply = reply_data.decode("utf-8") - print(f"Alice: received '{reply}'") + print(f"Alice: event loop finished (final state: {state}).") - print("Alice: done, disconnecting.") +# ── Entry point ─────────────────────────────────────────────────────────────── if __name__ == "__main__": # Load configuration files — paths are relative to this script's location diff --git a/examples/eventBased/pingPong/pingpongBob.py b/examples/eventBased/pingPong/pingpongBob.py index f791a9af..e58228d2 100644 --- a/examples/eventBased/pingPong/pingpongBob.py +++ b/examples/eventBased/pingPong/pingpongBob.py @@ -1,16 +1,44 @@ """ -Ping-Pong server (Bob). +Ping-Pong — Bob (server). -Bob listens for messages from Alice. For each message he receives: - - If the message is "ping", he replies with "pong". - - For anything else, he replies with "no way!". - -Bob keeps listening until Alice disconnects. +Bob listens for Alice's PINGs and replies with PONGs. +Both sides know NUM_ROUNDS, so no BYE is needed — Bob stops after +sending the last PONG and the connection closes naturally. This is a purely classical example — no quantum operations. -It demonstrates the event-based programming pattern that we will -later extend with quantum operations (teleportation, etc.). +It demonstrates the event-based state-machine pattern used throughout +SimulaQron examples. + +Bob's state diagram +------------------- + + ┌─ (connect) ──────────────────────────────────────┐ + │ │ + ▼ │ + IDLE ──[recv "PING"]──► PLAYING (start) + │ + send "PONG" + │ + ▼ + IDLE (next round) + ... after NUM_ROUNDS ... + │ + recv "PING" (last) + ▼ + DONE + +Transition table: + + State │ Event │ Action │ Next state + ─────────┼──────────────┼──────────────┼───────────── + IDLE │ recv "PING" │ — │ PLAYING + PLAYING │ (entry) │ send "PONG" │ IDLE + PLAYING │ (entry) │ (last round) │ DONE + + PLAYING → IDLE/DONE is an *entry action*: Bob sends PONG immediately + on entering PLAYING, before waiting for the next message. """ + from asyncio import StreamReader, StreamWriter from pathlib import Path @@ -20,40 +48,68 @@ from simulaqron.settings.network_config import NodeConfigType -async def run_bob(reader: StreamReader, writer: StreamWriter): - """ - Bob's event loop. +NUM_ROUNDS = 5 + +# ── States ─────────────────────────────────────────────────────────────────── + +STATE_IDLE = "IDLE" +STATE_PLAYING = "PLAYING" +STATE_DONE = "DONE" # noqa: E221 + + +# ── Handlers ───────────────────────────────────────────────────────────────── + +async def handle_ping(_writer: StreamWriter) -> str: + """Transition: IDLE ──[recv "PING"]──► PLAYING""" + print("Bob: received PING", flush=True) + return STATE_PLAYING - Each iteration: - 1. Wait for a message from Alice - 2. Decide on a reply based on the message content - 3. Send the reply back - """ - print("Bob: Alice connected, waiting for messages...", flush=True) - while True: - # Wait until Alice sends something - data = await reader.read(255) +# ── Dispatch table ──────────────────────────────────────────────────────────── - # If we get empty data, Alice has disconnected +BOB_DISPATCH = { + (STATE_IDLE, "PING"): handle_ping, +} + + +# ── Event loop ──────────────────────────────────────────────────────────────── + +async def run_bob(reader: StreamReader, writer: StreamWriter) -> None: + print("Bob: Alice connected.", flush=True) + rounds_done = 0 + state = STATE_IDLE + + while state != STATE_DONE: + # Entry action: PLAYING → send PONG → IDLE (or DONE) + if state == STATE_PLAYING: + rounds_done += 1 + writer.write(b"PONG\n") + print(f"Bob [round {rounds_done}]: sent PONG", flush=True) + state = STATE_IDLE if rounds_done < NUM_ROUNDS else STATE_DONE + continue + + data = await reader.readline() if not data: - print("Bob: Alice disconnected.", flush=True) + print(f"Bob [{state}]: connection dropped unexpectedly.", flush=True) break + msg = data.decode("utf-8") + print(f"Bob [{state}]: received '{msg}'", flush=True) + + handler = BOB_DISPATCH.get((state, msg)) + + if handler is None: + print( + f"Bob [{state}]: no transition for '{msg}' — ignoring.", + flush=True, + ) + continue - message = data.decode("utf-8") - print(f"Bob: received '{message}'", flush=True) + state = await handler(writer) - # Decide on a reply - if message == "ping": - reply = "pong" - else: - reply = "no way!" + print(f"Bob: event loop finished (final state: {state}).", flush=True) - # Send the reply - print(f"Bob: sending '{reply}'", flush=True) - writer.write(reply.encode("utf-8")) - await writer.drain() +# ── Entry point ─────────────────────────────────────────────────────────────── if __name__ == "__main__": # Load configuration files — paths are relative to this script's location diff --git a/examples/eventBased/politePingPong/politeAlice.py b/examples/eventBased/politePingPong/politeAlice.py index e510c88b..da8f5f9f 100644 --- a/examples/eventBased/politePingPong/politeAlice.py +++ b/examples/eventBased/politePingPong/politeAlice.py @@ -1,36 +1,37 @@ """ Polite Ping-Pong — Alice (client). -Alice waits for Bob's "READY" signal before sending each "PING". After -NUM_ROUNDS pings she sends "BYE" instead, ending the exchange. +Extends plain ping pong with a greeting phase: Bob sends "HI" on connect, +Alice replies with "HI", then the game proceeds exactly as in ping pong. Alice's state diagram --------------------- - ┌─ (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 + (connect) + │ + ▼ + WAITING_HI ──[recv "HI"]──► IDLE ← greeting phase + send "HI" │ + │ (entry action: send "PING") + ▼ + PLAYING + │ + recv "PONG" + ┌─────┴──────────────┐ + rounds left? done? + │ │ + ▼ ▼ + IDLE 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 + State │ Event │ Action │ Next state + ────────────┼──────────────┼──────────────┼──────────────── + WAITING_HI │ recv "HI" │ send "HI" │ IDLE + IDLE │ (entry) │ send "PING" │ PLAYING + PLAYING │ recv "PONG" │ — │ IDLE or DONE """ + from asyncio import StreamReader, StreamWriter from pathlib import Path @@ -44,61 +45,53 @@ # ── States ─────────────────────────────────────────────────────────────────── -STATE_WAITING_FOR_READY = "WAITING_FOR_READY" -STATE_WAITING_FOR_PONG = "WAITING_FOR_PONG" # noqa: E221 -STATE_DONE = "DONE" # noqa: E221 - -# ── Mutable round counter ───────────────────────────────────────────────────── - -rounds_left = NUM_ROUNDS - - -# ── 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_pong(writer: StreamWriter) -> str: - print("Alice: received PONG") - return STATE_WAITING_FOR_READY - - -# ── Dispatch table ──────────────────────────────────────────────────────────── - -ALICE_DISPATCH = { - (STATE_WAITING_FOR_READY, "READY"): handle_ready, - (STATE_WAITING_FOR_PONG, "PONG"): handle_pong, # noqa: E241 -} +STATE_WAITING_HI = "WAITING_HI" +STATE_IDLE = "IDLE" # noqa: E221 +STATE_PLAYING = "PLAYING" # noqa: E221 +STATE_DONE = "DONE" # noqa: E221 # ── Event loop ─────────────────────────────────────────────────────────────── async def run_alice(reader: StreamReader, writer: StreamWriter) -> None: - global rounds_left - rounds_left = NUM_ROUNDS + rounds_done = 0 + + async def handle_hi(writer: StreamWriter) -> str: + """Transition: WAITING_HI ──[recv "HI"]──► IDLE""" + writer.write(b"HI\n") + print("Alice: sent HI — greeting done, game starting") + return STATE_IDLE + + async def handle_pong(_writer: StreamWriter) -> str: + """Transition: PLAYING ──[recv "PONG"]──► IDLE (or DONE)""" + nonlocal rounds_done + print(f"Alice [round {rounds_done}]: received PONG") + if rounds_done < NUM_ROUNDS: + return STATE_IDLE + return STATE_DONE + + dispatch = { + (STATE_WAITING_HI, "HI"): handle_hi, + (STATE_PLAYING, "PONG"): handle_pong, + } - state = STATE_WAITING_FOR_READY + state = STATE_WAITING_HI while state != STATE_DONE: + # Entry action: IDLE → send PING → PLAYING + if state == STATE_IDLE: + rounds_done += 1 + writer.write(b"PING\n") + print(f"Alice [round {rounds_done}]: sent PING") + state = STATE_PLAYING + 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}'") - handler = ALICE_DISPATCH.get((state, msg)) + handler = dispatch.get((state, msg)) if handler is None: print(f"Alice [{state}]: no transition for '{msg}' — ignoring.") diff --git a/examples/eventBased/politePingPong/politeBob.py b/examples/eventBased/politePingPong/politeBob.py index a7ae53a5..75869929 100644 --- a/examples/eventBased/politePingPong/politeBob.py +++ b/examples/eventBased/politePingPong/politeBob.py @@ -1,28 +1,36 @@ """ Polite Ping-Pong — Bob (server). -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). +Extends plain ping pong with a greeting phase: Bob sends "HI" immediately +on connect and waits for Alice's "HI" before the game begins. Bob's state diagram ------------------- - ┌─ (connect) ──────────────────────────────────────────┐ - │ send "READY" │ - ▼ │ -WAITING_FOR_PING_OR_BYE (start) - │ - recv "PING" → send "PONG", send "READY" → (stay) - recv "BYE" → DONE + (connect) → send "HI" + │ + ▼ + WAITING_HI ──[recv "HI"]──► IDLE ← greeting phase + │ + recv "PING" + ▼ + PLAYING + │ (entry action: send "PONG") + ┌─────┴──────────────┐ + rounds left? done? + │ │ + ▼ ▼ + IDLE DONE Transition table: - 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 + State │ Event │ Action │ Next state + ────────────┼──────────────┼──────────────┼──────────────── + WAITING_HI │ recv "HI" │ — │ IDLE + IDLE │ recv "PING" │ — │ PLAYING + PLAYING │ (entry) │ send "PONG" │ IDLE or DONE """ + from asyncio import StreamReader, StreamWriter from pathlib import Path @@ -32,32 +40,35 @@ from simulaqron.settings.network_config import NodeConfigType +NUM_ROUNDS = 5 + # ── States ─────────────────────────────────────────────────────────────────── -STATE_WAITING_FOR_PING_OR_BYE = "WAITING_FOR_PING_OR_BYE" -STATE_DONE = "DONE" # noqa: E221 +STATE_WAITING_HI = "WAITING_HI" +STATE_IDLE = "IDLE" # noqa: E221 +STATE_PLAYING = "PLAYING" # noqa: E221 +STATE_DONE = "DONE" # noqa: E221 # ── Handlers ───────────────────────────────────────────────────────────────── -async def handle_ping(writer: StreamWriter) -> str: - 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_hi(_writer: StreamWriter) -> str: + """Transition: WAITING_HI ──[recv "HI"]──► IDLE""" + print("Bob: received HI — greeting done, game starting", flush=True) + return STATE_IDLE -async def handle_bye(writer: StreamWriter) -> str: - print("Bob: received BYE, closing.", flush=True) - return STATE_DONE +async def handle_ping(_writer: StreamWriter) -> str: + """Transition: IDLE ──[recv "PING"]──► PLAYING""" + print("Bob: received PING", flush=True) + return STATE_PLAYING # ── Dispatch table ──────────────────────────────────────────────────────────── BOB_DISPATCH = { - (STATE_WAITING_FOR_PING_OR_BYE, "PING"): handle_ping, - (STATE_WAITING_FOR_PING_OR_BYE, "BYE"): handle_bye, # noqa: E241 + (STATE_WAITING_HI, "HI"): handle_hi, + (STATE_IDLE, "PING"): handle_ping, # noqa: E241 } @@ -65,17 +76,27 @@ async def handle_bye(writer: StreamWriter) -> str: async def run_bob(reader: StreamReader, writer: StreamWriter) -> None: print("Bob: Alice connected.", flush=True) + rounds_done = 0 - writer.write(b"READY\n") - print("Bob: sent READY", flush=True) - state = STATE_WAITING_FOR_PING_OR_BYE + # Greet Alice before starting the game. + writer.write(b"HI\n") + print("Bob: sent HI", flush=True) + state = STATE_WAITING_HI while state != STATE_DONE: + # Entry action: PLAYING → send PONG → IDLE (or DONE) + if state == STATE_PLAYING: + rounds_done += 1 + writer.write(b"PONG\n") + print(f"Bob [round {rounds_done}]: sent PONG", flush=True) + state = STATE_IDLE if rounds_done < NUM_ROUNDS else STATE_DONE + continue + data = await reader.readline() if not data: print(f"Bob [{state}]: connection dropped unexpectedly.", flush=True) break - msg = data.decode().strip() + msg = data.decode("utf-8") print(f"Bob [{state}]: received '{msg}'", flush=True) handler = BOB_DISPATCH.get((state, msg))