Skip to content

Instantly share code, notes, and snippets.

@Bannerets
Last active April 29, 2023 10:13
Show Gist options
  • Save Bannerets/9a8bce3ae2ea5e82bab935c037b893ac to your computer and use it in GitHub Desktop.
Save Bannerets/9a8bce3ae2ea5e82bab935c037b893ac to your computer and use it in GitHub Desktop.
A bookmarklet to convert Hex boards from Board Game Arena to HexWorld

Create a bookmark with this URL:

javascript:void function(){const e="https://hexworld.org/board/#",r=window.location.href,n=r.match(/\/table\?table=(\d+)/),o=r.match(/\/gamereview\?table=(\d+)/),t=r.match(/\/replay\/.*?\/\?table=\d+/);function a(e){return e.match(/\/(\d+\/hex)\?table=(\d+)/)}const i=a(r);let s="";function l(e){let r,n;return e.reduce((e,r)=>[...e,...r.data],[]).map(({type:e,args:o,log:t})=>{if("message"===e&&o.size){o.red&&!r?r=String(o.red):console.warn('No "args.red"');const e=o.size.match(/(\d+)x\d+/);if(e)return e[1]+"c1,"}if("playToken"===e){r||(r=o.player_id);const e=n==o.player_id?":p"+o.coord:o.coord;return n=o.player_id,e}return"swapPieces"===e?(n=o.player_id,":s"):"pass"===e?(n=o.player_id,":p"):"resign"===e||"playerConcedeGame"===e?o.player_id==r?":rb":":rw":void 0}).join("")}function c(e){return fetch(window.location.origin+e,{cache:"no-cache",headers:{"X-Request-Token":s}})}function d(e,r,n){return c(e?`/archive/archive/logs.html?table=${r}`:`/${n}/hex/notificationHistory.html?table=${r}&from=1&privateinc=1&history=1`).then(e=>e.json()).then(r=>{if("0"===String(r.status)&&r.error)throw new Error(`Error while fetching the move history: ${r.error}`);const n=e?r.data.logs:r.data.data;return console.info("Events:",n),l(n)})}function h(r,n,o){const t=window.open("about:blank");t.document.write("<p>Loading...</p>"),d(r,n,o).catch(e=>{if(e instanceof Error&&e.message.includes("Cannot find gamenotifs log file of an archived table"))return function(e){return console.info(`Requesting table archives for #${e}`),c(`/gamereview/gamereview/requestTableArchive.html?table=${e}`).then(e=>e.json()).then(e=>{if(1!==e.status){if(e.error)throw new Error(`Error while requesting the table archive: ${e.error}`);console.warn("result.status !== 1",e)}})}(n).then(()=>d(r,n,o));throw e}).then(r=>{console.info(`HexWorld hash: ${r}`),t.location.href=e+r}).catch(e=>{if(t.close(),console.error(e),String(e).includes("is not valid JSON"))return window.alert("Error. Try to reload the page.");window.alert(String(e))})}if("undefined"==typeof bgaConfig?console.error('No "bgaConfig" variable found, cannot get the request token'):s=bgaConfig.requestToken,i){const e=i[1];h(!1,i[2],e)}else{if(n){const e=n[1],r=document.querySelector("#access_game_panel");if(r&&"none"!==r.style.display){const e=document.querySelector("#view_end_btn");if(!e||!e.href)return window.alert("Error: Cannot find #view_end_btn");const r=a(e.href);return r?void h(!1,r[2],r[1]):window.alert(`Cannot parse the url: ${e.href}`)}return r||console.warn("Cannot detect if the game is finished."),void h(!0,e)}if(o)h(!0,o[1]);else if(t){if("undefined"==typeof g_gamelogs||!g_gamelogs)return window.alert('Error: no "g_gamelogs" variable.');const r=l(g_gamelogs);window.open(e+r)}else window.alert("No table found. Use this bookmark while viewing a Hex game on Board Game Arena.")}}();

Click it while viewing a Hex table.

It can be executed on /hex, /table, /gamereview, or /archive/replay pages.

Updates

  • 2023-04-29: Fixed the "cannot read property 'data' of undefined" error on finished tables caused by changes on BGA
  • 2022-06-04: Fixed errors on finished / in progress tables caused by x-request-token changes on BGA
void function () {
const HEXWORLD_URL = 'https://hexworld.org/board/#'
const currentUrl = window.location.href
const tablePage = currentUrl.match(/\/table\?table=(\d+)/)
const gamereviewPage = currentUrl.match(/\/gamereview\?table=(\d+)/)
const replayPage = currentUrl.match(/\/replay\/.*?\/\?table=\d+/)
function parseGameInProgressUrl (url) {
return url.match(/\/(\d+\/hex)\?table=(\d+)/)
}
const gameInProgressPage = parseGameInProgressUrl(currentUrl)
let requestToken = ''
if (typeof bgaConfig === 'undefined')
console.error('No "bgaConfig" variable found, cannot get the request token')
else
requestToken = bgaConfig.requestToken
function convertGameLog (events) {
let black
let lastMove
return events
.reduce((acc, event) => [...acc, ...event.data], [])
.map(({ type, args, log }) => {
if (type === 'message' && args.size) {
if (args.red && !black) black = String(args.red)
else console.warn('No "args.red"')
const matched = args.size.match(/(\d+)x\d+/)
if (matched) return matched[1] + 'c1,'
}
if (type === 'playToken') {
if (!black) black = args.player_id
const result = lastMove == args.player_id // Can possibly happen if a player runs out of time
? ':p' + args.coord
: args.coord
lastMove = args.player_id
return result
}
if (type === 'swapPieces') {
lastMove = args.player_id
return ':s'
}
if (type === 'pass') {
lastMove = args.player_id
return ':p'
}
if (type === 'resign' || type === 'playerConcedeGame') {
return args.player_id == black ? ':rb' : ':rw'
}
})
.join('')
}
function fetchBga (url) {
return fetch(window.location.origin + url, {
cache: 'no-cache',
headers: {
'X-Request-Token': requestToken
}
})
}
function requestTableArchive (tableId) {
console.info(`Requesting table archives for #${tableId}`)
return fetchBga(`/gamereview/gamereview/requestTableArchive.html?table=${tableId}`)
.then(res => res.json())
.then(result => {
if (result.status === 1) return
if (result.error) throw new Error(`Error while requesting the table archive: ${result.error}`)
console.warn('result.status !== 1', result)
})
}
function fetchMoveHistory (finished, tableId, pathStart) {
const url = finished
? `/archive/archive/logs.html?table=${tableId}`
: `/${pathStart}/hex/notificationHistory.html?table=${tableId}&from=1&privateinc=1&history=1`
return fetchBga(url)
.then(res => res.json())
.then(result => {
if (String(result.status) === '0' && result.error) {
throw new Error(`Error while fetching the move history: ${result.error}`)
}
const events = finished ? result.data.logs : result.data.data
console.info('Events:', events)
return convertGameLog(events)
})
}
function fetchInNewTab (finished, tableId, pathStart) {
const win = window.open('about:blank') // Open a tab in advance not to trigger the popup blocker
win.document.write('<p>Loading...</p>')
fetchMoveHistory(finished, tableId, pathStart)
.catch(err => {
if (err instanceof Error && err.message.includes('Cannot find gamenotifs log file of an archived table'))
return requestTableArchive(tableId).then(() => fetchMoveHistory(finished, tableId, pathStart))
throw err
})
.then(hash => {
console.info(`HexWorld hash: ${hash}`)
win.location.href = HEXWORLD_URL + hash
})
.catch(err => {
win.close()
console.error(err)
if (String(err).includes('is not valid JSON'))
return window.alert('Error. Try to reload the page.')
window.alert(String(err))
})
}
if (gameInProgressPage) {
const pathStart = gameInProgressPage[1]
const tableId = gameInProgressPage[2]
fetchInNewTab(false, tableId, pathStart)
return
}
if (tablePage) {
const tableId = tablePage[1]
const goToGameEl = document.querySelector('#access_game_panel')
if (goToGameEl && goToGameEl.style.display !== 'none') {
const hiddenEndEl = document.querySelector('#view_end_btn')
if (!hiddenEndEl || !hiddenEndEl.href) return window.alert('Error: Cannot find #view_end_btn')
const matched = parseGameInProgressUrl(hiddenEndEl.href)
if (!matched) return window.alert(`Cannot parse the url: ${hiddenEndEl.href}`)
fetchInNewTab(false, matched[2], matched[1])
return
}
if (!goToGameEl) console.warn('Cannot detect if the game is finished.')
fetchInNewTab(true, tableId)
return
}
if (gamereviewPage) {
fetchInNewTab(true, gamereviewPage[1])
return
}
if (replayPage) {
if (typeof g_gamelogs === 'undefined' || !g_gamelogs)
return window.alert('Error: no "g_gamelogs" variable.')
const hash = convertGameLog(g_gamelogs)
window.open(HEXWORLD_URL + hash)
return
}
window.alert('No table found. Use this bookmark while viewing a Hex game on Board Game Arena.')
}()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment