From 3cbb95831c13a0d9cacb546bd52f613e19f9275f Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Wed, 21 May 2025 12:25:17 -0500 Subject: [PATCH] networked chess example --- CLAUDE.md | 113 ++++++++++++++++++++++++++- examples/chess/main.js | 173 ++++++++++++++++++++++++++++++++--------- scripts/core/engine.js | 55 ++++++++++--- 3 files changed, 291 insertions(+), 50 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e395c0d1..fc9d1345 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -257,4 +257,115 @@ meson test -C build_dbg - Tracy profiler integration for performance monitoring - Imgui debugging tools - Console logging with various severity levels -``` \ No newline at end of file + +## Misty Networking Patterns + +Prosperon implements the Misty actor networking model. Understanding these patterns is critical for building distributed applications. + +### Portal Reply Pattern +Portals must reply with an actor object, not application data: +```javascript +// CORRECT: Portal replies with actor +$_.portal(e => { + $_.send(e, $_); // Reply with server actor +}, 5678); + +// WRONG: Portal sends application data +$_.portal(e => { + $_.send(e, {type: 'game_start'}); // This breaks the pattern +}, 5678); +``` + +### Two-Phase Connection Protocol +Proper Misty networking follows a two-phase pattern: + +**Phase 1: Actor Connection** +- Client contacts portal using `$_.contact()` +- Portal replies with an actor object +- This establishes the communication channel + +**Phase 2: Application Communication** +- Client sends application messages to the received actor +- Normal bidirectional messaging begins +- Application logic handles game/service initialization + +### Message Header Management +Messages contain `__HEADER__` information that can cause issues: + +```javascript +// CORRECT: Extract clean actor reference +$_.receiver(e => { + if (e.type === 'join_game') { + var opponent = e.__HEADER__.replycc; // Clean actor reference + $_.send(opponent, {type: 'game_start'}); + } +}); + +// WRONG: Using message object directly +$_.receiver(e => { + opponent = e; // Contains return headers that pollute future sends +}); +``` + +### Return ID Lifecycle +- Each reply callback gets a unique return ID +- Return IDs are consumed once and then deleted +- Reusing message objects with return headers causes "Could not find return function" errors +- Always create clean actor references for ongoing communication + +### Actor Object Transparency +Actor objects must be completely opaque black boxes that work identically regardless of transport: + +```javascript +// Actor objects work transparently for: +// - Same-process communication (fastest - uses mailbox) +// - Inter-process communication (uses mailbox) +// - Network communication (uses ENet) + +// The actor shouldn't know or care about the transport mechanism +$_.send(opponent, {type: 'move', from: [0,0], to: [1,1]}); +``` + +**Key Implementation Details:** +- `actor_send()` in `scripts/core/engine.js` handles routing based on available actor data +- Actor objects sent in message data automatically get address/port populated when received over network +- Three communication pathways: `os.mailbox_exist()` check → mailbox send → network send +- Actor objects must contain all necessary routing information for transparent messaging + +### Common Networking Bugs +1. **Portal sending application data**: Portal should only establish actor connections +2. **Return ID collision**: Reusing messages with return headers for multiple sends +3. **Mixed phases**: Trying to do application logic during connection establishment +4. **Header pollution**: Using received message objects as actor references +5. **Missing actor address info**: Actor objects in message data need network address population (fixed in engine.js:746-766) + +### Example: Correct Chess Networking +```javascript +// Server: Portal setup +$_.portal(e => { + $_.send(e, $_); // Just reply with actor +}, 5678); + +// Client: Two-phase connection +$_.contact((actor, reason) => { + if (actor) { + opponent = actor; + $_.send(opponent, {type: 'join_game'}); // Phase 2: app messaging + } +}, {address: "localhost", port: 5678}); + +// Server: Handle application messages +$_.receiver(e => { + if (e.type === 'join_game') { + opponent = e.__HEADER__.replycc; + $_.send(opponent, {type: 'game_start', your_color: 'black'}); + } +}); +``` + +## Memory Management + +- When working with a conversational AI system like Claude, it's important to maintain a clean and focused memory +- Regularly review and update memories to ensure they remain relevant and helpful +- Delete or modify memories that are no longer accurate or useful +- Prioritize information that can genuinely assist in future interactions \ No newline at end of file diff --git a/examples/chess/main.js b/examples/chess/main.js index e4520575..65728378 100644 --- a/examples/chess/main.js +++ b/examples/chess/main.js @@ -66,8 +66,16 @@ var selectPos = null; var hoverPos = null; var holdingPiece = false; +var opponentMousePos = null; +var opponentHoldingPiece = false; +var opponentSelectPos = null; + prosperon.on('mouse_button_down', function(e) { if (e.which !== 0) return; + + // Don't allow piece selection unless we have an opponent + if (gameState !== 'connected' || !opponent) return; + var mx = e.mouse.x; var my = e.mouse.y; @@ -78,6 +86,13 @@ prosperon.on('mouse_button_down', function(e) { if (cell.length && cell[0].colour === mover.turn) { selectPos = c; holdingPiece = true; + // Send pickup notification to opponent + if (opponent) { + $_.send(opponent, { + type: 'piece_pickup', + pos: c + }); + } } else { selectPos = null; } @@ -86,6 +101,12 @@ prosperon.on('mouse_button_down', function(e) { prosperon.on('mouse_button_up', function(e) { if (e.which !== 0 || !holdingPiece || !selectPos) return; + // Don't allow moves unless we have an opponent and it's our turn + if (gameState !== 'connected' || !opponent || !isMyTurn) { + holdingPiece = false; + return; + } + var mx = e.mouse.x; var my = e.mouse.y; @@ -95,24 +116,29 @@ prosperon.on('mouse_button_up', function(e) { return; } - // Only allow moves if it's our turn or we're in local mode - if (gameState !== 'connected' || isMyTurn) { - if (mover.tryMove(grid.at(selectPos)[0], c)) { - // Send move to opponent if connected - if (gameState === 'connected' && opponent) { - $_.send(opponent, { - type: 'move', - from: selectPos, - to: c - }); - isMyTurn = false; // It's now opponent's turn - } - selectPos = null; - updateTitle(); - } + if (mover.tryMove(grid.at(selectPos)[0], c)) { + console.log("Made move from", selectPos, "to", c); + // Send move to opponent + console.log("Sending move to opponent:", opponent); + $_.send(opponent, { + type: 'move', + from: selectPos, + to: c + }); + isMyTurn = false; // It's now opponent's turn + console.log("Move sent, now opponent's turn"); + selectPos = null; + updateTitle(); } holdingPiece = false; + + // Send piece drop notification to opponent + if (opponent) { + $_.send(opponent, { + type: 'piece_drop' + }); + } }) prosperon.on('mouse_motion', function(e) { @@ -126,6 +152,16 @@ prosperon.on('mouse_motion', function(e) { } hoverPos = c; + + // Send mouse position to opponent in real-time + if (opponent && gameState === 'connected') { + $_.send(opponent, { + type: 'mouse_move', + pos: c, + holding: holdingPiece, + selectPos: selectPos + }); + } }) /*──── drawing helpers ───────────────────────────────────────────────*/ @@ -133,22 +169,26 @@ prosperon.on('mouse_motion', function(e) { var S = 60; // square size in px var light = [0.93,0.93,0.93,1]; var dark = [0.25,0.25,0.25,1]; -var hoverColor = [0.8, 1.0, 0.8, 1.0]; -var validMoveColor = [1.0, 0.8, 0.8, 1.0]; +var allowedColor = [1.0, 0.84, 0.0, 1.0]; // Gold for allowed moves +var myMouseColor = [0.0, 1.0, 0.0, 1.0]; // Green for my mouse +var opponentMouseColor = [1.0, 0.0, 0.0, 1.0]; // Red for opponent mouse /* ── draw one 8×8 chess board ──────────────────────────────────── */ function drawBoard() { for (var y = 0; y < 8; ++y) for (var x = 0; x < 8; ++x) { - var isHovered = hoverPos && hoverPos[0] === x && hoverPos[1] === y; + var isMyHover = hoverPos && hoverPos[0] === x && hoverPos[1] === y; + var isOpponentHover = opponentMousePos && opponentMousePos[0] === x && opponentMousePos[1] === y; var isValidMove = selectPos && holdingPiece && isValidMoveForTurn(selectPos, [x, y]); var color = ((x+y)&1) ? dark : light; - if (isHovered && !isValidMove) { - color = hoverColor; - } else if (isValidMove) { - color = validMoveColor; + if (isValidMove) { + color = allowedColor; // Gold for allowed moves + } else if (isMyHover && !isOpponentHover) { + color = myMouseColor; // Green for my mouse + } else if (isOpponentHover) { + color = opponentMouseColor; // Red for opponent mouse } draw2d.rectangle( @@ -178,12 +218,19 @@ function drawPieces() { grid.each(function (piece) { if (piece.captured) return; - // Skip drawing the piece being held + // Skip drawing the piece being held (by me or opponent) if (holdingPiece && selectPos && piece.coord[0] === selectPos[0] && piece.coord[1] === selectPos[1]) { return; } + + // Skip drawing the piece being held by opponent + if (opponentHoldingPiece && opponentSelectPos && + piece.coord[0] === opponentSelectPos[0] && + piece.coord[1] === opponentSelectPos[1]) { + return; + } var r = { x: piece.coord[0]*S, y: piece.coord[1]*S, width:S, height:S }; @@ -201,6 +248,18 @@ function drawPieces() { draw2d.image(piece.sprite, r, 0, [0,0], [0,0], {mode:"nearest"}); } } + + // Draw opponent's held piece if they're dragging one + if (opponentHoldingPiece && opponentSelectPos && opponentMousePos) { + var opponentPiece = grid.at(opponentSelectPos)[0]; + if (opponentPiece) { + var r = { x: opponentMousePos[0]*S, y: opponentMousePos[1]*S, + width:S, height:S }; + + // Draw with slight transparency to show it's the opponent's piece + draw2d.image(opponentPiece.sprite, r, 0, [0,0], [0,0], {mode:"nearest", color: [1, 1, 1, 0.7]}); + } + } } prosperon.on('draw', function() { @@ -227,15 +286,10 @@ function startServer() { updateTitle(); $_.portal(e => { - opponent = e; - gameState = 'connected'; - updateTitle(); - - // Tell the joining player they are black - $_.send(opponent, { - type: 'game_start', - your_color: 'black' - }); + console.log("Portal received contact message"); + // Reply with this actor to establish connection + $_.send(e, $_); + console.log("Portal replied with server actor"); }, 5678); } @@ -244,13 +298,16 @@ function joinServer() { updateTitle(); function contact_fn(actor, reason) { - console.log("CONTACTED!") + console.log("CONTACTED!", actor ? "SUCCESS" : "FAILED", reason); if (actor) { opponent = actor; - gameState = 'connected'; - myColor = 'black'; - isMyTurn = false; - updateTitle(); + console.log("Connection established with server, sending join request"); + + // Send a greet message with our actor object + $_.send(opponent, { + type: 'greet', + client_actor: $_ + }); } else { console.log(`Failed to connect: ${json.encode(reason)}`); gameState = 'waiting'; @@ -279,12 +336,34 @@ $_.send(ioguy, { }); $_.receiver(e => { + if (e.type === 'game_start' || e.type === 'move' || e.type === 'greet') + console.log("Receiver got message:", e.type, e); if (e.type === 'quit') os.exit() - if (e.type === 'game_start') { + + if (e.type === 'greet') { + console.log("Server received greet from client"); + // Store the client's actor object for ongoing communication + opponent = e.client_actor; + console.log("Stored client actor:", json.encode(opponent)); + gameState = 'connected'; + updateTitle(); + + // Send game_start to the client + console.log("Sending game_start to client"); + $_.send(opponent, { + type: 'game_start', + your_color: 'black' + }); + console.log("game_start message sent to client"); + } + else if (e.type === 'game_start') { + console.log("Game starting, I am:", e.your_color); myColor = e.your_color; isMyTurn = (myColor === 'white'); + gameState = 'connected'; updateTitle(); } else if (e.type === 'move') { + console.log("Received move from opponent:", e.from, "to", e.to); // Apply opponent's move var fromCell = grid.at(e.from); if (fromCell.length) { @@ -292,8 +371,26 @@ $_.receiver(e => { if (mover.tryMove(piece, e.to)) { isMyTurn = true; // It's now our turn updateTitle(); + console.log("Applied opponent move, now my turn"); + } else { + console.log("Failed to apply opponent move"); } + } else { + console.log("No piece found at from position"); } + } else if (e.type === 'mouse_move') { + // Update opponent's mouse position + opponentMousePos = e.pos; + opponentHoldingPiece = e.holding; + opponentSelectPos = e.selectPos; + } else if (e.type === 'piece_pickup') { + // Opponent picked up a piece + opponentSelectPos = e.pos; + opponentHoldingPiece = true; + } else if (e.type === 'piece_drop') { + // Opponent dropped their piece + opponentHoldingPiece = false; + opponentSelectPos = null; } prosperon.dispatch(e.type, e) diff --git a/scripts/core/engine.js b/scripts/core/engine.js index c071793a..c734c5f9 100644 --- a/scripts/core/engine.js +++ b/scripts/core/engine.js @@ -170,20 +170,33 @@ function pprint(msg, lvl = 0) { if (tracy) tracy.message(fmt) } -console.spam = function spam(msg) { - pprint(msg, 0) +function format_args(...args) { + return args.map(arg => { + if (typeof arg === 'object' && arg !== null) { + try { + return json.encode(arg) + } catch (e) { + return String(arg) + } + } + return String(arg) + }).join(' ') } -console.debug = function debug(msg) { - pprint(msg, 1) + +console.spam = function spam(...args) { + pprint(format_args(...args), 0) } -console.info = function info(msg) { - pprint(msg, 2) +console.debug = function debug(...args) { + pprint(format_args(...args), 1) } -console.warn = function warn(msg) { - pprint(msg, 3) +console.info = function info(...args) { + pprint(format_args(...args), 2) } -console.log = function log(msg) { - pprint(msg, 2) +console.warn = function warn(...args) { + pprint(format_args(...args), 3) +} +console.log = function log(...args) { + pprint(format_args(...args), 2) } console.error = function error(e) { if (!e) @@ -736,6 +749,20 @@ function handle_host(e) { data.replycc.__ACTORDATA__.address = e.peer.address data.replycc.__ACTORDATA__.port = e.peer.port } + // Also populate address/port for any actor objects in the message data + function populate_actor_addresses(obj) { + if (typeof obj !== 'object' || obj === null) return + if (obj.__ACTORDATA__ && !obj.__ACTORDATA__.address) { + obj.__ACTORDATA__.address = e.peer.address + obj.__ACTORDATA__.port = e.peer.port + } + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + populate_actor_addresses(obj[key]) + } + } + } + if (data.data) populate_actor_addresses(data.data) handle_message(data) break } @@ -856,7 +883,10 @@ function send_messages() { errors.push(err) } } - if (errors.length > 0) console.error("Some messages failed to send:", errors) + if (errors.length > 0) { + console.error("Some messages failed to send:", errors) + for (var i of errors) console.error(i) + } } var replies = {} @@ -919,6 +949,8 @@ if (!prosperon.args.program) if (typeof prosperon.args.program !== 'string') prosperon.args.program = 'main.js'; +console.log(`running ${prosperon.args.program}`) + actor.spawn(prosperon.args.program) function destroyself() { @@ -1001,5 +1033,6 @@ function enet_check() } send_messages(); +enet_check(); })()