I Cannot Figure Out This Bug - Post Move

Okay so I am using my client, Tsumego Dragon to play a game on OGS. Currently, I connect to the game and play a move. But my move sometimes does not show up on OGS. The OGS user has to refresh their page to see the move. BUT their clock DOES change over and the upper right corner shows it is their turn in a game, which links to this game. Additionally, after they refresh, my move IS there and when they play a move, their move shows up just fine on my client.

Here are screenshots from the Network. Firstly, from my TD’s side.

Now from OGS’s side…

BUT OGS console gives an error…

Invalid move for this game received [81936672]

Object { game_id: “81936672”, move_number: 3, move: (3) […] }

​game_id: “81936672”

​move: Array(3) [ 15, 16, 2702 ]

​​0: 15

​​1: 16

​​2: 2702

length: 3

This is my code…

// main_time_left is a variable for testing.
// Currently we are not using it until we figure out
// if we need it.
function PlayOgsMoveSimple(game_id, move, main_time_left) {

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

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

}

In case you need it, this is the full script.

EDIT: When I play a game using Tsumego Dragon against another Tsumego Dragon client, through OGS, it works perfectly.

We also tried with socket.emit instead of websocket.send but got the same bug.

console.log('OGS Socket.io Script Started');
    let socket; // Renamed from webSocket for clarity
    let local_jwt;
    let request_id = 0;
    let game_id;
    let player_id;
    // let latency = 0; // Socket.io handles latency checks internally usually
    let connected = false;
    let callbacks = [];
    let ogsGame;
    let accessToken = localStorage.getItem("access_token");
    
    if(accessToken !== null) accessToken = accessToken.replace(/"/g, '');

    loadOGS();

    // Connect to OGS using Socket.io
    function loadOGS() {
        // 2. Initialize Socket.io
        // Transports are forced to websocket to avoid long-polling delays
        socket = io('https://online-go.com', {
            transports: ['websocket'],
            reconnection: true,
            reconnectionDelay: 1000,
            reconnectionDelayMax: 5000,
            reconnectionAttempts: Infinity
        });

        SubscribeToMessages(messageCallback, 'connection script');

        // 3. Handle Connection Events
        socket.on('connect', () => {
            console.log(`Socket.io Connected: ${Date.now()}`);
            // Note: We do NOT need to set a manual Ping interval. Socket.io handles heartbeats.
        });

        socket.on('disconnect', (reason) => {
            console.log('Connection Lost:', reason);
            if (reason === "io server disconnect") {
                // the disconnection was initiated by the server, you need to reconnect manually
                socket.connect();
            }
            // else the socket will automatically try to reconnect
        });

        socket.on('connect_error', (error) => {
             console.log('Connection Error:', error);
        });

        // 4. Catch-all listener to bridge Socket.io events to your existing 'OnMessage' architecture
        socket.onAny((eventName, ...args) => {
            // Socket.io separates the event name (type) from the data (args).
            // Your existing OnMessage expects (type, data).
            // args[0] is usually the data object in OGS events.
            const data = args.length > 0 ? args[0] : {};
            OnMessage(eventName, data);
        });
    }

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

    function ConfigIsLoaded(jwt, new_player_id) {
        player_id = new_player_id;
        local_jwt = jwt;
        
        // 5. Changed to .emit
        // The OGS socket.io event for auth is 'authenticate'
        socket.emit('authenticate', { "jwt": local_jwt });
    }

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

    function UnsubscribeFromMessages(callback) {
        // Check if remove exists (standard JS arrays don't have .remove by default, using filter)
        // If you have a prototype extension for .remove, keep it. Otherwise:
        const index = callbacks.indexOf(callback);
        if (index > -1) {
            callbacks.splice(index, 1);
        }
    }

    function OnMessage(type, data) {
        // Log flattened for readability
        // 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) {
        // Socket.io handles net/pong internally, so we rarely need to listen for it,
        // but if your UI expects it, we can leave this here.
        if (type === "net/pong") {
            bubble_fn_pong(1);
        }
        
        // Connected
        else if (type === "active-bots" || type === "hostinfo") {
            // 'hostinfo' or 'active-bots' are usually sent right after auth/connect
            if (!connected) {
                console.log("connected to ogs (logic flow)");
                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);
                    if(game_id_from_url) SubscribeToGame(game_id_from_url);
                });

                connected = true;
                if(typeof bubble_fn_connected === 'function') bubble_fn_connected(true);
            }
        } 
        else if (type === 2) { // Keep this if OGS sends raw integer types (rare in socket.io but possible)
            bubble_fn_online(data);
        }

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

        // Notification event
        else if (type === 'notification') {
            NotificationEvent(data);
        }
    }

    function NotificationEvent(data) {
        let type = data.type;
        let message = data.message;

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

    async function GetConfigAPICall() {
        console.log('Getting Config');
        // Ensure accessToken is valid before fetch
        if (!accessToken) {
            console.error("No access token found for config call.");
            return;
        }
        
        try {
            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.');
            this.ogsGame.SetPlayerId(data.user.id);
            ConfigIsLoaded(data.user_jwt, data.user.id);
        } catch (err) {
            console.error("Error fetching config:", err);
        }
    }

    function ResumeGameFromPause(gameId) {
        const data = {
            "auth": accessToken,
            "game_id": gameId,
            "player_id": player_id,
        };
        socket.emit("game/resume", data);
    }

    // Updated Simple Move Function using Socket.io
    function PlayOgsMoveSimple(game_id, move, main_time_left) {
        if (move === '..') {
            this.ogsGame.PassTurn();
            return;
        }

        const data = {
            "game_id": Number(game_id),
            "move": move
            // Note: request_id is usually not needed in socket.io emits 
            // unless the server specifically expects it in the payload.
        };

        // 6. Emit the move
        socket.emit("game/move", data);
        
        // If you need specific acknowledgement and OGS supports the ack callback pattern:
        // socket.emit("game/move", data, (response) => { console.log(response) });
        
        // Otherwise, we rely on the generic message listener to catch the server's response
    }

    function PassTurnOgs(game_id) {
        const data = {
            "game_id": game_id,
            "move": ".."
        };
        socket.emit("game/move", data);
    }

    function ResignOgsMove(game_id) {
        const data = { "game_id": game_id };
        socket.emit("game/resign", data);
    }

    function CancelOgsGame(game_id) {
        const data = { "game_id": game_id };
        socket.emit("game/cancel", data);
    }

    function SubscribeToGame(game_id) {
        this.ogsGame = new OGSG(game_id);
        // Ensure GetGameAPICall exists in your OGSG class
        if (this.ogsGame.GetGameAPICall) {
            this.ogsGame.GetGameAPICall(goban);
        }

        console.log('Subscribing: ' + game_id);
        
        // 7. Subscribe events
        socket.emit("game/connect", { 'game_id': game_id, 'chat': false });
        socket.emit("ui-pushes/subscribe", { 'channel': `game-${game_id}` });
        
        if(typeof bubble_fn_subscribed_to_game === 'function') bubble_fn_subscribed_to_game(game_id);
    }

    function AcceptBoardState(game_id, stones) {
        console.log('Accept Board State.');
        let removedStones = '';
        if (stones != null) { removedStones = stones; }
        
        socket.emit("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.');
        if(this.ogsGame) this.ogsGame.BoardClicked(goban, x, y);
    }

    function GetCurrentOGSGame() {
        return this.ogsGame;
    }

    function SetRemainingTime(black_time_expire, white_time_expire) {
        if(this.ogsGame) {
            this.ogsGame.black_time_expire = black_time_expire;
            this.ogsGame.white_time_expire = white_time_expire;
        }
    }

</script>

Further testing, I found that OGS is converting my game id to a string even though I am sending a number which causes this line in OGS’s code to fire an error.

The recommended fix according to ChatGPT is to add this line of code to OGS @anoek

2 Likes

Nice investigation, IMO this is surely an OGS bug (though there must be a diff between TD and the other clients if it’s only happening on TD)

The server should definitely normalize the type (or not accept bad data at all), and the official OGS client could probably also do some normalization as well.

1 Like

May I ask what code format it is in @Clossius1 ? (e.g. Python, C++, Java) With your permission obvs, could I take this code into my platforms and try and fix / tell you what’s up? I love coding and am happy to help you on this one! :grinning_face_with_smiling_eyes: :robot:

I’m using javascript. I can share the code but i’m using bubble io so i don’t think you could replicate it perfectly.

1 Like

When you connect to the game, are you sending a string or a number?

1 Like

Looks like string

As a data point, I consistently send game_id as a number whenever it’s relevant (connecting to a game, making moves, etc) and it works fine for me (also in JS). I can’t say whether there’s a bug in how the server handles things when you inconsistently send strings vs numbers, but at least consistently sending it as a number is not problematic.

A different thought: when I was having issues getting moves accepted properly (Debugging WS issues?) two issues turned out to be (a) sending an invalid JWT, which the WS authenticate message gave no feedback on other than not sending me notifications/active games/etc, and (b) a malformed game/connect request, which was silently failing and causing game/move messages to fail because you apparently have to be connected to a game in order to send a move in that game.

The latter issue might explain what’s going on — if you’re connecting to the game in a valid way, but connecting to a string game_id and sending moves to a numeric game_id, that sounds like it could possibly be a cut-and-dry “server bug that you can work around”.

1 Like

I triple checked I am sending a number.

EDIT: Rereading your question you asked about when I connected to the game, not when I sent the move. I misunderstood and thought you meant connected referring to when I sent the move.

That may have been true on socket.emit :thinking: That was a temporary script though. But I’m trying to use websocket as that has worked for everything else.

Edit: Looks like I didn’t make that mistake for the play move function where the problem is.

// Updated Simple Move Function using Socket.io
    function PlayOgsMoveSimple(game_id, move, main_time_left) {
        if (move === '..') {
            this.ogsGame.PassTurn();
            return;
        }

        const data = {
            "game_id": Number(game_id),
            "move": move
            // Note: request_id is usually not needed in socket.io emits 
            // unless the server specifically expects it in the payload.
        };

        // 6. Emit the move
        socket.emit("game/move", data);
        
        // If you need specific acknowledgement and OGS supports the ack callback pattern:
        // socket.emit("game/move", data, (response) => { console.log(response) });
        
        // Otherwise, we rely on the generic message listener to catch the server's response
    }

Holy crap I think you are right! I checked when I subscribed to the game…

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

I AM subscribing the the game with a string. So even though I sent a NUMBER to play a move, the subscription was a string so it seems like OGS might have been saving and reusing the string subscription instead of the number subscription for the move even though I was sending a number for the move.

The really really really annoying issue, is everything else worked! And the move worked SOMETIMES! lol So what would have been helpful is if there was an error thrown on the subscribe to game call that says it only accepts numbers instead of strings. And/or as mentioned above, the move call should convert strings to numbers just in case someone makes this stupid mistake like I did. xD

Updated function

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

@anoek thoughts?

2 Likes

Right, the code I quoted was for “game/connect”

1 Like

Yeah, that sounds like the exact issue I was experiencing when I was mistakenly calling game/connect with {id: [number] } instead of { game_id: [number] } — I was not receiving game updates, and moves would nondeterministically sometimes work.

I would be unsurprised if the connect call is silently failing with a string.

Even tho it seems you’ve solved it i’ve noticed other problems like this with my friends (just on different platforms). So plz can u send me the code? :grinning_face_with_smiling_eyes: Thanks!

P.s. will u send it via email or this page… ect,ect?

<body>
    
    <script>

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

        let webSocket;
        let local_jwt;
        let request_id = 0;
        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, '');
        let chat_lines = {};
        let chat_greeting = 'Hello, have a good game!';
        let chat_finish = 'Thanks for the game!';
        let greetingSent = false;
        let chatFinishedSent = false;

        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) => {
                console.log(event.data);
                if (ogsGame.phase != 'play') return;
                alert('Connection Lost');
            }
        }

        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 }]));
            SubscribeToChat(this.ogsGame.game_id);
        }

        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(Number(game_id_from_url));
                    });




                    connected = true;
                    bubble_fn_connected(true);

                }

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

            else if (type === 'user/jwt') {
                // We are authenticated.
                console.log('We are authenticated.');
                SubscribeToChat(this.game_id);
            }

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

                if (type.includes("/chat")) {
                    // This is a chat line in a game.
                    let temp_game_id = type.split('/')[1];

                    if (!chat_lines[temp_game_id]) {
                        chat_lines[temp_game_id] = [];
                    }

                    chat_lines[`${temp_game_id}`].push(data.line);
                    console.log(`Chat Line: Game: ${temp_game_id} User: ${data.line.username} Message: ${data.line.body}`);
                }

                else if (type === `game/${this.ogsGame.game_id}/data`) {
                    console.log(`Game finished: ${this.ogsGame.game_id}`);
                    if (data.phase != 'finished') return;
                    if (!this.chatFinishedSent) {
                        this.chatFinishedSent = true;
                        SendChatMessageToGame(this.ogsGame.game_id, chat_finish);
                    }
                }
            }

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

            else if (type === 'chat-join') {
                ChatJoined(data);
            }
        }
        

        function ChatJoined(data) {
            console.log(`Chat joined: ${data.channel}`);

            if (data.users[0] === undefined || data.users[1] === undefined) return;
            // Send a greeting when you join the chat on the first or second move
            // if you are a player in the game.
            if (data.users[0].id === player_id || data.users[1].id === player_id) {
                // Check for move number and send greeting.
                console.log('This chat is our game.');
                let moveNum = this.ogsGame.game_api_data.gamedata.moves.length;
                if (moveNum <= 1) {
                    if (greetingSent) return;
                    greetingSent = true;

                    // Send greeting.
                    console.log('Sending greeting.');
                    SendChatMessageToGame(Number(data.channel.split('-')[1]), chat_greeting);
                }
            }
        }

        function SendChatMessageToGame(tempGameId, message) {
            let moveNum = this.ogsGame.game_api_data.gamedata.moves.length;

            const temp_data = {
                "body": message,
                "type": 'main',
                "game_id": tempGameId,
                "move_number": moveNum
            };
            webSocket.send(JSON.stringify(["game/chat", temp_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') {
                if (data.game_id !== game_id) return;
                bubble_fn_game_started(data.game_id);
            } else if (type === 'gameResumedFromStoneRemoval') {
                if (data.game_id !== game_id) return;
                bubble_fn_game_resumed(data.game_id);
            }
        }

        function GameResumed() {
            console.log('Game Resumed');
        }

        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]));
        }

        function SubscribeToChat(game_id) {
            webSocket.send(JSON.stringify(["chat/join", { 'channel': `game-${game_id}` }]));
        }

        // Play a move in the game if you are the player.
        // Outdated function
        // TODO: Remove this function.
        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,
                    "timed_out": timed_out
                }
            };

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

        // main_time_left is a variable for testing.
        // Currently we are not using it until we figure out
        // if we need it.
        function PlayOgsMoveSimple(game_id, move, main_time_left) {

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

            const data = {
                "game_id": Number(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': true }]));
            webSocket.send(JSON.stringify(["ui-pushes/subscribe", { 'channel': `game-${game_id}` }]));
            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 this.ogsGame;
        }

        function SetRemainingTime(black_time_expire, white_time_expire) {
            this.ogsGame.black_time_expire = black_time_expire;
            this.ogsGame.white_time_expire = white_time_expire;
        }

    </script>
</body>
<script>

    class OGSG {

        time_control = '';
        game_api_data;
        wgg_game;
        game_id;
        current_player_id; // The player on tsumego dragon using this script. (ID#)
        current_user_id = -1; // The current player whose turn it is. (ID#)
        game_goban;
        phase;
        not_my_game = false;
        black_time_expire = 0;
        white_time_expire = 0;
        black_captures = 0;
        white_captures = 0;
        done_loading = false;

        alphabet = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];

        constructor(gameId) {
            this.game_id = gameId;
        }

        // When OGS send a message with 'game' in it, do this...
        GameMessage(type, data) {

            let gameId = type.split('/')[1];
            let type_ = type.split('/')[2];

            let data_string = JSON.stringify(data);

            let bubble_data = { 'output1': type_, 'output2': data_string };

            if (type_ === 'conditional_moves') {
                // Do nothing for now.
            }

            else if (type_ === 'move') {
                console.log('Move');
                console.log(data);

                let bubbleData = {
                    'output1': data.move_number,
                    'output2': data.move[0],
                    'output3': data.move[1]
                };

                bubble_fn_ogs_move_received(bubbleData);

                if (data.move[0] !== -1 && data.move[1] !== -1) {
                    PlayAudioClick();
                }

                if (this.current_player_id === this.current_user_id) {
                    let result = this.wgg_game.PlayMove(data.move[0], data.move[1]);
                    this.CheckCapture(result);


                    console.log(`Received OGS Move: ${result}`);
                    DisplayPosition(this.wgg_game.game.position.schema, goban_id);
                    SetLastMoveDisplay({ x: this.wgg_game.last_y, y: this.wgg_game.last_x });

                    if (data.move[0] === -1 && data.move[1] === -1) {
                        bubble_fn_pass(this.wgg_game.pass);
                        AddTextToMove('Pass', this.wgg_game.last_x, this.wgg_game.last_y);
                    }

                } else if (this.not_my_game) {
                    let result = this.wgg_game.PlayMove(data.move[0], data.move[1]);
                    this.CheckCapture(result);
                    console.log(`Received OGS Move: ${result}`);
                    DisplayPosition(this.wgg_game.game.position.schema, goban_id);
                    SetLastMoveDisplay({ x: this.wgg_game.last_y, y: this.wgg_game.last_x });

                    if (data.move[0] === -1 && data.move[1] === -1) {
                        bubble_fn_pass(this.wgg_game.pass);
                        AddTextToMove('Pass', this.wgg_game.last_x, this.wgg_game.last_y);
                    }
                }

                EverySecond();
            }

            else if (type_ === 'clock') {
                console.log('clock');
                console.log(data);

                OGSClockMessage(data);
                this.current_player_id = data.current_player;

                let pausing_player_id = data?.pause?.paused?.pause_control?.paused?.pausing_player_id ?? undefined;
                // Handle pauses
                if (pausing_player_id != undefined) {
                    console.log('Game Paused');
                    //bubble_fn_game_paused(pausing_player_id);
                    ResumeGameFromPause(this.game_id);
                }

                bubble_fn_current_player(this.current_player_id);
            }

            else if (type_ === 'latency') {
                console.log('Latency: ' + data.latency);
                bubble_fn_latency(data.latency);
            }

            else if (type_ === 'gamedata') {
                console.log('Phase: ' + data.phase);
                this.phase = data.phase;
                bubble_fn_phase(data.phase);

                if (data.phase === 'finished') {
                    console.log("OGS Finished");

                    // Send the player id of the winner.
                    bubble_fn_ogs_result(data.winner);
                }
            }

            // Disconnected from the game
            else if (type_ === "disconnect") {
                //alert("Disconnected from the game.");
            }

            // Received Stones Removed Accepted Event from opponent.
            else if (type_ === "removed_stones") {
                this.BoardStateEvent(data);
            }

            // Received Stones Removed Accepted Event from opponent.
            else if (type_ === "removed_stones_accepted") {
                this.BoardStateEvent(data);
            }

            else if (type_ === "phase") {
                console.log('Phase: ' + data);
                this.phase = data;
                bubble_fn_phase(data);
            }
        }

        CheckCapture(result) {
            if (Array.isArray(result)) {
                let captureCount = result.length;

                if (this.wgg_game.game.turn === WGo.B) this.white_captures += captureCount;
                else this.black_captures += captureCount;

                if (!this.done_loading) return;
                bubble_fn_captures({ 'output1': this.black_captures, 'output2': this.white_captures });
            }
        }

        PlayOGSMove(move, main_time_left) {
            PostOgsMove(this.game_id, move, main_time_left);
        }

        // Skip the current turn.
        PassTurn() {
            let passNum = this.wgg_game.PassTurn();
            PassTurnOgs(this.game_id);
            AddTextToMove('Pass', this.wgg_game.last_x, this.wgg_game.last_y);
            bubble_fn_pass(this.wgg_game.pass);
        }

        // Give up on the given game.
        Resign() {
            ResignOgsMove(this.game_id);
        }

        BoardClicked(goban, x, y) {
            // Check it's our turn.
            console.log(`Board Clicked OGS Game: ${this.current_player_id} ${this.current_user_id} ${this.phase}`);
            if (this.current_player_id != this.current_user_id) return;
            if (this.phase != 'play') return; // Optional TODO: handle stone removal phase.

            // Try the move in the game.
            let result = this.wgg_game.PlayMove(x, y);
            this.CheckCapture(result);

            DisplayPosition(this.wgg_game.game.position.schema, goban_id);
            SetLastMoveDisplay({ x: this.wgg_game.last_y, y: this.wgg_game.last_x });
            console.log(`Result: ${result}`);
            if (Array.isArray(result)) {
                // If successful convert to sgf coords
                let sgfCoord = `${this.alphabet[x]}${this.alphabet[y]}`;
                console.log(sgfCoord);
                // & PlayOGSMove
                let time_remaining = this.black_time_expire - Date.now();
                if (this.wgg_game.game.turn === WGo.B) {
                    time_remaining = this.white_time_expire - Date.now();
                }
                this.PlayMoveSimple(sgfCoord, time_remaining);
            }
        }

        // PlayMoveSimple
        PlayMoveSimple(move, time_remaining) {
            PlayOgsMoveSimple(this.game_id, move, time_remaining);
        }

        // The board state in stone removal phase
        // has updated.
        BoardStateEvent(data) {

            console.log('Board State Event.');
            if (data === undefined) return;
            if (data.player_id != undefined) {
                if (data.player_id === player_id) return;
            }

            let bubble = {
                'output1': this.game_id,
                'output2': data.stones
            };

            console.log('Opponent accepted removed stones: ' + data.stones);
            //bubble_fn_boardState(bubble);
            // Accept Board State
            MarkSgfStonesToRemove(data.stones);
            AcceptBoardState(this.game_id, data.stones);
        }

        async GetGameAPICall(goban) {

            console.log(`Local Get Game API Call`);
            const res = await fetch(`https://online-go.com/api/v1/games/${this.game_id}`, {

            });

            const data = await res.json();
            console.log('We got the Game.');
            console.log(data);
            this.game_api_data = data;

            GetConfigAPICall();

            SetBasicClockInformation(data.time_control, data.players.black.id, data.players.white.id, this);
            
            this.wgg_game = new WGG();
            this.wgg_game.CreateGame(data.width, 'KO');
            this.game_goban = goban;
            CreateGoban(data.width, this.game_id);

            // add event listeners
            goban.addEventListener("mousemove", game_board_mouse_move.bind(goban, this));
            goban.addEventListener("mouseout", game_board_mouse_out.bind(goban));

            if (data.gamedata.initial_player === 'white') this.wgg_game.game.turn = -1;

            this.SetStartingStones(data.gamedata.initial_state.black, 1, this.wgg_game.game);
            this.SetStartingStones(data.gamedata.initial_state.white, -1, this.wgg_game.game);

            let moves = data.gamedata.moves; // [[x, y, miliseconds]]
            for (let i = 0; i < moves.length; i++) {
                let result = this.wgg_game.PlayMove(moves[i][0], moves[i][1]);
                this.CheckCapture(result);
            }

            DisplayPosition(this.wgg_game.game.position.schema, goban_id);
            SetLastMoveDisplay({ x: this.wgg_game.last_y, y: this.wgg_game.last_x });
            if (moves.length > 0) {
                if (moves[moves.length - 1][0] === -1 && moves[moves.length - 1][1] === -1) {
                    AddTextToMove('Pass', this.wgg_game.last_x, this.wgg_game.last_y);
                }
            }

            wgoIsOGSGame = true;
            console.log('Done loading initial goban position.');
            this.done_loading = true;

            console.log(`Initial Captures: Black: ${this.black_captures} White: ${this.white_captures}`);
            bubble_fn_captures({ 'output1': this.black_captures, 'output2': this.white_captures });

            let bubbleData = {
                'output1': data.gamedata.moves.length,
                'output2': 0,
                'output3': 0
            };

            bubble_fn_ogs_move_received(bubbleData);

            bubble_fn_game_ready();
            EverySecond();
        }

        SetPlayerId(userId) {
            this.current_user_id = userId;
            let black_player = this.game_api_data.black;
            let white_player = this.game_api_data.white;

            if (black_player !== this.current_user_id &&
                white_player !== this.current_user_id) {

                console.log(`Not users game: ${this.current_user_id}`);
                this.not_my_game = true;
            } else { console.log(`This is users game: ${this.current_user_id}`); }
        }

        ConvertLetterToNum(letter) {
            for (let i = 0; i < this.alphabet.length; i++) {
                if (this.alphabet[i] === letter) return i;
            }

            return -1;
        }

        SetStartingStones(sgfMoves, color, game) {
            for (let i = 0; i < sgfMoves.length; i += 2) {
                let x = this.ConvertLetterToNum(sgfMoves[i]);
                let y = this.ConvertLetterToNum(sgfMoves[i + 1]);

                game.setStone(x, y, color);
            }
        }

        IsUsersTurn() {
            return this.current_player_id === this.current_user_id;
        }

        GameResumed() {
            this.wgg_game.GameResumed();
        }
    }


    // *************************************************
    // Below is code copied for mouse hovering behavior
    // *************************************************

    // dot drawing handler
    WGo.Board.drawHandlers.red_dot = {
        stone: {
            draw: function (args, board) {
                var xr = board.getX(args.x),
                    yr = board.getY(args.y),
                    sr = board.stoneRadius;

                this.fillStyle = "red";
                this.beginPath();
                this.arc(xr - board.ls, yr - board.ls, sr / 3, 0, 2 * Math.PI, true);
                this.fill();
            },
        },
    }

    WGo.Board.drawHandlers.outlineSecond = {
        stone: {
            draw: function (args, board) {
                console.log(args);
                args.c = args.playerColor;
                if (args.alpha) this.globalAlpha = args.alpha;
                else this.globalAlpha = 0.3;
                if (args.stoneStyle) Board.drawHandlers[args.stoneStyle].stone.draw.call(this, args, board);
                else board.stoneHandler.stone.draw.call(this, args, board);
                this.globalAlpha = 1;
            }
        }
    }

    // board mousemove callback
    var game_board_mouse_move = function (ogsGameReference, x, y) {

        // set playerColor;
        let playerColor;
        if (ogsGameReference.current_user_id === ogsGameReference.game_api_data.black) playerColor = 1;
        else if (ogsGameReference.current_user_id === ogsGameReference.game_api_data.white) playerColor = -1;
        else playerColor = 0;

        // mouse position didn't change
        if (_lastX == x && _lastY == y) return;
        if (_last_mark) {
            // clean everything
            goban.removeObject(_last_mark);
            _last_mark = null;
            _lastX = -1;
            _lastY = -1;
        }
        // Not your turn
        if (!ogsGameReference.IsUsersTurn() || ogsGameReference.phase !== 'play') {
            _lastX = -1;
            _lastY = -1;

            return;
        }
        if (x < 0) return;
        if (y < 0) return;
        // Check if empty position
        if (ogsGameReference.wgg_game.game.getStone(x, y) !== 0) {
            _lastX = -1;
            _lastY = -1;

            return;
        }
        // remove old dot
        if (_last_mark) {
            goban.removeObject(_last_mark);
            delete _last_mark;
        }

        // Check for valid move
        if (ogsGameReference.wgg_game.game.isValid(x, y) === false) {
            console.log('Adding Red Dot.');
            _last_mark = {
                type: "red_dot",
                x: x,
                y: y,
            };
            goban.addObject(_last_mark);

            return;
        }


        // Don't remove the last move played in the game.


        // save current position
        _lastX = x;
        _lastY = y;
        console.log(6);
        if (x != -1 && y != -1) {
            console.log('Creating Stone Graphic');
            _last_mark = {
                type: "outlineSecond",
                x: x,
                y: y,
                playerColor: playerColor
            };
            goban.addObject(_last_mark);
        }
    }

    // board mouseout callback
    var game_board_mouse_out = function () {

        if (_last_mark) {
            // clean everything
            goban.removeObject(_last_mark);
            delete _last_mark;
            delete _lastX;
            delete _lastY;
        }
    }

</script>
1 Like

Thank you so much! :heart_eyes:

This will help A LOT!

-Clink

1 Like