Skip to content
Open
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
108 changes: 85 additions & 23 deletions examples/eventBased/pingPong/pingpongAlice.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand Down
122 changes: 89 additions & 33 deletions examples/eventBased/pingPong/pingpongBob.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand Down
Loading