/** * Tsumego (go problems) viewer for WGo.js. * It requires files: wgo.js, player.js, sgfparser.js, kifu.js */ (function (WGo) { "use strict"; function vhToPx(vh) { const viewportHeight = window.innerHeight || document.documentElement.clientHeight; return (vh * viewportHeight) / 100; } // decide whether variation is good or bad var evaluate_variation_rec = function (node) { var tmp, val = 0; if (node.children.length) { // node has descendants for (var i = 0; i < node.children.length; i++) { // extract the best variation tmp = evaluate_variation_rec(node.children[i]); if (tmp > val) val = tmp; } } else { // node is a leaf if (node.DO) val = 1; // doubtful move (not entirely incorrect) else if (node.IT) val = 2; // interesting move (not the best one but correct solution) else if (node.TE) val = 3; // correct solution } // store in the node as integer node._ev = val; return val; }; var get_coordinates = function (sgf_cords) { return [sgf_cords.charCodeAt(0) - 97, sgf_cords.charCodeAt(1) - 97]; }; // function extending board clicks var board_click = function (x, y) { if (this.config.questiontype == 6 || this.config.questiontype == 7) { this.kifuReader.game.turn = 1; if (!this.kifuReader.game.isValid(x, y)) { this.addPos.push({ x: x, y: y }); } if ( this.frozen || this.turn != this.kifuReader.game.turn || this.kifuReader.node.children.length == 0 || !this.kifuReader.game.isValid(x, y) ) return; this.addPos.push({ x: x, y: y }); this.kifuReader.node.appendChild( new WGo.KNode({ move: { x: x, y: y, c: this.kifuReader.game.turn, }, _ev: -1, }) ); this.next(this.kifuReader.node.children.length - 1); } else { if ( this.frozen || this.turn != this.kifuReader.game.turn || this.kifuReader.node.children.length == 0 || !this.kifuReader.game.isValid(x, y) ) return; this.kifuReader.node.appendChild( new WGo.KNode({ move: { x: x, y: y, c: this.kifuReader.game.turn, }, _ev: -1, }) ); this.next(this.kifuReader.node.children.length - 1); } }; /** * Class containing logic for tsumego mode (backend). */ var TsumegoApi = WGo.extendClass(WGo.Player, function (config) { this.config = config; // add default configuration of TsumegoApi for (var key in TsumegoApi.default) if (this.config[key] === undefined && TsumegoApi.default[key] !== undefined) this.config[key] = TsumegoApi.default[key]; // add default configuration of Player class for (var key in WGo.Player.default) if (this.config[key] === undefined && WGo.Player.default[key] !== undefined) this.config[key] = WGo.Player.default[key]; // create element with board - it can be inserted to DOM this.boardElement = document.createElement("div"); this.board = new WGo.Board(this.boardElement, this.config); // Player object has to contain element. this.element = this.element || this.boardElement; this.upper_left = [0, 0]; this.lower_right = [18, 18]; this.init(); this.board.addEventListener("click", board_click.bind(this)); this.listeners.variationEnd = [function () {}]; this.listeners.nextMove = [function () {}]; }); /** * Overrides loading of kifu, we must decide correct and incorrect variations and who turn it is. */ TsumegoApi.prototype.loadKifu = function (kifu, path) { // analyze kifu if (kifu.root.children.length && kifu.root.children[0].move) { this.turn = kifu.root.children[0].move?.c; } for (var i = 0; i < kifu.root.children.length; i++) { evaluate_variation_rec(kifu.root.children[i]); } this.kifu = kifu; this.kifuReader = new WGo.KifuReader(this.kifu, this.config.rememberPath, this.config.allowIllegalMoves); // fire kifu loaded event this.dispatchEvent({ type: "kifuLoaded", target: this, kifu: this.kifu, }); this.update("init"); if (path) { this.goTo(path); } // execute VW property if (kifu.info.VW) { var cords = kifu.info.VW.split(":"); this.upper_left = get_coordinates(cords[0]); this.lower_right = get_coordinates(cords[1]); var upper_left = [this.upper_left[0], this.upper_left[1]]; var lower_right = [this.lower_right[0], this.lower_right[1]]; if (this.coordinates) { if (this.upper_left[0] == 0) upper_left[0] = -0.5; if (this.upper_left[1] == 0) upper_left[1] = -0.5; if (this.lower_right[0] == 18) lower_right[0] = 18.5; if (this.lower_right[1] == 18) lower_right[1] = 18.5; } this.board.setSection( upper_left[1], this.board.size - 1 - lower_right[0], this.board.size - 1 - lower_right[1], upper_left[0] ); } }; /** * Overrides player's next() method. We must play 1 extra move */ TsumegoApi.prototype.next = function (i) { if (this.delay) return true; if (this.config.questiontype == 6 || this.config.questiontype == 7) { try { this.kifuReader.next(i); this.update(); this.dispatchEvent({ type: "nextMove", target: this, node: this.kifuReader.node, evaluation: this.kifuReader.node._ev, }); if (!this.onestep && this.kifuReader.node?.move?.c == this.turn && this.kifuReader.node.children.length) { var _this = this; setTimeout(() => { _this.delay = true; }, 0); window.setTimeout(function () { if (_this.kifuReader.node?.move?.c == _this.turn) { try { _this.kifuReader.next(0); // _this.update(); } catch (err) { console.log(err); _this.error(err); return; } if (_this.kifuReader.node.children.length == 0) { _this.dispatchEvent({ type: "variationEnd", target: _this, node: _this.kifuReader.node, evaluation: _this.kifuReader.node._ev, }); } _this.delay = false; } }, this.config.answerDelay); } else if (this.kifuReader.node.children.length == 0) { this.dispatchEvent({ type: "variationEnd", target: this, node: this.kifuReader.node, evaluation: this.kifuReader.node._ev, }); } } catch (err) { this.error(err); } } else { if (this.frozen || !this.kifu || this.kifuReader.node.children.length == 0) return; try { this.kifuReader.next(i); this.update(); this.dispatchEvent({ type: "nextMove", target: this, node: this.kifuReader.node, evaluation: this.kifuReader.node._ev, }); if (this.kifuReader.node?.move?.c == this.turn && this.kifuReader.node.children.length) { var _this = this; setTimeout(() => { _this.delay = true; }, 0); window.setTimeout(function () { if (_this.kifuReader.node?.move?.c == _this.turn) { try { // 백돌 놓기 전에 소리 재생 const app = document.querySelector('#app').__vue__; if (app && app.$store) { app.$store.dispatch('sEffect/soundEffectPlay', 'move'); } _this.kifuReader.next(0); _this.update(); } catch (err) { console.log(err); _this.error(err); return; } if (_this.kifuReader.node.children.length == 0) { _this.dispatchEvent({ type: "variationEnd", target: _this, node: _this.kifuReader.node, evaluation: _this.kifuReader.node._ev, }); } } _this.delay = false; }, this.config.answerDelay); } else if (this.kifuReader.node.children.length == 0) { this.dispatchEvent({ type: "variationEnd", target: this, node: this.kifuReader.node, evaluation: this.kifuReader.node._ev, }); } } catch (err) { this.error(err); } } }; TsumegoApi.prototype.setCoordinates = function (b) { if (!this.coordinates && b) { var upper_left = [this.upper_left[0], this.upper_left[1]]; var lower_right = [this.lower_right[0], this.lower_right[1]]; if (this.upper_left[0] == 0) upper_left[0] = -0.5; if (this.upper_left[1] == 0) upper_left[1] = -0.5; if (this.lower_right[0] == 18) lower_right[0] = 18.5; if (this.lower_right[1] == 18) lower_right[1] = 18.5; this.board.setSection( upper_left[0], this.board.size - 1 - lower_right[0], this.board.size - 1 - lower_right[1], upper_left[1] ); // this.board.setWidth(this.board.width); this.board?.setHeight?.(vhToPx(65)); this.board.addCustomObject(WGo.Board.coordinates); } else if (this.coordinates && !b) { this.board.setSection( this.upper_left[0], this.board.size - 1 - this.lower_right[0], this.board.size - 1 - this.lower_right[1], this.upper_left[1] ); this.board.removeCustomObject(WGo.Board.coordinates); } this.coordinates = b; }; TsumegoApi.default = { movePlayed: undefined, // callback function of move played by a player endOfVariation: undefined, // callback function for end of a variation (it can be solution of the problem or incorrect variation) answerDelay: 500, // delay of the answer (in ms) enableWheel: false, // override player's setting lockScroll: false, // override player's setting enableKeys: false, // override player's setting rememberPath: false, // override player's setting displayVariations: false, // override player's setting }; WGo.TsumegoApi = TsumegoApi; var generate_dom = function () { // clean up this.element.innerHTML = ""; // main wrapper this.wrapper = document.createElement("div"); this.wrapper.className = "wgo-tsumego"; this.element.appendChild(this.wrapper); // top part this.top = document.createElement("div"); this.comment = document.createElement("div"); this.comment.className = "wgo-tsumego-comment"; this.top.appendChild(this.comment); this.wrapper.appendChild(this.top); // board center part this.center = document.createElement("div"); this.wrapper.appendChild(this.center); this.center.appendChild(this.boardElement); // this.board.setWidth(this.center.offsetWidth); this.board?.setHeight?.(vhToPx(65)); // bottom part this.bottom = document.createElement("div"); this.bottom.className = "wgo-tsumego-bottom"; this.wrapper.appendChild(this.bottom); // control panel this.controlPanel = document.createElement("div"); this.controlPanel.className = "wgo-tsumego-control"; this.bottom.appendChild(this.controlPanel); // previous button this.resetWrapper = document.createElement("div"); this.resetWrapper.className = "wgo-tsumego-btnwrapper"; this.controlPanel.appendChild(this.resetWrapper); this.resetButton = document.createElement("button"); this.resetButton.className = "wgo-tsumego-btn"; this.resetButton.innerHTML = "다시 풀기"; this.resetButton.addEventListener("click", this.reset.bind(this)); this.resetWrapper.appendChild(this.resetButton); // previous button this.prevWrapper = document.createElement("div"); this.prevWrapper.className = "wgo-tsumego-btnwrapper"; this.controlPanel.appendChild(this.prevWrapper); this.prevButton = document.createElement("button"); this.prevButton.className = "wgo-tsumego-btn"; this.prevButton.innerHTML = "한 수 물림"; this.prevButton.addEventListener("click", this.undo.bind(this)); this.prevWrapper.appendChild(this.prevButton); // hint button this.hintWrapper = document.createElement("div"); this.hintWrapper.className = "wgo-tsumego-btnwrapper hint"; if (this.config.displayHintButton) this.controlPanel.appendChild(this.hintWrapper); this.hintButton = document.createElement("button"); this.hintButton.className = "wgo-tsumego-btn"; this.hintButton.innerHTML = "Hint"; this.hintButton.addEventListener("click", this.hint.bind(this)); this.hintWrapper.appendChild(this.hintButton); this.hint = this.hint.bind(this); this.reset = this.reset.bind(this); this.prev = this.undo.bind(this); this.correct = false; this.delay = false; this.ended = false; }; /** * Simple front end for TsumegoApi. It provides all html but isn't very adjustable. */ var Tsumego = WGo.extendClass(WGo.TsumegoApi, function (elem, config) { this.element = elem; this.super.call(this, config); if (this.config.questiontype == 6 || this.config.questiontype == 7) { this.cross = false; //클릭 시 X 표기 this.onestep = false; this.addPos = []; } // add default configuration of Tsumego for (var key in Tsumego.default) if (config[key] === undefined && Tsumego.default[key] !== undefined) this.config[key] = Tsumego.default[key]; generate_dom.call(this); this.listeners.update.push(this.updateTsumego.bind(this)); this.listeners.variationEnd.push(this.variationEnd.bind(this)); this.eventHandler = (event) => { if (!this.element || !document.body.contains(this.element)) { this.removeEventListeners(); } else { this.updateDimensions(); } }; window.addEventListener("resize", this.eventHandler); let visibilityChange; if (typeof document.hidden !== "undefined") { visibilityChange = "visibilitychange"; } else if (typeof document.msHidden !== "undefined") { visibilityChange = "msvisibilitychange"; } else if (typeof document.webkitHidden !== "undefined") { visibilityChange = "webkitvisibilitychange"; } window.addEventListener(visibilityChange, this.eventHandler); this.observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.removedNodes.forEach((removedNode) => { if (removedNode === this.element) { this.removeEventListeners(); this.observer.disconnect(); } }); }); }); this.observer.observe(document.body, { childList: true, subtree: true }); window.addEventListener("popstate", this.eventHandler); window.addEventListener("hashchange", this.eventHandler); // show variations if (this.config.debug) { this.variationLetters = []; this.listeners.update.push(this.showVariations.bind(this)); } this.initGame(); this.updateDimensions(); }); Tsumego.prototype.setCross = function (val) { this.cross = val; }; Tsumego.prototype.setOnestep = function (val) { this.onestep = val; }; Tsumego.prototype.updateTsumego = function (e) { if (e.node.comment) this.setInfo(WGo.filterHTML(e.node.comment)); // else this.comment.innerHTML = (this.turn == WGo.B ? "Black" : "White")+" to play"; else this.comment.innerHTML = this.turn == WGo.B ? "흑 차례입니다." : "백 차례입니다."; // by tomato 20221105 if (e.node.children.length == 0) this.hintButton.disabled = "disabled"; else this.hintButton.disabled = ""; if (!e.node.parent) { this.resetButton.disabled = "disabled"; this.prevButton.disabled = "disabled"; } else { this.resetButton.disabled = ""; this.prevButton.disabled = ""; } this.setClass(); }; Tsumego.prototype.setInfo = function (msg) { if (msg == "정답") { this.correct = true; } else if (msg == "오답") { this.correct = false; } this.comment.innerHTML = msg; }; Tsumego.prototype.setClass = function (className) { this.wrapper.className = "wgo-tsumego" + (className ? " " + className : ""); }; Tsumego.prototype.reset = function () { if (this.config.questiontype != 6 && this.config.questiontype != 7) this.first(); }; Tsumego.prototype.undo = function () { this.previous(); if (this.kifuReader.node.move && this.kifuReader.node?.move?.c == this.turn) { this.previous(); } }; Tsumego.prototype.hint = function (e) { for (var i in this.kifuReader.node.children) { if (this.kifuReader.node.children[i]._ev == 3) { this.next(i); return; } } this.setInfo("Already wrong variation! Try again."); }; Tsumego.prototype.variationEnd = function (e) { setTimeout(() => { this.ended = true; }, 10); if (!e.node.comment) { switch (e.node._ev) { case 0: this.setInfo("Incorrect solution! Try again."); break; case 1: this.setInfo("There is a better way to solve this! Try again."); break; case 2: this.setInfo("Correct solution, but there is a better move."); break; case 3: this.setInfo("You have solved it!"); break; default: this.setInfo("실패"); break; } } switch (e.node._ev) { case 0: this.setClass("wgo-tsumego-incorrect"); break; case 1: this.setClass("wgo-tsumego-doubtful"); break; case 2: this.setClass("wgo-tsumego-interesting"); break; case 3: this.setClass("wgo-tsumego-correct"); break; default: this.setClass("wgo-tsumego-unknown"); break; } }; Tsumego.prototype.showVariations = function (e) { // remove old variations this.board.removeObject(this.variationLetters); // show variations this.variationLetters = []; for (var i = 0; i < e.node.children.length; i++) { if (e.node.children[i].move && e.node.children[i].move?.c == this.turn && !e.node.children[i].move.pass) this.variationLetters.push({ type: "LB", text: String.fromCharCode(65 + i), x: e.node.children[i].move.x, y: e.node.children[i].move.y, c: e.node.children[i]._ev == 3 ? "rgba(0,128,0,0.8)" : "rgba(196,0,0,0.8)", }); } this.board.addObject(this.variationLetters); }; /** * Set right width of board. */ Tsumego.prototype.updateDimensions = function () { // this.board.setWidth(this.center.offsetWidth); setTimeout(() => { let size = 65; if (this.element?.className?.includes("wgo-content-")) { size = 20; } else if (this.element?.className?.includes("tsumego-multiple-wrap")) { size = 40; } else if (this.element?.className?.includes("wgo-choice")) { //보기 사이즈 조정 size = 15; } const parent = this.element?.parentElement; if (parent) { // 부모의 가로, 세로 중 작은 값으로 최대 크기 제한 const maxSize = Math.min(parent.offsetWidth, parent.offsetHeight || parent.offsetWidth); // size(px)와 maxSize(px) 중 작은 값 사용 const finalSize = Math.min(vhToPx(size), maxSize); this.board?.setHeight?.(finalSize); this.board.setWidth(finalSize); } else { // 부모가 없으면 기존 방식대로 this.board?.setHeight?.(vhToPx(size)); if (this?.board?.width > this?.element?.offsetWidth) { this?.board?.setWidth(this?.element?.offsetWidth); } } //실제 canvas 크기가 더 커지면 조정 }); }; TsumegoApi.prototype.removeEventListeners = function () { window.removeEventListener("resize", this.eventHandler); let visibilityChange; if (typeof document.hidden !== "undefined") { visibilityChange = "visibilitychange"; } else if (typeof document.msHidden !== "undefined") { visibilityChange = "msvisibilitychange"; } else if (typeof document.webkitHidden !== "undefined") { visibilityChange = "webkitvisibilitychange"; } window.removeEventListener(visibilityChange, this.eventHandler); window.removeEventListener("popstate", this.eventHandler); window.removeEventListener("hashchange", this.eventHandler); }; // Tsumego viewer settings Tsumego.default = { displayHintButton: true, debug: false, }; WGo.Tsumego = Tsumego; })(WGo);