diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..3bd3c48 --- /dev/null +++ b/build.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +rm dist/webxdc-go.xdc 2> /dev/null +# idk why zipping from project dir retains src/ prefix but it does +# and this makes the resultant .xdc unreadable to client +cd src +zip -9 --recurse-paths "../dist/webxdc-go.xdc" . +cd .. diff --git a/dist/myapp.xdc b/dist/myapp.xdc deleted file mode 100644 index 2b1c886..0000000 Binary files a/dist/myapp.xdc and /dev/null differ diff --git a/dist/webxdc-go.xdc b/dist/webxdc-go.xdc new file mode 100644 index 0000000..6209d29 Binary files /dev/null and b/dist/webxdc-go.xdc differ diff --git a/package.json b/package.json index b5e4612..a58fc54 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "webxdc-dev": "^0.9.0" }, "scripts": { - "build": "zip -9 --recurse-paths dist/myapp.xdc src", + "build": "./build.sh", "dev": "webxdc-dev run --port 4000 src" } } diff --git a/src/audio/go_loud.wav b/src/audio/go_loud.wav new file mode 100644 index 0000000..a2dfb8f Binary files /dev/null and b/src/audio/go_loud.wav differ diff --git a/src/audio/go_loud_2.wav b/src/audio/go_loud_2.wav new file mode 100644 index 0000000..c234296 Binary files /dev/null and b/src/audio/go_loud_2.wav differ diff --git a/src/audio/go_loud_3.wav b/src/audio/go_loud_3.wav new file mode 100644 index 0000000..c0c3336 Binary files /dev/null and b/src/audio/go_loud_3.wav differ diff --git a/src/audio/go_loud_4.wav b/src/audio/go_loud_4.wav new file mode 100644 index 0000000..ff1b313 Binary files /dev/null and b/src/audio/go_loud_4.wav differ diff --git a/src/audio/go_soft.wav b/src/audio/go_soft.wav new file mode 100644 index 0000000..7df37f9 Binary files /dev/null and b/src/audio/go_soft.wav differ diff --git a/src/audio/go_soft_2.wav b/src/audio/go_soft_2.wav new file mode 100644 index 0000000..07a62fb Binary files /dev/null and b/src/audio/go_soft_2.wav differ diff --git a/src/audio/go_soft_3.wav b/src/audio/go_soft_3.wav new file mode 100644 index 0000000..6ebd408 Binary files /dev/null and b/src/audio/go_soft_3.wav differ diff --git a/src/audio/go_soft_4.wav b/src/audio/go_soft_4.wav new file mode 100644 index 0000000..d802f6c Binary files /dev/null and b/src/audio/go_soft_4.wav differ diff --git a/src/audio/go_soft_5.wav b/src/audio/go_soft_5.wav new file mode 100644 index 0000000..288e7a0 Binary files /dev/null and b/src/audio/go_soft_5.wav differ diff --git a/src/audio/go_soft_6.wav b/src/audio/go_soft_6.wav new file mode 100644 index 0000000..b1de353 Binary files /dev/null and b/src/audio/go_soft_6.wav differ diff --git a/src/audio/go_soft_7.wav b/src/audio/go_soft_7.wav new file mode 100644 index 0000000..c1376f8 Binary files /dev/null and b/src/audio/go_soft_7.wav differ diff --git a/src/images/black-stones-bowl.jpg b/src/images/black-stones-bowl.jpg new file mode 100644 index 0000000..7eeea5c Binary files /dev/null and b/src/images/black-stones-bowl.jpg differ diff --git a/src/images/board.png b/src/images/board.png new file mode 100644 index 0000000..37fcd07 Binary files /dev/null and b/src/images/board.png differ diff --git a/src/images/white-stones-bowl.jpg b/src/images/white-stones-bowl.jpg new file mode 100644 index 0000000..850bca5 Binary files /dev/null and b/src/images/white-stones-bowl.jpg differ diff --git a/src/index.html b/src/index.html index 2db6a44..ad1c071 100644 --- a/src/index.html +++ b/src/index.html @@ -1,67 +1,103 @@ - - - Hello - - - - - - -

Hello

-
- - -
-

-

- + + Browser Go + + + + +
+

Pass?

+
+

Browser Go

+

Resign?

+
+
+

+
+
+
+
+ + + +
+
+
+
+

Pass?

+
+

by oxaliq

+

Resign?

+
+
+
+
+
+ diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..596b92f --- /dev/null +++ b/src/main.js @@ -0,0 +1,834 @@ +/*----- constants -----*/ +const STONES_DATA = { + '-1': 'white', + '0': 'none', + '1': 'black', + 'k': 'ko' +}; + +const DOTS_DATA = { + '-1': 'white', + '0': 'none', + '1': 'black', + 'd': 'dame', +}; + +const RANKS = [ + '30k', '29k', '28k', '27k', '26k', '25k', '24k', '23k', '22k', '21k', '20k', + '19k', '18k', '17k', '16k', '15k', '14k', '13k', '12k', '11k', '10k', + '9k', '8k', '7k', '6k', '5k', '4k', '3k', '2k', '1k', + '1d', '2d', '3d', '4d', '5d', '6d', '7d', '8d', '9d' +]; +// index corresponds to difference in player rank +const KOMI_REC = { + '9': [ + 5.5, 2.5, -0.5, -3.5, -6.5, -9.5, 12.5, 15.5, 18.5, 21.5 + ], + '13': [ + 5.5, 0.5, -5.5, 0.5, -5.5, 0.5, -5.5, 0.5, -5.5, 0.5 + ], + '19': [ + 7.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5 + ] +}; + +const HANDI_REC = { + '9': [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ], + '13': [ + 0, 0, 0, 2, 2, 3, 3, 4, 4, 5 + ], + '19': [ + 0, 0, 2, 3, 4, 5, 6, 7, 8, 9 + ] +}; + +const PLACEMENT_SOUNDS = { + soft: [ + 'audio/go_soft.wav', + 'audio/go_soft_2.wav', + 'audio/go_soft_3.wav', + 'audio/go_soft_4.wav', + 'audio/go_soft_5.wav', + 'audio/go_soft_6.wav', + 'audio/go_soft_7.wav' + ], + loud: [ + 'audio/go_loud.wav', + 'audio/go_loud_2.wav', + 'audio/go_loud_3.wav', + 'audio/go_loud_4.wav', + ] +}; + +const gameState = { // pre-init values (render prior to any player input) + winner: null, + turn: null, // turn logic depends on handicap stones + pass: null, // -1 represents state in which resignation has been submitted, not confirmed + komi: null, // komi depends on handicap stones + player rank + handicap: null, + boardSize: null, + playerState: { + bCaptures: null, + wCaptures: null, + bScore: null, + wScore: null + }, + gameMeta: { // declared at game start and not editable after + date: null, // contains metadata + start: false + }, + playerMeta: { // editable during game + b: { + name: null, + rank: null, + rankCertain: false + }, + w: { + name: null, + rank: null, + rankCertain: false + }, + }, + groups: {}, + gameRecord : [] +}; + +// index represents handicap placement for different board-sizes, eg handiPlace['9][1] = { (3, 3), (7, 7) } +// last array in each property also used for hoshi rendering +const HANDI_PLACE = { + '9' : [ + 0, 0, + [[ 7, 3 ], [ 3, 7 ] ], + [ [ 7, 7 ], [ 7, 3 ], [ 3, 7 ] ], + [ [ 3, 3 ], [ 7, 7 ], [ 3, 7 ], [ 7, 3 ] ] + ], + '13' : [ + 0, 0, + [ [ 4, 10 ], [ 10, 4 ] ], + [ [ 10, 10 ], [ 4, 10 ], [ 10, 4] ], + [ [ 4, 4 ], [ 10, 10 ], [ 4, 10 ], [ 10, 4] ], + [ [ 7, 7 ], [ 4, 4 ], [ 10, 10 ], [ 4, 10 ], [ 10, 4] ], + [ [ 7, 4 ], [ 4, 7 ], [ 4, 4 ], [ 10, 10 ], [ 4, 10 ], [ 10, 4] ], + [ [ 7, 7 ], [ 7, 4 ], [ 4, 7 ], [ 4, 4 ], [ 10, 10 ], [ 4, 10 ], [ 10, 4] ], + [ [ 10, 7 ], [ 7, 4 ], [ 7, 10 ], [ 4, 7 ], [ 4, 4 ], [ 10, 10 ], [ 4, 10 ], [ 10, 4] ], + [ [ 7, 7 ], [ 10, 7 ], [ 7, 4 ], [ 7, 10 ], [ 4, 7 ], [ 4, 4 ], [ 10, 10 ], [ 4, 10 ], [ 10, 4] ], + ], + '19' : [ + 0, 0, + [ [ 4, 16 ], [ 16, 4 ] ], + [ [ 16, 16 ], [ 4, 16 ], [ 16, 4] ], + [ [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ], + [ [ 10, 10 ], [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ], + [ [ 10, 4 ], [ 4, 10 ], [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ], + [ [ 10, 10 ], [ 10, 4 ], [ 4, 10 ], [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ], + [ [ 16, 10 ], [ 10, 4 ], [ 10, 16 ], [ 4, 10 ], [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ], + [ [ 10, 10 ], [ 16, 10 ], [ 10, 4 ], [ 10, 16 ], [ 4, 10 ], [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ], + ] +}; + +class Point { + constructor(x, y) { + this.pos = [ x, y ]; + this.stone = 0; // this is where move placement will go 0, 1, -1, also contains ko: 'k' + this.legal = true; + this.territory = null; + this.capturing = []; + this.groupMembers = [ this ]; + this.neighbors = { + top: {}, + btm: {}, + lft: {}, + rgt: {} + }; + this.neighbors.top = x > 1 ? [ x - 1, y ] : null; + this.neighbors.btm = x < gameState.boardSize ? [ x + 1, y ] : null; + this.neighbors.rgt = y < gameState.boardSize ? [ x, y + 1 ] : null; + this.neighbors.lft = y > 1 ? [ x, y - 1 ] : null; + } + checkNeighbors = () => { + let neighborsArr = []; + for (let neighbor in this.neighbors) { + let nbr = this.neighbors[neighbor]; + // neighbor exists it's point is stored as { rPos, cPos} + if ( nbr !== null ) { + neighborsArr.push(boardState.find(pt => pt.pos[0] === nbr[0] && pt.pos[1] === nbr[1])); + } + }; + // returns array of existing neighbors to calling function + return neighborsArr; + } + getLiberties = () => { + let neighborsArr = this.checkNeighbors().filter(pt => pt.stone === 0); + return neighborsArr; + } + joinGroup = () => { + this.groupMembers = this.groupMembers.filter(grp => grp.stone === this.stone); + this.groupMembers.push(this); + let frns = this.checkNeighbors().filter(nbr => nbr.stone === this.stone); + for (let frn of frns) { + this.groupMembers.push(frn); + } + this.groupMembers = Array.from(new Set(this.groupMembers)); + for (let grpMem in this.groupMembers) { + this.groupMembers = Array.from(new Set(this.groupMembers.concat(this.groupMembers[grpMem].groupMembers))); + } + for (let grpMem in this.groupMembers) { + this.groupMembers[grpMem].groupMembers = Array.from(new Set(this.groupMembers[grpMem].groupMembers.concat(this.groupMembers))); + } + } + checkCapture = () => { + let opps = this.checkNeighbors().filter(nbr => nbr.stone === gameState.turn * -1 + && nbr.getLiberties().every(liberty => liberty === this)); + for (let opp of opps) { + if (opp.groupMembers.every(stone => stone.getLiberties().filter(liberty => liberty !== this).length === 0)) { + this.capturing = this.capturing.concat(opp.groupMembers); + }; + } + this.capturing = Array.from(new Set(this.capturing)); + return this.capturing; + } + checkGroup = () => { // liberty is true when called by move false when called by check Capture + let frns = this.checkNeighbors().filter(nbr => nbr.stone === gameState.turn); + for (let frn in frns) { + if (frns[frn].groupMembers.find(stone => stone.getLiberties().find(liberty => liberty !== this))) return true; + continue; + } + } + cycleTerritory = () => { + if (this.stone) { + this.groupMembers.forEach(pt => pt.territory = pt.territory * -1); + } else { + this.groupMembers.forEach(pt => { + switch (pt.territory) { + case 1: + pt.territory = -1; + break; + case -1: + pt.territory = 'd'; + break; + case 'd': + pt.territory = 1; + break; + } + }); + } + } +} + +/*----- app's state (variables) -----*/ +let boardState = []; + + +/*----- cached element references -----*/ +const whiteCapsEl = document.getElementById('white-caps'); +const blackCapsEl = document.getElementById('black-caps'); +const modalEl = document.querySelector('.modal'); +const komiSliderEl = document.querySelector('input[name="komi-slider"]'); +const handiSliderEl = document.querySelector('input[name="handicap-slider"]'); +const blackRankEl = document.getElementById('black-rank'); +const blackRankUpEl = document.getElementById('black-rank-up'); +const blackRankDownEl = document.getElementById('black-rank-down'); +const whiteRankEl = document.getElementById('white-rank'); +const whiteRankUpEl = document.getElementById('black-rank-up'); +const whiteRankDownEl = document.getElementById('black-rank-down'); +const blackNameInputEl = document.querySelector('input[name="black-name"]'); +const whiteNameInputEl = document.querySelector('input[name="white-name"]'); +const blackNameDisplayEl = document.querySelector('h4#black-player-name'); +const whiteNameDisplayEl = document.querySelector('h4#white-player-name'); +const gameHudEl = document.querySelector('#game-hud p'); +const dateEl = document.getElementById('date'); +const boardSizeEl = document.getElementById('board-size-radio'); +const komiDisplayEl = document.getElementById('komi'); +const handiDisplayEl = document.getElementById('handicap'); +const boardEl = document.querySelector('#board tbody'); +const gameStartEl = document.querySelector('input[name="game-start"]'); +const komiSuggestEl = document.querySelector('input[name="komi-suggest"]'); +const soundPlayerEl = new Audio(); +const boardSizeRadioEls = [ + document.querySelectorAll('input[name="board-size"')[0], + document.querySelectorAll('input[name="board-size"')[1], + document.querySelectorAll('input[name="board-size"')[2] +]; + +/*----- event listeners -----*/ +document.getElementById('board').addEventListener('mousemove', hoverPreview); +document.getElementById('board').addEventListener('click', clickBoard); +document.getElementById('white-bowl').addEventListener('click',clickPass); +document.getElementById('black-bowl').addEventListener('click',clickPass); +document.getElementById('kifu').addEventListener('click', clickMenuOpen); +document.getElementById('white-caps-space').addEventListener('click', clickResign); +document.getElementById('black-caps-space').addEventListener('click', clickResign); +modalEl.addEventListener('click', clickCloseMenu); +komiSliderEl.addEventListener('change', changeUpdateKomi); +handiSliderEl.addEventListener('change', changeUpdateHandicap); +document.getElementById('player-meta').addEventListener('click', clickUpdatePlayerMeta); +document.getElementById('player-meta').addEventListener('change', clickUpdatePlayerMeta); +komiSuggestEl.addEventListener('click', clickKomiSuggestion); +gameHudEl.addEventListener('click', clickGameHud); +boardSizeEl.addEventListener('click', clickBoardSize); +gameStartEl.addEventListener('click', clickSubmitStart); + +/*----- FUNCTIONS ----------------------------------*/ +/*----- init functions -----*/ +init(); + +function init() { + gameState.gameMeta.date = getDate(); + gameState.komi = 5.5; + gameState.handicap = 0; + gameState.winner = null; + gameState.pass = null; + gameState.boardSize = 19; + gameState.playerState.bCaptures = 0; + gameState.playerMeta.b.rank = 21; + gameState.playerState.wCaptures = 0; + gameState.playerMeta.w.rank = 21; + gameState.gameRecord = []; + boardState = []; + gameState.gameMeta.start = false; + startMenu(); +}; + +function getDate() { + let d = new Date; + return `${d.getFullYear()}-${String(d.getMonth()+1).charAt(-1)||0}${String(d.getMonth()+1).charAt(-0)}-${String(d.getDate()).charAt(-1)||0}${String(d.getDate()+1).charAt(-0)}`; +} + +function startMenu() { + modalEl.style.visibility = 'visible'; + renderMenu(); +} +function clickSubmitStart(evt) { + if (gameState.gameMeta.start) return init(); + evt.preventDefault(); + evt.stopPropagation(); + gameState.playerMeta.b.name = blackNameInputEl.value || 'black'; + gameState.playerMeta.w.name = whiteNameInputEl.value || 'white'; + modalEl.style.visibility = 'hidden'; + return initGame(); +} + +function initGame() { + gameState.winner = null; + gameState.pass = null; + gameState.turn = gameState.handicap ? -1 : 1; + gameState.gameMeta.start = true; + initBoard(); + renderBoardInit(); + renderGame(); +} + +function initBoard() { + let i = 0; + while (i < gameState.boardSize * gameState.boardSize) { + let point = new Point( Math.floor(i / gameState.boardSize) + 1, i % gameState.boardSize + 1); + boardState.push(point); + i++; + } + initHandi(); +} + +function initHandi() { + if (gameState.handicap < 2) return; + HANDI_PLACE[gameState.boardSize][gameState.handicap].forEach(pt => { + if (!pt) return; + let handi = findPointFromIdx(pt); + handi.stone = 1; + handi.joinGroup(); + }); +} + +/*----- meta functions -----*/ + +// include save function + // unpack existing gamerecords + // globalGameRecord = JSON.parse(localStorage.getItem('browser-go-saved-games')); + // append current game record to globalGameRecord - globalGameRecord.gameName = gameState + // stringify all stored gamerecords JSON.stringify(globalGameRecord) + // localStorage.clear() + // localStorage.setItem('browser-go-saved-games') + +// load function + // unpack existing gamerecords - globalGameRecord = JSON.parse(localStorage.getItem('browser-)) + // display each game record name - Object.keys(globalGameRecord).forEach( ... ) + // upon user selection initgame from gameState meta data + +// undo last move + // set up gameState var to track number of 'misclicks' + // after every move JSON.stringify(localGameRecord) + // localStorage.clear() + // localStorage.setItem() + // on undo click - load JSON.parse(localStorage.getItem()) + // reset gameState + +// plus general purpose + +function findPointFromIdx(arr) { + return pointFromIdx = boardState.find( point => point.pos[0] === arr[0] && point.pos[1] === arr[1] ); +} + +function changeUpdateKomi(evt) { + evt.stopPropagation(); + komiDisplayEl.textContent = komiSliderEl.value; + gameState.komi = komiSliderEl.value; + renderMenu(); +} + +function changeUpdateHandicap(evt) { + evt.stopPropagation(); + handiDisplayEl.textContent = handiSliderEl.value !== 1 ? handiSliderEl.value : 0; + gameState.handicap = handiSliderEl.value !== 1 ? handiSliderEl.value : 0; + renderMenu(); +} + +function clickUpdatePlayerMeta(evt) { + evt.stopPropagation(); + if (evt.target.id) { + switch (evt.target.id) { + case 'black-rank-up': + if (gameState.playerMeta.b.rank < RANKS.length - 1) gameState.playerMeta.b.rank++; + break; + case 'black-rank-down': + if (gameState.playerMeta.b.rank > 0) gameState.playerMeta.b.rank--; + break; + case 'white-rank-up': + if (gameState.playerMeta.w.rank < RANKS.length - 1) gameState.playerMeta.w.rank++; + break; + case 'white-rank-down': + if (gameState.playerMeta.w.rank > 0) gameState.playerMeta.w.rank--; + break; + } + } + if (evt.target.name == 'black-rank-certain') gameState.playerMeta.b.rankCertain = !gameState.playerMeta.b.rankCertain; + if (evt.target.name == 'white-rank-certain') gameState.playerMeta.w.rankCertain = !gameState.playerMeta.w.rankCertain; + renderMenu(); + +} + +function clickBoardSize(evt) { + evt.stopPropagation(); + gameState.boardSize = boardSizeRadioEls.find(el => el.checked === true).value; + renderMenu(); +} + +function clickKomiSuggestion(evt) { + evt.preventDefault(); + evt.stopPropagation(); + if (gameState.gameMeta.start) { + gameState.playerMeta.b.name = blackNameInputEl.value || 'black'; + gameState.playerMeta.w.name = whiteNameInputEl.value || 'white'; + modalEl.style.visibility = 'hidden'; + return; + } + let sugg = KOMI_REC[gameState.boardSize][Math.abs(gameState.playerMeta.w.rank - gameState.playerMeta.b.rank)]; + let handi = HANDI_REC[gameState.boardSize][Math.abs(gameState.playerMeta.w.rank - gameState.playerMeta.b.rank)]; + gameState.komi = sugg; + gameState.handicap = handi; + renderMenu(); +} + +function clickCloseMenu(evt) { + evt.stopPropagation(); + if (evt.target.className === "modal" && gameState.gameMeta.start) modalEl.style.visibility = 'hidden'; +} + +/*----- gameplay functions -----*/ + +function clickBoard(evt) { + evt.stopPropagation(); + if (gameState.pass > 1 || gameState.winner) return editTerritory(evt); + // checks for placement and pushes to cell + let placement = [ parseInt(evt.target.closest('td').id.split('-')[0]), parseInt(evt.target.closest('td').id.split('-')[1]) ]; + let point = findPointFromIdx(placement); + //checks that this placement was marked as legal + if ( !checkLegal(point) ) return null; + clearKo(); + clearPass(); + resolveCaptures(point); + point.stone = gameState.turn; + point.joinGroup(); + playSound(point); + clearCaptures(); + gameState.gameRecord.push(`${STONES_DATA[gameState.turn]}: ${point.pos}`); + gameState.turn*= -1; + renderGame(); +} + +function clearKo() { + for (let point in boardState) { + point = boardState[point]; + point.stone = point.stone === 'k' ? 0 : point.stone; + } +} + +function clearPass() { + gameState.pass = 0; +} + +function resolveCaptures(point) { + if(!point.capturing.length) { + point.checkCapture(); + } + if(point.capturing.length) { + point.capturing.forEach(cap => { + gameState.playerState[gameState.turn > 0 ? 'bCaptures' : 'wCaptures']++; + cap.stone = checkKo(point) ? 'k' : 0; + cap.groupMembers = []; + }); + } +} + +function checkLegal(point) { + clearOverlay(); + // first step in logic: is point occupied, or in ko + if (point.stone) return false; + // if point is not empty check if liberties + if (point.getLiberties().length < 1) { + //if no liberties check if enemy group has liberties + if ( point.checkCapture().length ) return true; + //if neighboring point is not empty check if friendly group is alive + if (point.checkGroup()) return true; + return false; + } + return true; +} + +function clearOverlay() { + for (let point in boardState) { + point = boardState[point]; + point.legal = false; + } +} + +// +function checkKo(point) { // currently prevents snapback // capturing point has no liberties and is only capturing one stone and + if (!point.getLiberties().length && point.capturing.length === 1 && !point.checkNeighbors().some(stone => stone.stone === gameState.turn)) return true; + return false; +} + +function playSound(point) { //plays louder sounds for tenuki and for captures + if (point.capturing.length || (gameState.boardSize === 19 && gameState.gameRecord.length > 90 && point.groupMembers.length === 1) + || (gameState.boardSize === 13 && gameState.gameRecord.length > 40 && point.groupMembers.length === 1)) { + soundPlayerEl.src = PLACEMENT_SOUNDS.loud[Math.floor(Math.random() * 5)]; + soundPlayerEl.play(); + } else { + soundPlayerEl.src = PLACEMENT_SOUNDS.soft[Math.floor(Math.random() * 8)]; + soundPlayerEl.play(); + } +} + +function clearCaptures() { + for (let point in boardState) { + point = boardState[point]; + point.capturing = []; + } +} + +function clickPass(evt) { + if (evt.target.parentElement.id === `${STONES_DATA[gameState.turn]}-bowl`) playerPass(); +} + +function playerPass() { + // display confirmation message + clearKo(); + clearCaptures(); + gameState.gameRecord.push(`${STONES_DATA[gameState.turn]}: pass`); + gameState.pass++; + if (gameState.pass === 2) return endGame(); + gameState.turn*= -1; + return renderGame(); +} + +function clickMenuOpen() { + modalEl.style.visibility = 'visible'; + renderMenu(); +} + +function hoverPreview(evt) { + evt.stopPropagation(); + if (gameState.pass > 1 || gameState.winner) return; + // renders preview stone if move is legal + let hover = evt.target.closest('td').id.split('-'); + hover = [parseInt(hover[0]), parseInt(hover[1])]; + let point = findPointFromIdx(hover); + if (checkLegal(point)) { + point.legal = true; // legal + renderPreview(point); + } +} + +/*----- render functions ----------------------*/ +/*----- meta render -----*/ + +function renderMenu() { + dateEl.textContent = gameState.gameMeta.date; + if (gameState.gameMeta.start) { + gameStartEl.value = "New Game"; + komiSuggestEl.value = "Close Menu"; + } + renderKomiSlider(); + renderHandiSlider(); + renderBoardSizeRadio(); + blackRankEl.textContent = RANKS[gameState.playerMeta.b.rank]; + whiteRankEl.textContent = RANKS[gameState.playerMeta.w.rank]; +} + +function renderKomiSlider() { + komiSliderEl.value = gameState.komi; + komiDisplayEl.textContent = gameState.komi; + if (gameState.gameMeta.start) komiSliderEl.setAttribute('disabled', true); +} + +function renderHandiSlider() { + handiSliderEl.value = gameState.handicap; + handiDisplayEl.textContent = gameState.handicap; + if (gameState.gameMeta.start) handiSliderEl.setAttribute('disabled', true); +} + +function renderBoardSizeRadio() { + boardSizeEl.value = gameState.boardSize; + if (gameState.gameMeta.start) boardSizeRadioEls.forEach(el => el.setAttribute('disabled', true)); +} + +/*----- game render -----*/ + +function renderBoardInit() { + renderClearBoard(); + renderBoardTableRows(); + renderHoshi(); + renderBoardTableStyle(); +} + +function renderClearBoard() { + boardEl.innerHTML = ''; + boardEl.classList = ''; +} + +function renderBoardTableRows() { + let i = 1; + while (i <= gameState.boardSize) { + let tableRow = document.createElement('tr'); + tableRow.id = `row-${i}`; + tableRow.innerHTML = renderBoardTableCells(i); + boardEl.appendChild(tableRow); + i++; + } + boardEl.classList = `board-${gameState.boardSize}x${gameState.boardSize}`; +} + +// iterator ^ becomes x index ̌ +function renderBoardTableCells(x) { + let y = 1; + let cells = ''; + while (y <= gameState.boardSize) { + let newCell = ` + +
+
+
+ + `; + cells = cells + newCell; + y++; + } + return cells; +} + +function renderHoshi() { // gets hoshi placement from handiplace const and adds a class to dot elem + let hoshi = HANDI_PLACE[gameState.boardSize].slice(-1); + hoshi = hoshi[0]; + hoshi.forEach(star => { + let boardPt = document.getElementById(`${star[0]}-${star[1]}`).getElementsByClassName('stone')[0]; + boardPt.className += ' hoshi'; + }); +} + +function renderBoardTableStyle() { + document.querySelectorAll('#board-space td[id^="1-"]').forEach(pt => pt.className += 'top '); + document.querySelectorAll(`#board-space td[id^="${gameState.boardSize}-"]`).forEach(pt => pt.className += 'btm '); + document.querySelectorAll('#board-space td[id$="-1"]').forEach(pt => pt.className += 'lft '); + document.querySelectorAll(`#board-space td[id$="-${gameState.boardSize}"]`).forEach(pt => pt.className += 'rgt '); +} + +function renderGame() { + if (gameState.winner || gameState.pass > 1) { + renderTerritory(); + renderMessage(); + } + blackNameDisplayEl.textContent = + `${gameState.playerMeta.b.name}, + ${RANKS[gameState.playerMeta.b.rank]}`; + whiteNameDisplayEl.textContent = + `${gameState.playerMeta.w.name}, + ${RANKS[gameState.playerMeta.w.rank]}`; + gameState.gameRecord.length ? renderTurn() : renderFirstTurn(); + renderBoardState(); + renderCaps(); +} + +function renderFirstTurn() { + document.getElementById(`${STONES_DATA[gameState.turn]}-bowl`).toggleAttribute('data-turn'); +} + +function renderTurn() { + if (gameState.winner || gameState.pass > 1) document.querySelectorAll(`.bowl`).forEach(bowl => { + bowl.removeAttribute('data-turn'); + bowl.toggleAttribute('data-turn'); + }); + document.querySelectorAll(`.bowl`).forEach(bowl => bowl.toggleAttribute('data-turn')); +} + +function renderBoardState() { + boardState.forEach(val => { + let stoneElem = document.getElementById(`${val.pos[0]}-${val.pos[1]}`).getElementsByClassName('stone')[0]; + stoneElem.setAttribute("data-stone", STONES_DATA[val.stone]); + }); +} + +function renderCaps() { + blackCapsEl.textContent = gameState.playerState.bCaptures; + whiteCapsEl.textContent = gameState.playerState.wCaptures; +} + +function renderPreview(hoverPoint) { + boardState.forEach(val => { + let dot = document.getElementById(`${val.pos[0]}-${val.pos[1]}`).getElementsByClassName('dot')[0]; + dot.setAttribute("data-dot", val.legal === true && val.pos[0] === hoverPoint.pos[0] && val.pos[1] === hoverPoint.pos[1] + ? DOTS_DATA[gameState.turn] : DOTS_DATA[0]); + }); +} + +function renderMessage() { + if (gameState.winner && gameState.pass < 2) { + gameHudEl.style.visibility = 'visible'; + gameHudEl.style.cursor = 'default'; + gameHudEl.textContent = `${gameState.playerMeta[gameState.winner === 1 ? 'b' : 'w'].name} won by resignation`; + } + else if (gameState.winner && gameState.pass > 1) { + gameHudEl.style.visibility = 'visible'; + gameHudEl.style.cursor = 'default'; + gameHudEl.textContent = `${gameState.playerMeta[gameState.winner === 1 ? 'b' : 'w'].name || STONES_DATA[gameState.winner]} won by ${Math.abs(gameState.playerState.wScore - gameState.playerState.bScore)}`; + } else if (gameState.pass > 1) { + gameHudEl.style.visibility = 'visible'; + gameHudEl.textContent = 'finalize game'; + } else { + gameHudEl.style.visibility = 'hidden'; + } +} + +function renderTerritory() { + boardState.forEach(val => { + let stoneElem = document.getElementById(`${val.pos[0]}-${val.pos[1]}`).getElementsByClassName('dot')[0]; + stoneElem.setAttribute("data-dot", DOTS_DATA[val.territory]); + }); +} + +/*----- endgame functions -----*/ + +function clickResign(evt) { + if (evt.target.parentElement.id === `${STONES_DATA[gameState.turn]}-caps-space`) playerResign(); +} + +function playerResign() { + // display confirmation message + gameState.pass = -1; + gameHudEl.style.visibility = "visible"; + gameHudEl.textContent = "Do you want to resign?"; +} + +function clickGameHud() { + if (gameState.pass > 1 && !gameState.winner) calculateWinner(); + if (gameState.pass < 0) confirmResign(); +} + +function confirmResign() { + gameState.gameRecord.push(`${STONES_DATA[gameState.turn]}: resign`); + gameState.winner = STONES_DATA[gameState.turn * -1]; + endGame(); +} + +function endGame() { + if (!gameState.winner) endGameSetTerritory(); + renderGame(); +} + +function editTerritory(evt) { + let placement = [ parseInt(evt.target.closest('td').id.split('-')[0]), parseInt(evt.target.closest('td').id.split('-')[1]) ]; + let point = findPointFromIdx(placement); + point.cycleTerritory(); + renderGame(); +} + +function calculateWinner() { + let whiteTerritory = boardState.reduce((acc, pt) => { + if (pt.territory === -1 && pt.stone !== -1) { + return acc = acc + (pt.stone === 0 ? 1 : 2); + } + return acc; + }, 0); + let blackTerritory = boardState.reduce((acc, pt) => { + if (pt.territory === 1 && pt.stone !== 1) { + return acc + (pt.stone === 0 ? 1 : 2); + } + return acc; + }, 0); + gameState.playerState.wScore = + gameState.playerState.wCaptures + + (gameState.komi < 0 ? gameState.komi * -1 : 0) + + whiteTerritory; + gameState.playerState.bScore = + gameState.playerState.bCaptures + + (gameState.komi > 0 ? gameState.komi : 0) + + blackTerritory; + gameState.winner = gameState.playerState.wScore > gameState.playerState.bScore ? -1 : 1; + gameState.gameRecord.push(`${STONES_DATA[gameState.winner]}: +${Math.abs(gameState.playerState.wScore - gameState.playerState.bScore)}`); + renderGame(); +} + +function endGameSetTerritory() { + let emptyPoints = boardState.filter(pt => !pt.stone); + emptyPoints.forEach(pt => pt.joinGroup()); + emptyPointSetTerritory(emptyPoints); + groupsMarkDeadLive(); + // reviseTerritory(); +} + +function groupsMarkDeadLive() { + boardState.filter(pt => (!pt.territory )) + .forEach(pt => { + if (pt.groupMembers.some(grpMem => { + return grpMem.checkNeighbors().some(nbr => nbr.territory === pt.stone && nbr.stone === 0); + })) { + pt.groupMembers.forEach(grpMem => grpMem.territory = pt.stone); + } + }); + boardState.filter(pt => (!pt.territory)).forEach(pt => { + pt.territory = pt.stone * -1; + }); +} + +function emptyPointSetTerritory(emptyPoints) { + emptyPoints.filter(pt => !pt.territory && pt.checkNeighbors().filter(nbr => nbr.stone !== 0)) + .forEach(pt => { + let b = pt.groupMembers.reduce((acc, grpMem) => { + let bNbr = grpMem.checkNeighbors().filter(nbr => nbr.stone === 1).length; + return acc + bNbr; + }, 0); + let w = pt.groupMembers.reduce((acc, grpMem) => { + let wNbr = grpMem.checkNeighbors().filter(nbr => nbr.stone === -1).length; + return acc + wNbr; + }, 0); + pt.groupMembers.forEach(grp => { + if (Math.abs(b - w) < 4 && b && w) grp.territory = 'd'; + else grp.territory = b > w ? 1 : -1; + }); + }); +} + +function reviseTerritory() { + // count eyes + // for each group marked live get liberties + // +} diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..f1dff3b --- /dev/null +++ b/src/style.css @@ -0,0 +1,510 @@ +* { + box-sizing: border-box; + margin: 0; + vertical-align: middle; + font-family: arial, sans-serif; +} + +html { + font-size: 12px; + background: radial-gradient(farthest-corner at 55% 40%, rgb(150, 200, 220) 0%, rgb(97, 166, 194) 65%, rgb(70,100,120) 90%, rgb(40, 80, 90) 100%); +} + +body { + height: vh; + width: vw; + display: flex; + justify-content: center; +} + +.modal { + display: flex; + position: fixed; + z-index: 2; + /* display: none; */ + width: 100vw; + height: 100vh; + background-color: rgba(0,0,0,0.3); + align-items: flex-start; + justify-content: center; + visibility: hidden; + overflow-y: scroll; + +} + +#menu { + position: relative; + background-color: rgb(250, 2250, 255, 0.9); + padding: 1vmin; + display: grid; + grid-template-columns: 60vw; + grid-template-rows: auto auto 60vw auto; + grid-template-areas: + "meta" + "player" + "record" + "submit"; + font-family: 'La Belle Aurore', cursive; + min-height: 0; + max-height: 100vh; + z-index: 3; +} + +#menu .menu-subblock { + display: flex; + justify-content: space-between; + margin: .25em; +} + +#game-meta { + grid-area: meta; + display: flex; + flex-direction: column; +} + +.menu-heading, content, #instructions, div[data-player-meta] label { + font-family: 'Raleway', sans-serif; +} + +h1 { + font-size: 140%; + font-weight: 600; +} + +h4 { + font-weight: 600; + margin: .25em; + font-size: 110%; +} + +#player-meta { + grid-area: player; + display: flex; + justify-items: stretch; + flex-direction: column; +} + +#player-meta span[id$="rank"] { + margin: 0 2em; +} + +#player-meta input[type="button"] { + margin: .25em; +} + +#player-meta * .menu-line { + display: flex; + flex-flow: row nowrap; + align-items: baseline; + justify-items: flex-start; +} + +div[data-player-meta] { + width: 100%; + justify-self: stretch; +} + +div[data-player-meta] input[type="text"] { + width: 90%; + justify-self: stretch; +} + +div[data-player-meta] input { + margin: 1vmin; +} + +#confirm { + visibility: hidden; +} + +div[data-player-meta] label { + margin: .25em; + font-size: 100%; +} + +#game-record-space { + grid-area: record; +} + +#instructions { + padding: .5em; + line-height: 1.5; + overflow: scroll; + height: 100%; + width: 100% +} + +#instructions, #game-record{ +border: 2px solid black; +height: 1; +} + +#game-record { + visibility: hidden; +} + +#game-update-space { + grid-area: submit; + margin: .5em; + display: flex; + justify-content: space-between; +} + +content { + display: flex !important; + flex-direction: column; + justify-content: space-between !important; + height: 100vh; + width: 100vw; +} + +.player-pos { + display: flex; + align-items: flex-end; + justify-content: space-around; + flex: 5; + height: 9vmin; +} + +#game-hud p { + font-size: 130%; + width: 100%; + order: 0; + width: 10vh; + background-color: rgba(0,0,0,0.3); + padding: 1vh; + color: #fff; + cursor: pointer; + visibility: hidden; +} + +.player-pos#black-pos { + align-items: flex-start; + flex-direction: row-reverse; + justify-self: flex-end; +} + +#kifu { + order: 0; + height: 10vh; + width: 8vh; + background-color: #FFF; + transform: rotate(-20deg); +} + +.bowl { + order: -1; + margin: 4vh; + height: 15vh; + width: 15vh; + /* border: solid black; */ + border-radius: 50%; + background-color: rgb(116, 48, 17); + background: radial-gradient(farthest-corner at 48% 54%, rgba(30, 5, 0, 0.25) 0%, rgba(30, 5, 0, 0.45) 2%, rgba(30, 5, 0, 0.75) 32%, rgb(0,0,0,0.85)35%, rgb(116,48,17) 48%, rgb(140, 60, 40) 52%, rgb(100, 40, 5) 55%, rgb(116, 48, 17) 58%, rgb(140,60,40) 65%, rgb(100, 40, 5) 80%, rgb(80, 20, 0) 90%); + box-shadow: -1vmin 2vmin 1.5vmin rgba(83, 53, 35, 0.61); + display: flex; + align-items: center; + justify-content: center; +} + +.bowl p { + display: none; +} + +.stone-image { + height: 100%; + width: 100%; + border-radius: 50%; + background-size: cover; + z-index: -1; +} + +#white-stone-image { + background-image: url(../images/white-stones-bowl.jpg); +} + +#black-stone-image { + background-image: url(../images/black-stones-bowl.jpg); +} + +.bowl[data-turn]:hover p { + display: block; + color: #FFF; + background-color: rgba(0,0,0,0.3); + padding: .5em; + cursor: grab; +} + +.bowl[data-turn] { + box-shadow: 0 0 3vh 3vh rgb(255, 175, 2); +} + +.caps-space { + color: #FFF; + margin: 1vh; + height: 10vh; + width: 10vh; + border-radius: 50%; + background: radial-gradient(farthest-side at 49% 52%, rgb(150, 75, 50) 0%, rgb(116,48,17) 35%, rgb(116,48,17) 64%, rgb(80, 20, 0) 65%, rgb(175, 140, 95) 70%, rgb(120, 50, 40) 80%, rgb(80, 20, 0) 95%, rgb(175, 140, 95) 100%); + box-shadow: -0.5vmin 1vmin 1vmin rgba(83, 53, 35, 0.61); + display: flex; + align-items: center; + justify-content: center; +} + +.caps-space :first-child { + display: none; +} + +.bowl[data-turn] + .name-space .caps-space:hover :first-child { + display: block; + position: absolute; + background-color: rgba(0,0,0,0.7); + padding: .5em; + cursor: grab; +} + +.name-space { + order: 1; + display: flex; + flex-direction: column; + align-items: center; +} + +.name-space h4 { + font-size: 120%; + color: rgb(255,240,230); + background-color: rgba(0,0,0,0.7); + padding: 0.25em; + z-index: 1; +} + +#board-container { + width: 100%; + display: grid; + grid-template-areas: 100%; + grid-template-columns: 100%; + grid-template-areas: + "board"; +} + +#board-space tbody { + background: radial-gradient(farthest-corner at 55% 40%, rgba(244, 230, 120, 0.75) 0%, rgba(234, 178, 78, 0.5) 65%, rgba(200, 160, 90, 0.45) 90%, rgba(200, 140, 90, 0.45) 100%); + background-size: cover; + padding: 1vmin; +} + +#board-space { + grid-area: board; + margin: 0 auto; + display: flex; + flex-direction: column; + background-image: url(../images/board.png); + z-index: 1; + box-shadow: -2vmin 4vmin 3vmin rgba(145, 92, 23, 0.5); + flex: 1; +} + + +#board-space table { + display: flex; + align-items: stretch; + justify-content: space-between; + margin: auto; +} + +#board-space td { + background: conic-gradient(#000 0%, rgba(0,0,0,0) 1%, rgba(0,0,0,0) 24%, #000 25%, rgba(0,0,0,0) 26%, rgba(0,0,0,0) 49%, #000 50%, rgba(0,0,0,0) 51%, rgba(0,0,0,0) 74%, #000 75%, rgba(0,0,0,0) 76%, rgba(0,0,0,0) 99%, #000 100%); + border-radius: 50% solid black; + color: black; + margin: auto; + padding: 0; + vertical-align: middle; +} + +#board-space .board-9x9 td { + height: 9vmin; + width: 9vmin; +} + +#board-space .board-13x13 td { + height: 7vmin; + width: 7vmin; +} + +#board-space .board-19x19 td { + height: 5vmin; + width: 5vmin; +} + +#board-space td.top { + background: conic-gradient( rgba(0,0,0,0) 24%, #000 25%, rgba(0,0,0,0) 26%, rgba(0,0,0,0) 49%, #000 50%, rgba(0,0,0,0) 51%, rgba(0,0,0,0) 74%, #000 75%, rgba(0,0,0,0) 76%); +} + +#board-space td.btm { + background: conic-gradient(#000 0%, rgba(0,0,0,0) 1%, rgba(0,0,0,0) 24%, #000 25%, rgba(0,0,0,0) 26%, rgba(0,0,0,0) 74%, #000 75%, rgba(0,0,0,0) 76%, rgba(0,0,0,0) 99%, #000 100%); +} + +#board-space td.lft { + background: conic-gradient(#000 0%, rgba(0,0,0,0) 1%, rgba(0,0,0,0) 24%, #000 25%, rgba(0,0,0,0) 26%, rgba(0,0,0,0) 49%, #000 50%, rgba(0,0,0,0) 51%, rgba(0,0,0,0) 99%, #000 100%); +} + +#board-space td.rgt { + background: conic-gradient(#000 0%, rgba(0,0,0,0) 1%, rgba(0,0,0,0) 49%, #000 50%, rgba(0,0,0,0) 51%, rgba(0,0,0,0) 74%, #000 75%, rgba(0,0,0,0) 76%, rgba(0,0,0,0) 99%, #000 100%); +} + +#board-space td.top.lft { + background: conic-gradient( rgba(0,0,0,0) 24%, #000 25%, rgba(0,0,0,0) 26%, rgba(0,0,0,0) 49%, #000 50%, rgba(0,0,0,0) 51%); +} + +#board-space td.top.rgt { + background: conic-gradient( rgba(0,0,0,0) 49%, #000 50%, rgba(0,0,0,0) 51%, rgba(0,0,0,0) 74%, #000 75%, rgba(0,0,0,0) 76% ); +} + +#board-space td.btm.lft { + background: conic-gradient(#000 0%, rgba(0,0,0,0) 1%, rgba(0,0,0,0) 24%, #000 25%, rgba(0,0,0,0) 26%, rgba(0,0,0,0) 99%, #000 100%); +} + +#board-space td.btm.rgt { + background: conic-gradient(#000 0%, rgba(0,0,0,0) 1%, rgba(0,0,0,0) 74%, #000 75%, rgba(0,0,0,0) 76%, rgba(0,0,0,0) 99%, #000 100%); +} + +.stone.hoshi { + background: radial-gradient(circle farthest-corner at center, #000 0%, #000 14%, rgba(0,0,0,0) 15%); + z-index: 3; +} + +td .stone { + width: 85%; + height: 85%; + border-radius: 50%; + margin: auto; + vertical-align: middle; + display: flex; + flex-direction: column; + justify-content: center; +} +td .stone .dot { + width: 35%; + height: 35%; + border-radius: 50%; + margin: auto; + vertical-align: middle; +} + +td .stone[data-stone="ko"] { + background-color: transparent; + border: 1vmin solid rgba(200,20,50,0.8); + border-radius: 0%; +} +td .stone[data-stone="white"] { + background: radial-gradient(farthest-side at 55% 40%, white 0%, rgb(200,200,200) 65%, rgb(100,100,100) 90%, rgb(68, 50, 0) 100%); + box-shadow: -.25vmin .5vmin .5vmin rgba(145, 92, 23, 0.5); +} +td .stone[data-stone="black"] { + background-color: black; + background: radial-gradient(farthest-side at 55% 40%, rgb(220,220,220) 0%, rgb(60,60,60) 45%, rgb(15,15,15) 90%, rgb(5, 5, 0) 100%); + box-shadow: -.25vmin .5vmin .5vmin rgba(145, 92, 23, 0.75); +} + +td .stone[data-stone="none"] { + background-color: transparent; +} +td .dot[data-dot="white"] { + background-color: white; +} +td .dot[data-dot="black"] { + background-color: black; +} +td .dot[data-dot="none"] { + background-color: transparent; +} +td .dot[data-dot="dame"] { + background-color: purple; +} + +@media only screen and (min-width: 591px) { + + #player-meta { + flex-direction: row; + } + + div[data-player-meta] { + width: 50%; + } + +} + +/* Responsive Design */ + +@media only screen and (min-width: 500px) { + + html { + font-size: 14px; + } + + .player-pos { + height: 14vh; + } + +} + +@media only screen and (min-width: 590px) { + + #board-space .board-19x19 td { + height: 3.5vh; + width: 3.5vh; + } + +} + +@media only screen and (min-width: 570px) { + + #board-space .board-9x9 td { + height: 7.5vh; + width: 7.5vh; + } + + #board-space .board-13x13 td { + height: 5vh; + width: 5vh; + } + + .bowl { + order: -1; + margin: 3vh; + height: 10vh; + width: 10vh; + } + + .caps-space { + color: #FFF; + margin: 2vh; + height: 7vh; + width: 7vh; + } + + +} + +@media only screen and (min-width: 700px) { + + content { + width: 700px; + } + + #menu { + grid-template-columns: 50vw; + grid-template-rows: auto auto 50vw auto; + } + +} + +@media only screen and (min-width: 900px) { + + #menu { + grid-template-columns: 55vh; + grid-template-rows: auto auto 55vh auto; + } + +}