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
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:
111
CLAUDE.md
111
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
|
||||
|
||||
## 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
})()
|
||||
|
||||
Reference in New Issue
Block a user