networked chess example
Some checks failed
Build and Deploy / build-macos (push) Failing after 4s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled

This commit is contained in:
2025-05-21 12:25:17 -05:00
parent 146baf1d23
commit 3cbb95831c
3 changed files with 291 additions and 50 deletions

111
CLAUDE.md
View File

@@ -257,4 +257,115 @@ meson test -C build_dbg
- Tracy profiler integration for performance monitoring
- Imgui debugging tools
- Console logging with various severity levels
## 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

View File

@@ -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) {
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,13 +218,20 @@ 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)

View File

@@ -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)
}
console.debug = function debug(msg) {
pprint(msg, 1)
}
console.info = function info(msg) {
pprint(msg, 2)
return String(arg)
}).join(' ')
}
console.warn = function warn(msg) {
pprint(msg, 3)
console.spam = function spam(...args) {
pprint(format_args(...args), 0)
}
console.log = function log(msg) {
pprint(msg, 2)
console.debug = function debug(...args) {
pprint(format_args(...args), 1)
}
console.info = function info(...args) {
pprint(format_args(...args), 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();
})()