diff --git a/README.md b/README.md index a81af96..504cc12 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,57 @@ # Browser Go +##### 0.8.1 +A two-player [in-browser Go application](https://sorrelbri.github.io/browser-go/) developed by Sorrel June. - +--- +## About Go +--- +[skip to application details](#how-browser-go-was-built) - +Go is the oldest continuously played game on Earth. The Go ruleset can be understood in an afternoon while offering a depth and complexity that inspires for a lifetime. - - \ No newline at end of file + +--- +## How Browser Go was Built +--- +Browser Go was originally developed for the Software Engineering Immersive at General Assembly in August, 2019. +Technologies used inclue: +* HTML5 +* CSS3 +* JavaScript (ES6) + +Assets acquired from: +* subtlepatterns.com +* freesound.org +* the developer's photos of her go equipment + +--- +## Using Browser Go +--- +[Play Browser Go Here](https://github.com/sorrelbri/browser-go) + +### Starting a Game +![image of game menu at start](images/gameplay-images/browser-go-new-game-screen.png/browser-go-new-game-screen.png){ width=200px } + +Upon initiation of a new game session, Browser Go will display the new game menu. Here, you will be asked to confirm the size of + +### Gameplay Elements + +### Ending a Game + +--- +## The Future of Browser Go +--- +Browser Go's functionality will evolve as it transitions out of it's current client-side architecture. + +Additional features in development include: +* game timer +* smart game format support with + * read/write .sgf files + * tsumego (go problems) support + * support for multiple game lines +* toggleable 'misclick' undo feature +* improved touch screen support +* board animations and expanded sound library diff --git a/audio/go_loud.wav b/audio/go_loud.wav new file mode 100644 index 0000000..a2dfb8f Binary files /dev/null and b/audio/go_loud.wav differ diff --git a/audio/go_loud_2.wav b/audio/go_loud_2.wav new file mode 100644 index 0000000..c234296 Binary files /dev/null and b/audio/go_loud_2.wav differ diff --git a/audio/go_loud_3.wav b/audio/go_loud_3.wav new file mode 100644 index 0000000..c0c3336 Binary files /dev/null and b/audio/go_loud_3.wav differ diff --git a/audio/go_loud_4.wav b/audio/go_loud_4.wav new file mode 100644 index 0000000..ff1b313 Binary files /dev/null and b/audio/go_loud_4.wav differ diff --git a/audio/go_soft.wav b/audio/go_soft.wav new file mode 100644 index 0000000..7df37f9 Binary files /dev/null and b/audio/go_soft.wav differ diff --git a/audio/go_soft_2.wav b/audio/go_soft_2.wav new file mode 100644 index 0000000..07a62fb Binary files /dev/null and b/audio/go_soft_2.wav differ diff --git a/audio/go_soft_3.wav b/audio/go_soft_3.wav new file mode 100644 index 0000000..6ebd408 Binary files /dev/null and b/audio/go_soft_3.wav differ diff --git a/audio/go_soft_4.wav b/audio/go_soft_4.wav new file mode 100644 index 0000000..d802f6c Binary files /dev/null and b/audio/go_soft_4.wav differ diff --git a/audio/go_soft_5.wav b/audio/go_soft_5.wav new file mode 100644 index 0000000..288e7a0 Binary files /dev/null and b/audio/go_soft_5.wav differ diff --git a/audio/go_soft_6.wav b/audio/go_soft_6.wav new file mode 100644 index 0000000..b1de353 Binary files /dev/null and b/audio/go_soft_6.wav differ diff --git a/audio/go_soft_7.wav b/audio/go_soft_7.wav new file mode 100644 index 0000000..c1376f8 Binary files /dev/null and b/audio/go_soft_7.wav differ diff --git a/css/main.css b/css/main.css index 5bf0bb0..f2ed8df 100644 --- a/css/main.css +++ b/css/main.css @@ -48,6 +48,7 @@ body { "submit"; font-family: 'La Belle Aurore', cursive; min-height: 0; + max-height: 100vh; z-index: 3; } @@ -201,7 +202,7 @@ content { /* border: solid black; */ border-radius: 50%; background-color: rgb(116, 48, 17); - background: radial-gradient(closest-corner at 52% 46%, rgba(30, 5, 0, 0.5) 0%, rgb(0,0,0,0.5)35%, rgb(116,48,17) 48%, rgb(140, 60, 40) 52%, rgb(116, 48, 17) 65%, rgb(100,40,5) 70%, rgb(80, 20, 0) 80%); + 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; @@ -216,9 +217,18 @@ content { 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; @@ -495,17 +505,8 @@ td .dot[data-dot="dame"] { @media only screen and (min-width: 900px) { #menu { - grid-template-columns: 40vw; - grid-template-rows: auto auto 40vw auto; - } - -} - -@media only screen and (min-width: 1200px) { - - #menu { - grid-template-columns: 35vw; - grid-template-rows: auto auto 35vw auto; + grid-template-columns: 55vh; + grid-template-rows: auto auto 55vh auto; } } diff --git a/images/black-stones-bowl.jpg b/images/black-stones-bowl.jpg new file mode 100644 index 0000000..7eeea5c Binary files /dev/null and b/images/black-stones-bowl.jpg differ diff --git a/images/gameplay-images/browser-go-new-game-screen.png b/images/gameplay-images/browser-go-new-game-screen.png new file mode 100644 index 0000000..330c76c Binary files /dev/null and b/images/gameplay-images/browser-go-new-game-screen.png differ diff --git a/images/white-stones-bowl.jpg b/images/white-stones-bowl.jpg new file mode 100644 index 0000000..850bca5 Binary files /dev/null and b/images/white-stones-bowl.jpg differ diff --git a/index.html b/index.html index 8c7fbda..719b571 100644 --- a/index.html +++ b/index.html @@ -1,16 +1,16 @@ - - - - - - - - - - Browser Go + + + + + + + + + + Browser Go
@@ -2293,7 +2293,7 @@
-

Pass?

+

Pass?

by Sorrel June

Resign?

diff --git a/js/main.js b/js/main.js index 7ceeda5..d4dc162 100644 --- a/js/main.js +++ b/js/main.js @@ -44,13 +44,31 @@ const HANDI_REC = { ] } +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: 1, // turn logic depends on handicap stones + 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: 9, + boardSize: null, playerState: { bCaptures: null, wCaptures: null, @@ -64,12 +82,12 @@ const gameState = { // pre-init values (render prior to any player input) playerMeta: { // editable during game b: { name: null, - rank: 21, + rank: null, rankCertain: false }, w: { name: null, - rank: 21, + rank: null, rankCertain: false }, }, @@ -77,9 +95,6 @@ const gameState = { // pre-init values (render prior to any player input) gameRecord : [] } - -// deadShapes{} - // 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 = { @@ -112,16 +127,6 @@ const HANDI_PLACE = { [ [ 10, 10 ], [ 16, 10 ], [ 10, 4 ], [ 10, 16 ], [ 4, 10 ], [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ], ] }; - -const BOARD_POINT_SIZE = { - '9' : '9vmin', - '13': '6vmin', - '19' : '4vmin' -} - -/*----- app's state (variables) -----*/ - -let boardState = []; class Point { constructor(x, y) { @@ -191,27 +196,31 @@ class Point { 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; - } - }); - } + 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'); @@ -236,6 +245,7 @@ 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], @@ -260,11 +270,80 @@ gameHudEl.addEventListener('click', clickGameHud); boardSizeEl.addEventListener('click', clickBoardSize); gameStartEl.addEventListener('click', clickSubmitStart); -/*----- functions -----*/ +/*----- 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'; + 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 -----*/ +// plus general purpose + function findPointFromIdx(arr) { - console.log(arr); return pointFromIdx = boardState.find( point => point.pos[0] === arr[0] && point.pos[1] === arr[1] ); } @@ -287,16 +366,16 @@ function clickUpdatePlayerMeta(evt) { if (evt.target.id) { switch (evt.target.id) { case 'black-rank-up': - gameState.playerMeta.b.rank++; + if (gameState.playerMeta.b.rank < RANKS.length - 1) gameState.playerMeta.b.rank++; break; case 'black-rank-down': - gameState.playerMeta.b.rank--; + if (gameState.playerMeta.b.rank > 0) gameState.playerMeta.b.rank--; break; case 'white-rank-up': - gameState.playerMeta.w.rank++; + if (gameState.playerMeta.w.rank < RANKS.length - 1) gameState.playerMeta.w.rank++; break; case 'white-rank-down': - gameState.playerMeta.w.rank--; + if (gameState.playerMeta.w.rank > 0) gameState.playerMeta.w.rank--; break; } } @@ -328,109 +407,54 @@ function clickKomiSuggestion(evt) { renderMenu(); } -function clickGameHud() { - if (gameState.pass > 1 && !gameState.winner) calculateWinner(); - if (gameState.pass < 0) confirmResign(); -} - -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'; - initGame(); -} - -function renderKomi() { - 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)); -} - -function renderMenu() { - dateEl.textContent = gameState.gameMeta.date; - if (gameState.gameMeta.start) { - gameStartEl.value = "New Game"; - komiSuggestEl.value = "Close Menu"; - } - renderKomi() - renderHandiSlider(); - renderBoardSizeRadio(); - blackRankEl.textContent = RANKS[gameState.playerMeta.b.rank]; - whiteRankEl.textContent = RANKS[gameState.playerMeta.w.rank]; -} - -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; - renderGame(); -} - -function clickMenuOpen() { - modalEl.style.visibility = 'visible'; - renderMenu(); -} - -function startMenu() { - modalEl.style.visibility = 'visible'; - renderMenu(); -} - function clickCloseMenu(evt) { evt.stopPropagation(); if (evt.target.className === "modal" && gameState.gameMeta.start) modalEl.style.visibility = 'hidden'; } -function clickResign(evt) { - if (evt.target.parentElement.id === `${STONES_DATA[gameState.turn]}-caps-space`) playerResign(); -} +/*----- gameplay functions -----*/ -function playerResign() { - // display confirmation message - gameState.pass = -1; - gameHudEl.style.visibility = "visible"; - gameHudEl.textContent = "Do you want to resign?"; -} - -function confirmResign() { - gameState.gameRecord.push(`${STONES_DATA[gameState.turn]}: resign`); - gameState.winner = STONES_DATA[gameState.turn * -1]; - endGame(); -} - - -function hoverPreview(evt) { +function clickBoard(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); + 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; + 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.groupMembers = []; + cap.stone = checkKo(point, cap) ? 'k' : 0; + }) } } @@ -456,61 +480,20 @@ function clearOverlay() { } } -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.groupMembers = []; - cap.stone = checkKo(point, cap) ? 'k' : 0; - }) - } -} - -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 checkKo(point, cap) { if (!point.getLiberties().length && cap.checkNeighbors().filter(stone => stone.stone === gameState.turn * -1) && point.capturing.length === 1) return true; } -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]) ]; - console.log(placement); - console.log(evt); - let point = findPointFromIdx(placement); - //checks that this placement was marked as legal - if ( !checkLegal(point) ) return; - clearKo(); - clearPass(); - resolveCaptures(point); - point.stone = gameState.turn; - point.joinGroup(); - 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 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() { @@ -520,71 +503,74 @@ function clearCaptures() { } } -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 clickPass(evt) { + if (evt.target.parentElement.id === `${STONES_DATA[gameState.turn]}-bowl`) playerPass(); } -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(); - }) -} - -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 init() { - gameState.gameMeta.date = getDate(); - gameState.komi = 5.5; - gameState.handicap = 0; - gameState.winner = null; - gameState.pass = null; - gameState.boardSize = 9; - gameState.playerState.bCaptures = 0; - gameState.playerState.wCaptures = 0; - gameState.gameRecord = []; - boardState = []; - gameState.gameMeta.start = false; - startMenu(); -}; - -function initGame() { - gameState.winner = null; - gameState.pass = null; - gameState.turn = gameState.handicap ? -1 : 1; - gameState.gameMeta.start = true; - initBoard(); - renderBoardInit(); +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; renderGame(); } - -function renderGame() { - if (gameState.winner || gameState.pass > 1) { - renderTerritory(); - renderMessage(); - } - blackNameDisplayEl.textContent = - `${gameState.playerMeta.b.name}, - ${gameState.playerMeta.b.rank}`; - whiteNameDisplayEl.textContent = - `${gameState.playerMeta.w.name}, - ${gameState.playerMeta.w.rank}`; - gameState.gameRecord.length? renderTurn() : renderFirstTurn(); - renderBoardState(); - renderCaps(); + +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(); @@ -592,30 +578,11 @@ function renderBoardInit() { renderBoardTableStyle(); } -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 => { - console.log(hoshi); - console.log(`star: ${star[0][0]} - ${star[0][1]} end star`) - let boardPt = document.getElementById(`${star[0]}-${star[1]}`).getElementsByClassName('stone')[0]; - console.log(boardPt); - boardPt.className += ' hoshi' }); -} - function renderClearBoard() { boardEl.innerHTML = ''; boardEl.classList = ''; } -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 renderBoardTableRows() { let i = 1; while (i <= gameState.boardSize) { @@ -628,6 +595,7 @@ function renderBoardTableRows() { boardEl.classList = `board-${gameState.boardSize}x${gameState.boardSize}`; } +// iterator ^ becomes x index ̌ function renderBoardTableCells(x) { let y = 1 let cells = ''; @@ -645,6 +613,69 @@ function renderBoardTableCells(x) { 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'; @@ -670,36 +701,40 @@ function renderTerritory() { }) } -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'); +/*----- endgame functions -----*/ - }); - document.querySelectorAll(`.bowl`).forEach(bowl => bowl.toggleAttribute('data-turn')); +function clickResign(evt) { + if (evt.target.parentElement.id === `${STONES_DATA[gameState.turn]}-caps-space`) playerResign(); } -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 playerResign() { + // display confirmation message + gameState.pass = -1; + gameHudEl.style.visibility = "visible"; + gameHudEl.textContent = "Do you want to resign?"; } -function renderCaps() { - blackCapsEl.textContent = gameState.playerState.bCaptures; - whiteCapsEl.textContent = gameState.playerState.wCaptures; +function clickGameHud() { + if (gameState.pass > 1 && !gameState.winner) calculateWinner(); + if (gameState.pass < 0) confirmResign(); } -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 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() { @@ -766,8 +801,3 @@ function emptyPointSetTerritory(emptyPoints) { }) }); } - -function endGame() { - if (!gameState.winner) endGameSetTerritory() - renderGame(); -} \ No newline at end of file diff --git a/version tracking.md b/version tracking.md index 63f2069..8d2fd74 100644 --- a/version tracking.md +++ b/version tracking.md @@ -1,7 +1,7 @@ # Browser Go -#### Minimum Deliverable Product +#### Version 1 Requirements -a working game of go for a 9x9 board that +a working game of go that - [x] displays well on mobile or desktop - [x] initiates a game with suggested handicap and komi according to rank input - [x] displays how to play in open screen @@ -19,19 +19,21 @@ a working game of go for a 9x9 board that - [x] allows users to submit finalized score to game record - [ ] displays game record as string -stretch goals +additional features - [x] uses stone placement GUI for resign and pass - [ ] maintains a one move game state history for 'undo mismove' - [ ] converts string to .sgf format - [x] allows users to edit game info mid game -- [ ] add stone placement sounds - -superstretch goals +- [x] add stone placement sounds - [x] allows users to select board size (9x9, 13x13, 19x19) -- [ ] allows users to load .sgf main lines -- [ ] allow for responsivity in the form of -- - [x] 9x9 games simply stretch with screen size -- - [ ] larger games allow small displays one click to zoom before running legal move calculations and move placement +- [x] 9x9 games simply stretch with screen size +- [ ] timed game functionality +- [ ] larger games allow small displays one click to zoom before running legal move calculations and move placement + +later version features +- [ ] allows users to read/write .sgf files +- [ ] allow users to edit multiple game lines +- [ ] allow users to play and generate tsumego