Posts: 131
Threads: 8
Joined: Apr 2012
Reputation:
6
@Ahchay: code integrated in bomberman, 2 players mode started and soon available. (great sample, thx!)
Unfortunately, ngpcraft emu does not display my hicolor screen correctly... same result than on vdmgr, but ok on older emulators...
Posts: 107
Threads: 13
Joined: Feb 2018
Reputation:
4
Do you still have a working flash/ console combo - i can give it a test on real hardware if you don't?
I'm having some issues with the link code not always identifying the host/ client correctly, but I'm working on that
Posts: 131
Threads: 8
Joined: Apr 2012
Reputation:
6
> Do you still have a working flash/ console combo - i can give it a test on real hardware if you don't?
yes, but with one handheld without a working screen 
I'll share a version later today.
> I'm having some issues with the link code not always identifying the host/ client correctly, but I'm working on that
Same for me, I want to put the host player at the top left corner and client at the bottom right, but sometimes, they re both at the same location.
Posts: 131
Threads: 8
Joined: Apr 2012
Reputation:
6
github updated, still wip as I get random bugs during 2 players game mode (at least in ngpcraft-emu)
https://github.com/sodthor/ngpcdev/tree/main/bomberman
23 years after
Posts: 107
Threads: 13
Joined: Feb 2018
Reputation:
4
This response from tixu
> Dug into this, and the good news is most of it isn't your fault. The negotiation first, because you were looking in the wrong place and so was I. The token isn't the problem — the comparison is. I wrote host = (link_token() > tok), but link_token() reads my search counter live while tok is a snapshot from when you built your HELLO, up to 10 frames old by the time I see it. So the two consoles never compare the same pair of numbers. Open the link screen within ~170 ms of each other and both sides can honestly conclude "I'm bigger", and they're both right from where they're standing. Then it latches, because once the session goes READY the host flag never gets recoomputed. Design mistake, not a tuning issue. <
Sounds like a Claude issue to me - I'm working on incorporating a timer into the game-specific handover, but I've got it working well in async mode
Published my playground app on itch - versions of VCS classics basically
https://ahchay.itch.io/ngpc-head-to-head-classics
Posts: 107
Threads: 13
Joined: Feb 2018
Reputation:
4
(08-07-2026, 03:27 AM)sodthor Wrote: github updated, still wip as I get random bugs during 2 players game mode (at least in ngpcraft-emu)
https://github.com/sodthor/ngpcdev/tree/main/bomberman
23 years after 
Works okay for a bit, but keeps getting out of sync - I've had the background disappear/corrupted a few times, and bombs not being displayed properly (mostly on P1?)
I'll do some proper testing later and try and get some video
Posts: 107
Threads: 13
Joined: Feb 2018
Reputation:
4
08-07-2026, 07:04 PM
(This post was last modified: 08-07-2026, 07:15 PM by Ahchay.)
I've further abstracted the linkkit code to my own handshake routine, which I think handles the host/client detection better and also attempts to sync to the same "game" on the other side of the cable - this might be a bit clunky still, but it seems to be consistent from the couple of dozen tests I've run - I should probably make that counter a u16 as it wraps pretty quickly. For my purposes I don't really mind much - as long as the handshake consistently nominates one machine as the host and the other as the client.
Worked example in ahchay/HEADTOHEAD.ngp: Head to head classics for the NGPC - Codeberg.org
Code: /*******************************************************************
* Set up the link to the other console
*
* The four Char* parameters here are the unique code for the game
* P-O-N-G for Pong, N-O-O-N for High Noon etc
*
********************************************************************/
u8 HeadToHeadHandshake(u8 Char1, u8 Char2, u8 Char3, u8 Char4)
{
u8 myBuffer[8];
u8 theirBuffer[8];
bool ReadyHost;
bool ReadyClient;
u8 iLoop;
u8 link_state;
u8 ConnectionTime;
u8 retval;
ReadyHost=false;
ReadyClient=false;
ngpc_linkkit_init();
link_state=ngpc_linkkit_state();
//PrintString(SCR_FORE_PLANE,PAL_TEXT,0,0,"PLAYER VS LINK");
//PrintString(SCR_FORE_PLANE,PAL_TEXT,8,0,"...");
ConnectionTime=0;
retval=ROLE_CLIENT;
while((!ReadyHost)||(!ReadyClient))
{
// Send and receive the joystick state between the two machines
u8 iLoop;
if(!ReadyHost) ConnectionTime++;
myBuffer[0]=LINK_HANDSHAKE;
myBuffer[1]=Char1;
myBuffer[2]=Char2;
myBuffer[3]=Char3;
myBuffer[4]=Char4;
myBuffer[5]=ConnectionTime;
myBuffer[6]=QRandom();
myBuffer[7]=ReadyHost;
ngpc_linkkit_stage(myBuffer); // stages the send
ngpc_linkkit_update(); // sends and receives data
if (ngpc_linkkit_fresh()) // Checks that the link is active
{
ReadyHost=true;
ngpc_linkkit_peek(theirBuffer); // Retrieves the data into gRx[]
//UnpackTheirs();
}
link_state=ngpc_linkkit_state();
switch (link_state) // Check link status
{
case LINKKIT_CONNECTING:
//PrintString(SCR_FORE_PLANE,PAL_TEXT,8,0,"...");
break;
case LINKKIT_READY:
//PrintString(SCR_FORE_PLANE,PAL_TEXT,8,0,"OK!");
break;
case LINKKIT_LOST:
//PrintString(SCR_FORE_PLANE,PAL_TEXT,8,0,"!!!");
break;
case LINKKIT_MISMATCH:
//PrintString(SCR_FORE_PLANE,PAL_TEXT,8,0,"???");
//PrintString(SCR_FORE_PLANE,PAL_TEXT, 0, 17, "OTHER CART DIFFERS ");
break;
default:
//PrintString(SCR_FORE_PLANE,PAL_TEXT,8,0,"***");
ngpc_linkkit_init();
break;
}
ReadyClient=true;
switch(theirBuffer[0])
{
case LINK_HANDSHAKE:
// other client in handshake mode (as they should be)
// Check that their buffer contains the magic letters
// for this game mode
// Probably need to complain/abort if these differ
ReadyClient=theirBuffer[7];
if(ReadyClient)
{
for(iLoop=0;iLoop<5;iLoop++)
{
if(theirBuffer[iLoop]!=myBuffer[iLoop])
{
ReadyClient=false;
}
}
if(ReadyClient)
{
if(ConnectionTime<theirBuffer[5])
{
retval=ROLE_HOST;
}
else if(ConnectionTime==theirBuffer[5]&&myBuffer[6]<theirBuffer[6])
{
retval=ROLE_HOST;
}
}
}
break;
case LINK_GAME:
case LINK_META:
// other client is already in game - shouldn't happen, but
// it can do
// Just let it fall through - they may join the game late
// but the sync routine should sort it out
// At this point, we're assuming that the other client
// is the same cartid. They shouldn't have got past the
// handshake on their end if not
// Players can force this by starting one game mode and then
// quitting one console and rejoining in another.
// That's on them though ;)
break;
default:
// Nonsense or empty data
ReadyClient=false;
break;
}
// if(myBuffer[0]!=theirBuffer[0])
// {
// ReadyClient=false;
// }
if(JOYPAD_OPTION)
{
return ROLE_ABORT;
}
}
return retval;
}
Posts: 131
Threads: 8
Joined: Apr 2012
Reputation:
6
> Works okay for a bit, but keeps getting out of sync - I've had the background disappear/corrupted a few times, and bombs not being displayed properly (mostly on P1?)
yes, same for me, I've done some fixes (hopefully), files updated on github.
I have to find a way to get a valid sync method within the 8 bytes buffer!
Posts: 107
Threads: 13
Joined: Feb 2018
Reputation:
4
08-08-2026, 12:52 AM
(This post was last modified: 08-08-2026, 01:01 AM by Ahchay.)
Yeah, it's not a lot
My (much much) simpler games are working okay with a sort of async data structure like so:
* Crafty Tanks
* ============
* [0] Packet Type
* [1] myTank xPos
* [2] myTank yPos
* [3] myTank Direction/Action Indicator
* - Bits 0-4: Direction
* - Bits 5-6: Action
* - Bit 7: Shoot/Fire indicator
* [4] Their Bullet xPos
* [5] THeir Bullet yPos
* [6] Their BulletID
* - The other client doesn't care *what* the BulletID
* is, but it can check against NULL_SPRITE to determine
* whether there is a bullet at the moment or not
* [7] My Health
Essentially - each client is responsible for their own "character" sprite and the "enemy" bullets - to keep all collision detection local basically - oh, and I'm reducing the fidelity of the sprite positions to u8 for transfer (internally, the games run on u16 for sprite positions to give me options for sub-pixel movement)
But Bomberman has a lot more going on - I guess you could compress the playfield down to 8 bytes or so? So, one frame update playfield, next update players etc? Going to get really tight, really quick.
This is what tixu came up for keeping in sync - I ended up fixing it my original way (as above basically) and might come back to this approach for more complex stuff
> Last one: I'd drop the ball handover rather than debug it. Your packets carry no step number and no owner, so during handover each side keeps applying the peer's last known state, its own position from a few frames back. That's the double bounce, and it's structural. Input lockstep removes the problem instead of managing it: each console sends only its pad for step N, neither advances without the other's, both simulate the ball. No state on the wire, no two truths, no handover. Use 2 steps of input delay (measured: 0 = 20 fps, 1 = 44 % stalled frames, 2 = 13 waiting frames out of 600) and set NGPC_LINK_RX_QUEUE to 4 or 8, at 0, two packets in one frame overwrite each other and you desync silently. Step number in every packet, integer physics, no QRandom() on the shared path. <
I struggle to understand Claude-speak, but I think that this relies on the game being completely deterministic - and you'd have to process both players based on the JOYPAD struct being passed back-and-forth - I suppose you could pre-populate the power-up locations before the round starts which is your main element of chance during gameplay?
Posts: 131
Threads: 8
Joined: Apr 2012
Reputation:
6
I added a 'start' timer in the 2 players game loop, to help the sync.
Today's changes seem to improve this mode a lot (at least in the emu).
Next: take a look at this host/client stuff.
|