import { reactive, ref, computed, onMounted, toRaw } from "vue"; import { getQuery, setKirby } from "../../kirby.js"; import { handleActive, ArrowVerticalKeyHandler, ArrowHorizontalKeyHandler, NumberKeyHandler } from "../../handlers.js"; import { overlayAndGet, powerStateMachine, overlayAndPop, popLastElem } from "../../componentPromise.js"; import { getGameProps, initGame, initStats, storeVisit, formatDate, removeLastVisit, extension, getFinalWinner, playerFromUUID } from "./logic.js"; const html = (v) => { return v[0] }; //////////////////////////////////////////////////////////////////////////////// // Components //////////////////////////in////////////////////////////////////////////////////// const pregame = { props: ['page','active', 'stack'], setup(props, context) { handleActive(props, [ArrowVerticalKeyHandler]); const selectPlayer = async (i) => { if (props.active) { let [result, error] = await overlayAndPop("d-select", { type: "player", options: props.page.participants, class:"overlay"}, props.stack); if (error === undefined) { props.page.players[i] = result; } } } const selectScorer = async (i) => { if (props.active) { const [scorer, error] = await overlayAndPop("d-select", { type: "scorer", options: props.page.participants, class:"overlay"}, props.stack); if (error === undefined) { if (scorer.member.length > 1){ const [captain, err] = await overlayAndPop("d-select", { type: "player", title: "Team Captain", options: [scorer.member[0],scorer.member[1]], class:"overlay"}, props.stack); if (err !== undefined){ return selectScorer(i); } scorer.member = [captain, scorer.member[0] == captain ? scorer.member[1] : scorer.member[0]]; } props.page.scorers[i] = scorer; } } } const selectX01 = async () => { if (props.active) { const [result, error] = await overlayAndPop("d-select", { options: [ "301", "501" ], class:"overlay"}, props.stack); if (error === undefined) { props.page.modus = result; } } } const selectIn = async () => { if (props.active) { const [result, error] = await overlayAndPop("d-select", { options: [ "Straight", "Double" ], class:"overlay"}, props.stack); if (error === undefined) { props.page.in = result; } } } return { selectX01, selectIn, selectScorer } }, template: html` ` } export const bullselect = { props: ['scorers', 'active', 'stack'], setup(props, { emit }) { handleActive(props, [ArrowHorizontalKeyHandler, NumberKeyHandler]); const children = computed(() => { const items = []; for (let i in props.scorers){ const player = props.scorers[i].member[0]; items.push({ component: "d-squareElem", props: { text: player.forename + " " + player.surname, icon: player.img ? player.img : '/assets/img/placeholder_person.png' }, onClick: () => { emit("resolve", props.scorers[i]); } }) } return items; }); return { children } }, template: html`

Who won bull?

` } const score = { props: ['page', 'justlegs', 'current_set_points', 'current_leg_points'], setup(props) { return { } }, template: html`

{{ current_set_points[idx] }}

Best of {{ page.sets }}

Sets

{{ current_leg_points[idx] }}

Best of {{ page.legs }}

Legs

` } const scorer = { props: ['page', 'id', 'current_stat', 'current_leg'], setup(props) { const addStats = (stat1, stat2) => { let res = { average: [stat1.average[0]+stat2.average[0], stat1.average[1]+stat2.average[1]], first9: [stat1.first9[0]+stat2.first9[0], stat1.first9[1]+stat2.first9[1]], "60+": stat1["60+"]+stat2["60+"], "100+": stat1["100+"]+stat2["100+"], "140+": stat1["140+"]+stat2["140+"], "180": stat1["180"]+stat2["180"], "checkouts": [stat1.checkouts[0]+stat2.checkouts[0], stat1.checkouts[1]+stat2.checkouts[1]], "checkoutPoints": [...stat1.checkoutPoints, ...stat2.checkoutPoints] } if ("checkins" in stat1 && "checkins" in stat2){ res["checkins"] = [stat1.checkins[0]+stat2.checkins[0], stat1.checkins[1]+stat2.checkins[1]]; res["checkinPoints"] = [...stat1.checkinPoints, ...stat2.checkinPoints]; } return res; } const inspect = !!props.page.enddate; const tourStats = computed(() => { if (inspect) { return props.page.tournamentStats; } return props.page.tournamentStats.map((s,i) => { if (s.length == 0){ return props.page.stats.stats[i]; } return addStats(s,props.page.stats.stats[i]) }); }); const getAverage = (avg) => { return avg && avg[1] != 0 ? ((3*avg[0])/avg[1]).toFixed(1) : "-"; } const getCheckout = (checkout) => { return checkout && checkout[1] != 0 ? Math.round(1000*checkout[0]/checkout[1])/10 : "- " } const getMax = (checkouts) => { if (checkouts && checkouts.length != 0) { return Math.max(...checkouts); } return "-" } const scorer = ref(props.page.scorers[props.id]); const allScorerIndices = computed(() => scorer.value.member.map((p) => props.page.stats.stats.map((s) => s.player).indexOf(p.uuid))); const currentScorerIdx = computed(() => { if (scorer.value.member.length == 1){ return 0; } const idx = props.page.scorers[props.id].member.map((p) => p.uuid).indexOf(props.current_leg.visits.at(-1).player); return idx; }); const tnmStats = computed(() => { if (currentScorerIdx.value == -1){ return addStats(tourStats.value[allScorerIndices.value[0]], tourStats.value[allScorerIndices.value[1]]); } return tourStats.value[allScorerIndices.value[currentScorerIdx.value]]; }); const matchStats = computed(() => { if (currentScorerIdx.value == -1){ return addStats(props.page.stats.stats[allScorerIndices.value[0]], props.page.stats.stats[allScorerIndices.value[1]]); } return props.page.stats.stats[allScorerIndices.value[currentScorerIdx.value]]; }); const legStats = computed(() => { if (currentScorerIdx.value == -1){ return addStats(props.current_stat.stats[allScorerIndices.value[0]], props.current_stat.stats[allScorerIndices.value[1]]); } return props.current_stat.stats[allScorerIndices.value[currentScorerIdx.value]]; }); return { tnmStats, matchStats, legStats, getAverage, getCheckout, getMax, scorer, currentScorerIdx } }, template: html`
Stat
Tnm
Match
Leg
Avg:
{{ getAverage(tnmStats.average) }}
{{ getAverage(matchStats.average) }}
{{ getAverage(legStats.average) }}
First 9:
{{ getAverage(tnmStats.first9) }}
{{ getAverage(matchStats.first9) }}
{{ getAverage(legStats.first9) }}
60+:
{{ tnmStats["60+"] }}
{{ matchStats["60+"] }}
{{ legStats["60+"] }}
100+:
{{ tnmStats["100+"] }}
{{ matchStats["100+"] }}
{{ legStats["100+"] }}
140+:
{{ tnmStats["140+"] }}
{{ matchStats["140+"] }}
{{ legStats["140+"] }}
180:
{{ tnmStats["180"] }}
{{ matchStats["180"] }}
{{ legStats["180"] }}
Ch. I. %:
{{ getCheckout(tnmStats.checkins) }}%
{{ getCheckout(matchStats.checkins) }}%
{{ getCheckout(legStats.checkins) }}%
Best Ch.:
{{ getMax(tnmStats.checkinPoints) }}
{{ getMax(matchStats.checkinPoints) }}
{{ getMax(legStats.checkinPoints) }}
Ch. O. %:
{{ getCheckout(tnmStats.checkouts) }}%
{{ getCheckout(matchStats.checkouts) }}%
Best Ch.:
{{ getMax(tnmStats.checkoutPoints) }}
{{ getMax(matchStats.checkoutPoints) }}
` } const game = { props: ['active', 'stack', 'scorers', 'current_leg', 'max'], setup(props, context) { const visits = computed(() => props.current_leg ? props.current_leg.visits : undefined ); const getScorersVisits = (scorer) => { // const vs = visits.value.filter((v,i) => i % 2 == props.scorers.map((p) => p.uuid).indexOf(uuid) % 2); const vs = visits.value.filter((v,i) => scorer.member.map((p) => p.uuid).indexOf(v.player) != -1); if (vs.length < 9) { for (var i = vs.length; i < 9; i++) { vs.push({ "sum": "", "toGo":["",""]}) } } else if (vs.length*2 < visits.value.length) { vs.push({ "sum": "", "toGo":["",""]}); } return vs; } return { getScorersVisits } }, template: html`
Points
ToGo
Round
Points
ToGo
{{ max }}
0
{{ max }}
` } const gameinput = { props: ['input','active', 'stack'], setup(props, context) { handleActive(props, []); const check_remove = (event) => { if (!event.repeat && event.target.value.length < 1) { event.preventDefault(); context.emit('reject', -2); } } const keyhandler = (e) => { if (e.key == "F1" || e.keyCode == 112) { e.preventDefault(); context.emit('resolve', "60"); } else if (e.key == "F2" || e.keyCode == 113) { e.preventDefault(); context.emit('resolve', "45"); } else if (e.key == "F3" || e.keyCode == 114) { e.preventDefault(); context.emit('resolve', "41"); } else if (e.key == "F4" || e.keyCode == 115) { e.preventDefault(); context.emit('resolve', "26"); } else if (e.key == "/" || e.keyCode == 111 || e.key == "r") { e.preventDefault(); context.emit('resolve', -1*parseFloat(e.target.value)); } } return { check_remove, keyhandler } }, template: html` ` } const xoi = { props: ['page', 'active', 'stack', 'inspect', 'watch'], components: { "d-scorer": scorer, "d-score": score, "d-game": game }, setup(props, context) { const gamestack = reactive([]); const inspectstack = reactive([]); let current_set, current_leg; const set_id = ref(props.page.game.sets.length - 1); const leg_id = ref(props.page.game.sets[set_id.value].legs.length - 1); let current_stat; if (props.inspect) { current_set = computed(() => props.page.game.sets[set_id.value]); current_leg = computed(() => current_set.value.legs[leg_id.value]); current_stat = computed(() => props.page.stats?.sets[set_id.value].legs[leg_id.value]); } if (props.watch) { setInterval(async () => { let gs = await refreshGame(props.page.id); props.page.game = gs.game; props.page.stats = gs.stats; }, 2000); } const computedProps = getGameProps(props.page, current_set, current_leg); if (!props.inspect) { current_stat = computed( () => props.page.stats?.sets[props.page.stats.sets.length-1].legs[computedProps.current_set.value.legs.length-1]); } const mounted = onMounted(async () => { if (props.inspect || props.watch) { } else { const winner = await gameHandler(gamestack, props.stack, props.page, computedProps) if (winner === undefined){ context.emit('reject', winner); } else { context.emit('resolve', winner); } } }) return { ...computedProps, set_id, leg_id, gamestack, inspectstack, current_stat } }, template: html`
{{ current_toGo[0] }}
{{ current_toGo[1] }}
` } //////////////////////////////////////////////////////////////////////////////// // Dialogs //////////////////////////////////////////////////////////////////////////////// const numDartsDialog = () => { const buttons = []; for (var i = 1; i <= 3; i++) { buttons.push({ "component": "d-plainElem", "props" : { "text": `${i}` }, "result" : i }) } return { "withshortkey": true, "title": "Congratulations!", "text": `How many darts did you need?`, "buttons": buttons }; } const numCheckoutTriesDialog = (numDarts, start=1) => { const buttons = []; for (var i = start; i <= numDarts; i++) { buttons.push({ "component": "d-plainElem", "props" : { "text": `${i}`, "autofocus": i == 0, "data-tabindex": i }, "result" : i }) } return { "withshortkey": true, "title": "Checkout Tries", "text": `How many tries on a Checkout?` , "buttons": buttons }; } const numCheckinTriesDialog = (numDarts, start=1) => { const buttons = []; for (var i = start; i <= numDarts; i++) { buttons.push({ "component": "d-plainElem", "props" : { "text": `${i}`, "autofocus": i == 0, "data-tabindex": i }, "result" : i }) } return { "withshortkey": true, "title": "Checkin Tries", "text": `How many tries on a Checkin?` , "buttons": buttons }; } const impossibleDialog = (sum) => { return { "withshortkey": true, "title": "Impossible", "text": `A score of ${sum} is not possible`, "buttons": [{ "component": "d-plainElem", "props" : { "text": "ok" }, "result" : "ok" }]} } const gameOverDialog = (winner, points, breaks) => { return { "withshortkey": true, "title": `Game Over`, "text": `${winner != "DRAW" ? "The winner is": ""} ${winner} with ${points[0]}-${points[1]} (Breaks: ${breaks[0]}-${breaks[1]})`, "buttons": [ { "component": "d-plainElem", "props" : { "text": "End Game" }, "result" : "end" },{ "component": "d-plainElem", "props" : { "text": "Continue" }, "result" : "continue" }] } } //////////////////////////////////////////////////////////////////////////////// // API //////////////////////////////////////////////////////////////////////////////// function refreshGame(id){ return getQuery(`site.find('${id}')`, { select: { game: "page.rounds.parseJSON", stats: "page.stats.parseJSON", tournamentStats: "page.tournamentStats" } }) } async function reloadGame(page){ let gs = await refreshGame(page.id); page.game = gs.game; page.stats = gs.stats; page.tournamentStats = gs.tournamentStats; } function getGame(id){ const playerInfo = { forename: "page.forename", surname: "page.surname", nickname: "page.nickname", uuid: "page.uuid", img: "page.pic.toFile?.thumbnail(350).url" }; return getQuery(`site.find('${id}')`, { select: { title: "page.title", id: "page.id", modus: "page.max.toInt", in: "page.in", out: "page.out", game: "page.rounds.parseJSON", stats: "page.stats.parseJSON", sets: "page.sets.toInt", legs: "page.legs.toInt", startdate: "page.Startdate", enddate: "page.Enddate", tournamentStats: "page.tournamentStats", participants: { query: "page.parent.scorers", select: { member: { query: "structureItem.member.toPages", select: playerInfo } } }, scorers: { query: "page.scorers.toStructure", select: { member: { query: "structureItem.member.toPages", select: playerInfo } } } } }) } function savePregame(page){ return setKirby(page.id, { sets: page.sets, legs: page.legs, max: page.modus, in: page.in, scorers: page.scorers.map((s) => { const copy = Object.assign({},s); copy.member = copy.member.map((p) => p.uuid) return copy }), startdate: page.startdate ? page.startdate : "", rounds: page.game ? JSON.stringify(page.game) : "", stats: page.stats ? JSON.stringify(page.stats) : "" }); } function saveGame(page){ return setKirby(page.id, { rounds: page.game ? JSON.stringify(page.game) : "", stats: page.stats ? JSON.stringify(page.stats) : "", enddate: page.enddate ? page.enddate : "", }); } //////////////////////////////////////////////////////////////////////////////// // State Machine //////////////////////////////////////////////////////////////////////////////// // Checkout Pipeline const checkoutPipeline = (stack) => { return { 0: async (sum, reGet) => { // Ask for num Darts const [numDarts, error] = await overlayAndGet("d-dialog", numDartsDialog(), stack, reGet); if (error != undefined) { popLastElem(stack); return [undefined, [-1, -1]]; } return [1, [numDarts, 1]]; }, 1: async ([numDarts, start], reGet) => { // Ask for checkoutTries if (numDarts <= start) { // Store with one checkout try popLastElem(stack); return [undefined, [1, 1]]; } const [tries, error] = await overlayAndPop("d-dialog", numCheckoutTriesDialog(numDarts, start), stack); if (error != undefined) { if (start == 0) { return [undefined, [-1, -1]]; } return [-1, undefined]; } if (start == 1) { popLastElem(stack); } return [undefined, [numDarts, tries]]; } }; }; // Game State Machine const gameStateMachine = (gamestack, stack, page, computedProps) => { return { 0: async (input, reGet) => { // Dispatcher if (page.stats.winner){ return [undefined, page.stats.winner]; } // Check for Game State return [1, undefined]; }, 1: async (input, reGet) => { // Normal Game Loop // Get Game Input let [visit, error] = await overlayAndPop("d-gameinput", { input: input }, gamestack); if (parseFloat(visit) < 0 ){ visit = ""+(computedProps.current_toGo.value[computedProps.current_scorer_idx.value * 1] - parseFloat(visit)*-1); } // back/delete last throw if (error != undefined) { const val = removeLastVisit(page); if (val === undefined) { return [undefined, undefined]; } saveGame(page); if (val == undefined) return [1, val]; return [1, val.join(",")]; } // Validate throw let numDarts, tries; const [ret, sum] = computedProps.checkVisit(visit); if (ret == -1) { // Impossible const [_, error] = await overlayAndPop("d-dialog", impossibleDialog(sum), stack); return [1, visit]; } else if (ret == -2) { // Bust TODO const [_, error] = await overlayAndPop("d-dialog", impossibleDialog(sum), stack); return [1, visit]; } else if (ret == 3) { // Checkins const [ret, error] = await overlayAndPop("d-dialog", numCheckinTriesDialog(3), stack); if (error) { return [1, visit]; } storeVisit(page, visit.split(","), sum, 3, 0, ret); saveGame(page); return [1, undefined] } else if (ret == 2) { // Normal storeVisit(page, visit.split(","), sum, 3, 0); saveGame(page); return [1, undefined] } else if (ret == 0) { // Checkout: Ask for num Darts [numDarts, tries] = await powerStateMachine(checkoutPipeline(stack), stack, /*initState=*/0, /*initInput=*/sum); } else if (ret == 1) { // <=50: Ask for checkout tries [numDarts, tries] = await powerStateMachine(checkoutPipeline(stack), stack, /*initState=*/1, /*initInput=*/[3,0]); } if (numDarts == -1) { // Error/Back return [1, visit]; } else { const points = storeVisit(page, visit.split(","), sum, numDarts, tries); if (points){ // Game Over const winner = getFinalWinner(page); let name = "DRAW" if (winner != -2){ name = page.scorers[winner].member.map((p) => p.forename).join(" + "); } const [answer, error] = await overlayAndPop("d-dialog", gameOverDialog(name, points[1], computedProps.breaks.value), stack); if (error != undefined) { extension(page); const val = removeLastVisit(page); saveGame(page); if (val == undefined) return [1, val]; return [1, val.join(",")]; } if (answer == "end") { saveGame(page); return [undefined, page.stats.winner]; } else { extension(page); saveGame(page); } } else { saveGame(page); } return [1, undefined]; } }, }; } export const gameHandler = async (gamestack, stack, page, computedProps) => { const sm = gameStateMachine(gamestack, stack, page, computedProps); return powerStateMachine(sm, gamestack); } function getUrlParam(name) { var url_string = window.location; let url = new URL(url_string); let params = new URLSearchParams(url.search); return params.get(name); } // General State Machine const stateMachine = (stack, page) => { return { 0: async (input, reGet) => { // Dispatcher // Check if in watchmode // Check Game State: if ( page.scorers.length != 2 || page.startdate === undefined || page.startdate === ""){ // Pre Game return [1, page]; } else if (page.enddate === undefined || page.enddate === ""){ if (page.game === "" || page.game === undefined || page.game === null) { // Who won Bull? return [2, page]; } else { // In Game if (getUrlParam("w") != null){ // Watch Game return [5, undefined]; } return [3, page]; } } else { // Post Game return [4, undefined]; } }, 1: async (page, reGet) => { // Pre Game const [result, error] = await overlayAndPop("d-pregame", { page: page}, stack); if (error != undefined) { var re = /^https?:\/\/[^/]+/i; window.setTimeout(() => { window.location.href = re.exec(window.location.href)[0]; return false; },1); return [undefined, undefined]; } page.startdate = formatDate(new Date(Date.now())); // Update game in database const ret = await savePregame(page); if (ret.status != "ok") { console.error("Error save page:", ret.status, ret.error); } return [0, page]; }, 2: async (page, reGet) => { // Ask for Bull const [scorer, error] = await overlayAndPop("d-bullselect", { scorers: page.scorers}, stack); if (error != undefined) { page.startdate = undefined; const ret = await savePregame(page); return [1, page]; } // reorder Scorer if (scorer !== page.scorers[0]){ page.scorers = [page.scorers[1], page.scorers[0]]; } // Setup Game initGame(page); initStats(page); const ret = await savePregame(page); await reloadGame(page); return [3, page]; }, 3: async (page, reGet) => { // In Game const [result, error] = await overlayAndPop("d-xoi", { page: page, inspect: false }, stack); if (error) { page.game = undefined; page.stats = undefined; const ret = await savePregame(page); return [2, page]; } return [4, result]; }, 4: async (winnerUUID, reGet) => { const [res, e] = await overlayAndPop("d-xoi", { page: page, inspect: true }, stack); var re = /^https?:\/\/[^/]+/i; window.setTimeout(() => { window.location.href = re.exec(window.location.href)[0]; return false; },1); return [4, res]; }, 5: async (_, reGet) => { const [res, e] = await overlayAndPop("d-xoi", { page: page, watch: true }, stack); var re = /^https?:\/\/[^/]+/i; window.setTimeout(() => { window.location.href = re.exec(window.location.href)[0]; return false; },1); return [5, res]; } }; } //////////////////////////////////////////////////////////////////////////////// // Exports //////////////////////////////////////////////////////////////////////////////// export const initXoiView = (app) => { app.component('d-pregame', pregame).component('d-xoi', xoi).component('d-bullselect', bullselect).component('d-gameinput', gameinput) } export const xoiHandler = async (stack, id) => { const page = reactive(await getGame(id)); const sm = stateMachine(stack, page); await powerStateMachine(sm, stack); }