Major bug: opponent moves not appearing on the board

In my last game, the opponent moves did not update on the board without refreshing the browser. After each move, the browser console log would show:
OGSConnectivity.ts:543 Invalid move for this game received [81869256] {game_id: ‘81869256’, move_number: 95, move: Array(3)}

The browser is Chrome Version 142.0.7444.176 (Official Build) (64-bit).

Here’s full console logs from one of those “sessions” (where I need to refresh the page, then wait for the invalid move error to appear, then refresh again:
WASM support detected, score estimator will be fast
sockets.ts:61 Websocket host not overridden
sockets.ts:82 Connecting via Cloudflare
NetworkStatus.tsx:83 Network status: connected : warning toggle on, not in live game, didn’t close notification time control: undefined
GobanSocket.ts:215 GobanSocket connected to wss://online-go.com
sfx.ts:448 The AudioContext was not allowed to start. It must be resumed (or created) after a user gesture on the page. ``https://developer.chrome.com/blog/autoplay/#web_audio
_autoResume @ howler.js:525
play @ howler.js:822
action @ howler.js:803
_loadQueue @ howler.js:1939
u @ howler.js:2505
g @ howler.js:2471
Promise.then
c @ howler.js:2479
v.onload @ howler.js:2428
i @ helpers.js:93
XMLHttpRequest.send
(anonymous) @ browserapierrors.js:146
apply @ xhr.js:148
l @ howler.js:2450
o @ howler.js:2440
load @ howler.js:729
init @ howler.js:646
(anonymous) @ howler.js:2755
i @ howler.js:565
load @ sfx.ts:448
sync @ sfx.ts:367
enable @ sfx.ts:356
(anonymous) @ sfx.ts:641
setInterval
(anonymous) @ sfx.ts:635Understand this warning
NetworkStatus.tsx:83 Network status: connected : warning toggle on, not in live game, didn’t close notification time control: {system: ‘none’, speed: ‘correspondence’, pause_on_weekends: true}
GobanSocket.ts:215 GobanSocket connected to wss://ai.online-go.com
sfx.ts:448 The AudioContext was not allowed to start. It must be resumed (or created) after a user gesture on the page. ``https://developer.chrome.com/blog/autoplay/#web_audio
_autoResume @ howler.js:525
play @ howler.js:822
action @ howler.js:803
_loadQueue @ howler.js:1939
u @ howler.js:2505
g @ howler.js:2471
Promise.then
c @ howler.js:2479
v.onload @ howler.js:2428
i @ helpers.js:93
XMLHttpRequest.send
(anonymous) @ browserapierrors.js:146
apply @ xhr.js:148
l @ howler.js:2450
o @ howler.js:2440
load @ howler.js:729
init @ howler.js:646
(anonymous) @ howler.js:2755
i @ howler.js:565
load @ sfx.ts:448
sync @ sfx.ts:369
enable @ sfx.ts:356
(anonymous) @ sfx.ts:641
setInterval
(anonymous) @ sfx.ts:635Understand this warning
sfx.ts:448 The AudioContext was not allowed to start. It must be resumed (or created) after a user gesture on the page. ``https://developer.chrome.com/blog/autoplay/#web_audio
_autoResume @ howler.js:525
play @ howler.js:822
action @ howler.js:803
_loadQueue @ howler.js:1939
u @ howler.js:2505
g @ howler.js:2471
Promise.then
c @ howler.js:2479
v.onload @ howler.js:2428
i @ helpers.js:93
XMLHttpRequest.send
(anonymous) @ browserapierrors.js:146
apply @ xhr.js:148
l @ howler.js:2450
o @ howler.js:2440
load @ howler.js:729
init @ howler.js:646
(anonymous) @ howler.js:2755
i @ howler.js:565
load @ sfx.ts:448
sync @ sfx.ts:370
enable @ sfx.ts:356
(anonymous) @ sfx.ts:641
setInterval
(anonymous) @ sfx.ts:635Understand this warning
sfx.ts:448 The AudioContext was not allowed to start. It must be resumed (or created) after a user gesture on the page. ``https://developer.chrome.com/blog/autoplay/#web_audio
_autoResume @ howler.js:525
play @ howler.js:822
action @ howler.js:803
_loadQueue @ howler.js:1939
u @ howler.js:2505
g @ howler.js:2471
Promise.then
c @ howler.js:2479
v.onload @ howler.js:2428
i @ helpers.js:93
XMLHttpRequest.send
(anonymous) @ browserapierrors.js:146
apply @ xhr.js:148
l @ howler.js:2450
o @ howler.js:2440
load @ howler.js:729
init @ howler.js:646
(anonymous) @ howler.js:2755
i @ howler.js:565
load @ sfx.ts:448
sync @ sfx.ts:368
enable @ sfx.ts:356
(anonymous) @ sfx.ts:641
setInterval
(anonymous) @ sfx.ts:635Understand this warning
GobanController.ts:1165 Player : 382071 disconnected
GobanController.ts:1194 Player : 382071 reconnected
OGSConnectivity.ts:543 Invalid move for this game received [81869256] {game_id: ‘81869256’, move_number: 95, move: Array(3)}

Was it this game? smog山人 vs. mikko.karjanmaa

So I’m thinking this is somehow my fault. Smoglet was testing Tsumego Dragon client to play a game. (only this one) So I’m thinking this bug is two fold. @anoek

Firstly, I am using this code to send a move. My theory is that this is not what ogs is expecting since the stone is not showing up, even though the clock does change over.

function PlayOgsMoveSimple(game_id, move) {

        if (move === '..') {
            this.ogsGame.PassTurn();
            return;
        }

        const data = {
            "game_id": game_id,
            "move": move
        };
        webSocket.send(JSON.stringify(["game/move", data]));
    }

The question then becomes, why does it work fine for my client’s side? If this is not what ogs wants, then it should have sent me an error stating what it’s missing instead of accepting my move yea? But it does accept it and then doesn’t show it.

So I need to know what is wrong with my code and ogs needs to throw an error when this method is sent since it clearly is not what ogs wants. (I think)

Below is my full connection script for my game page.

    console.log('OGS Connection Script Started');

    let webSocket;
    let local_jwt;
    let game_id;
    let player_id;
    let latency = 0;
    let clock;
    let connected = false;
    let games;
    let activeGames = [];
    let callbacks = [];
    let ogsGame;
    let accessToken = localStorage.getItem("access_token");
    if(accessToken !== null) accessToken = accessToken.replace(/"/g, '');

    loadOGS();

    // Connect to OGS
    function loadOGS ()
    {


        webSocket = new WebSocket('wss://online-go.com');

        SubscribeToMessages(messageCallback, 'connection script');

        webSocket.onopen = (event) => {

            console.log(`Connected to the websocket: ${Date.now()}`);
            setInterval(() => {
                webSocket.send('["net/ping",{}]');
                //bubble_fn_ping(0);
            }, 5000);

            webSocket.onmessage = (event) => {
                const json = JSON.parse(event.data);
                OnMessage(json[0], json[1]);
            };
        }

        webSocket.onclose = (event) => {
            alert('Connection Lost');
            console.log(event.data);
        }
    }

    function waitForGoban() {
        return new Promise((resolve) => {
            const interval = setInterval(() => {
                if (typeof goban !== 'undefined' && goban !== null) {
                    clearInterval(interval);
                    resolve(goban);
                }
            }, 50); // check every 50ms
        });
    }

    function ConfigIsLoaded(jwt, new_player_id)
    {
    	player_id = new_player_id;
        local_jwt = jwt;
        webSocket.send(JSON.stringify(["authenticate", {"jwt": local_jwt}]));
    }

    function SubscribeToMessages (callback, name) {
        callbacks.push(callback);
        console.log(name + ' subscribed to messages');
        //console.log(callbacks.length);
    }

    function UnsubscribeFromMessages (callback) {
        callbacks.remove(callback);
    }

    function OnMessage(type, data) {
        let log = "Log: " + type + ": " + JSON.stringify(data);

        for(var i=0; i<callbacks.length; i++) {
            let callback = callbacks[i];
            if(callback === undefined) {console.log('ERROR: Empty callback.' );
                                       return;}
            callback(type, data);
        }
    }

    function messageCallback(type, data) {
            if(type === "net/pong") { bubble_fn_pong(1); }

        	// Connected
            else if(type === "active-bots")
            {
                if(!connected) {
                    console.log("connected to ogs");

                    console.log(`Connected & Subscribing to game.`);
                    const params = new URLSearchParams(window.location.search);
                    const game_id_from_url = params.get("game_id");

                    waitForGoban().then((gobanInstance) => {
                        console.log("Goban is ready!", gobanInstance);
                        // Initialize the next HTML block or run your code here
                        SubscribeToGame(game_id_from_url);
                    });

					
                    

                    connected = true;
                    bubble_fn_connected(true);

                }

            }
            else if(type === 2) {
                bubble_fn_online(data);
            }

            // Move played
            else if (type.includes("game/")) {
                this.ogsGame.GameMessage(type, data);
            }

        	// Notifcation event. Server told us something.
            else if (type === 'notification') {
                NotificationEvent(data);
            }
    }

    // Get notification events and send them to handlers.
	function NotificationEvent(data) {
        let type = data.type;
        let message = data.message;
        //console.log('Notification: ' + type);
        //console.log(data);

        if (type === 'gameOfferRejected') {
            let bubble_data = {
                'output1': data.game_id,
                'output2': message
            };
            bubble_fn_challenge_rejected(bubble_data);
        } else if (type === 'gameStarted') {
            bubble_fn_game_started(data.game_id);
        } else if (type === 'gameResumedFromStoneRemoval') {
            bubble_fn_game_resumed(data.game_id);
        }
    }


    async function GetConfigAPICall() {

      console.log('Getting Config');
      const res = await fetch("https://online-go.com/api/v1/ui/config", {
        headers: {
          "Authorization": "Bearer " + accessToken,
          'Content-Type': 'application/json'
        }
      });

      const data = await res.json();
        console.log('We got the local config.');
        console.log(data);
        this.ogsGame.SetPlayerId(data.user.id);
        ConfigIsLoaded(data.user_jwt, data.user.id);
        
    }

    function ResumeGameFromPause(gameId)
    {
        const data = {
            "auth": accessToken,
            "game_id": gameId,
            "player_id": player_id,
        };
        webSocket.send(JSON.stringify(["game/resume", data]));
    }

    // Play a move in the game if you are the player.
    function PostOgsMove(game_id, move, main_time_left)
    {
        let timed_out = false;
        if (main_time_left <= 0) { timed_out = true; }
        const data = {
            "auth": accessToken,
            "game_id": game_id,
            "player_id": player_id,
            "move": move,
            "clock": {
                "main_time": main_time_left,
                "time_out": timed_out
            }
        };

        webSocket.send(JSON.stringify(["game/move", data]));
    }

    function PlayOgsMoveSimple(game_id, move) {

        if (move === '..') {
            this.ogsGame.PassTurn();
            return;
        }

        const data = {
            "game_id": game_id,
            "move": move
        };
        webSocket.send(JSON.stringify(["game/move", data]));
    }

    function PassTurnOgs(game_id) {

        const data = {
            "game_id": game_id,
            "move": ".."
        };
        webSocket.send(JSON.stringify(["game/move", data]));
    }

    function ResignOgsMove(game_id) {
        const data = {
            "game_id": game_id
        };
        webSocket.send(JSON.stringify(["game/resign", data]));
    }

    function CancelOgsGame(game_id) {
        const data = {
            "game_id": game_id
        };
        webSocket.send(JSON.stringify(["game/cancel", data]));
    }

    function SubscribeToGame(game_id) {
        this.ogsGame = new OGSG(game_id);
        this.ogsGame.GetGameAPICall(goban);

        console.log('Subscribing: ' + game_id);
        webSocket.send(JSON.stringify(["game/connect", { 'game_id': game_id, 'chat': false }]));
        bubble_fn_subscribed_to_game(game_id);

    }

    function AcceptBoardState(game_id, stones) {
        console.log('Accept Board State.');
        let removedStones = '';
        if (stones != null) { removedStones = stones; }
        webSocket.send(JSON.stringify([
            "game/removed_stones/accept", {
                "auth": accessToken,
                "game_id": game_id,
                "player_id": player_id,
                "stones": removedStones,
                "strict_seki_mode": false
            }
        ]));
    }

    function GameBoardClicked(goban, x, y) {
        console.log('OGS Game Connection Go Board Clicked.');
        this.ogsGame.BoardClicked(goban, x, y);
    }

    function GetCurrentOGSGame() {
        return ogsGame;
    }

</script>
1 Like

Here’s the goban code: https://www.npmjs.com/package/goban and GitHub - online-go/goban: A JavaScript library for exploring and playing the game of Go that’s what you’ll want to either use or reference

According to this,

interface MoveCommand {
blur?: number;
clock?: JGOFPlayerClock;
game_id: number;
move: string;
}

I am doing it correctly,

const data = {
“game_id”: game_id,
“move”: move
};
webSocket.send(JSON.stringify([“game/move”, data]));

And the ogs clock updates, so why would the board not show the stone? :face_with_monocle::exploding_head:

Yes, that’s the game. I would also think that OGS client should handle the situation more gracefully, if I hadn’t opened the dev console, I would have only seen an empty board with a running clock and no interactivity.

That’s a good question of OGS server code’s design philosophy, should it be treated as a public api defensive to bad inputs from badly-written or even malicious clients, or trusting that clients are well-behaved. The answer is highly influenced by the amount of developer resources, which is small.

As 3rd-party clients are allowed, perhaps some indication that your opponent is using one and its name (on KGS if they were using the phone app they had a little phone icon next to their username) would be a good idea, so people are alerted to the fact that if weird things happen it might be due to their opponent using one, rather than OGS official client.

Its possible to timeout on official OGS site because opponent using 3rd-party app with bug ??

Have you connected to the game chat?

I’ve found this is important for many things, including

  • participating in scoring
  • showing up as “online”

I don’t recall if this feeds into moves showing up.


If it’s not chat and you need to track down a bug bug, I recommend a couple things:

  • Gather logs for your app (console.log will only log to the user’s console, you want something that you can revuew as a developer)
  • watch the “game/error” message over WS (though… if OGS is half accepting it - probably it’s not doing errors correctly)

Finally, if TD is sending sane inputs, and the error should be fixed on OGS side, probably the best way to communicate this is to find a minimum reproducible example. This goes back to the logging tip earlier.

If you can consistently reproduce with a set of WS messages, it should be much easier for OGS team to find out where moves should be handled better

Oh wait, Mikko did share logs, and OGS did send you an error! (awesome!)

@Clossius1 i recommend you figure out how to make the move field print more than Array(3), and there’s your answer.

Also, display the OGSConnectivity message in the UI, in case your next report isn’t thoughtful enough to include logs (and your users will know to refresh the page)

nvm this is OGS client generating the logs (still, I think logging on your client can help you!)