<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>