add audio for stone placement, fix menu overflow bug, fix turn marker bug

This commit is contained in:
Sorrel Bri 2019-08-08 22:39:12 -07:00
parent 45dfa9a36c
commit 165e2d17ea
19 changed files with 410 additions and 331 deletions

View file

@ -1,11 +1,57 @@
# Browser Go # Browser Go
##### 0.8.1
A two-player [in-browser Go application](https://sorrelbri.github.io/browser-go/) developed by Sorrel June.
<!-- Screenshot(s): Images of your actual game. --> <!-- Screenshot(s): Images of your actual game. -->
<!-- background info on Go --> ---
## About Go
---
[skip to application details](#how-browser-go-was-built)
<!-- ☐ Technologies Used: List of the technologies used, e.g., JavaScript, HTML, CSS... --> 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.
<!-- ☐ Getting Started: In this section include the link to your deployed game and any instructions you deem important. -->
<!-- ☐ Next Steps: Planned future enhancements (icebox items). -->
---
## 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

BIN
audio/go_loud.wav Normal file

Binary file not shown.

BIN
audio/go_loud_2.wav Normal file

Binary file not shown.

BIN
audio/go_loud_3.wav Normal file

Binary file not shown.

BIN
audio/go_loud_4.wav Normal file

Binary file not shown.

BIN
audio/go_soft.wav Normal file

Binary file not shown.

BIN
audio/go_soft_2.wav Normal file

Binary file not shown.

BIN
audio/go_soft_3.wav Normal file

Binary file not shown.

BIN
audio/go_soft_4.wav Normal file

Binary file not shown.

BIN
audio/go_soft_5.wav Normal file

Binary file not shown.

BIN
audio/go_soft_6.wav Normal file

Binary file not shown.

BIN
audio/go_soft_7.wav Normal file

Binary file not shown.

View file

@ -48,6 +48,7 @@ body {
"submit"; "submit";
font-family: 'La Belle Aurore', cursive; font-family: 'La Belle Aurore', cursive;
min-height: 0; min-height: 0;
max-height: 100vh;
z-index: 3; z-index: 3;
} }
@ -201,7 +202,7 @@ content {
/* border: solid black; */ /* border: solid black; */
border-radius: 50%; border-radius: 50%;
background-color: rgb(116, 48, 17); 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); box-shadow: -1vmin 2vmin 1.5vmin rgba(83, 53, 35, 0.61);
display: flex; display: flex;
align-items: center; align-items: center;
@ -216,9 +217,18 @@ content {
height: 100%; height: 100%;
width: 100%; width: 100%;
border-radius: 50%; border-radius: 50%;
background-size: cover;
z-index: -1; 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 { .bowl[data-turn]:hover p {
display: block; display: block;
color: #FFF; color: #FFF;
@ -495,17 +505,8 @@ td .dot[data-dot="dame"] {
@media only screen and (min-width: 900px) { @media only screen and (min-width: 900px) {
#menu { #menu {
grid-template-columns: 40vw; grid-template-columns: 55vh;
grid-template-rows: auto auto 40vw auto; grid-template-rows: auto auto 55vh auto;
}
}
@media only screen and (min-width: 1200px) {
#menu {
grid-template-columns: 35vw;
grid-template-rows: auto auto 35vw auto;
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View file

@ -1,16 +1,16 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta http-equiv="X-UA-Compatible" content="ie=edge">
<!-- <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" type="stylesheet"> --> <!-- <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" type="stylesheet"> -->
<link rel="stylesheet" href="css/reset.css" type="text/css"> <link rel="stylesheet" href="css/reset.css" type="text/css">
<link rel="stylesheet" href="css/main.css" type="text/css" /> <link rel="stylesheet" href="css/main.css" type="text/css" />
<!-- <script defer src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script> --> <!-- <script defer src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script> -->
<script defer src="js/main.js"></script> <script defer src="js/main.js"></script>
<title>Browser Go</title> <title>Browser Go</title>
</head> </head>
<body> <body>
<div class="modal"> <div class="modal">
@ -25,9 +25,9 @@
<input type="range" min="0" max="9" step="1" value="0" name="handicap-slider"> <input type="range" min="0" max="9" step="1" value="0" name="handicap-slider">
<div id="board-size-radio" class="menu-subblock"> <div id="board-size-radio" class="menu-subblock">
<p class="menu-heading">Board Size</p> <p class="menu-heading">Board Size</p>
<input type="radio" name="board-size" value="9" checked>9 x 9<br> <input type="radio" name="board-size" value="9">9 x 9<br>
<input type="radio" name="board-size" value="13">13 x 13<br> <input type="radio" name="board-size" value="13">13 x 13<br>
<input type="radio" name="board-size" value="19">19 x 19 <input type="radio" name="board-size" value="19" checked>19 x 19
</div> </div>
</div> </div>
<div id="player-meta"> <div id="player-meta">
@ -2293,7 +2293,7 @@
</div> </div>
</div> </div>
<div id="black-pos" class="player-pos"> <div id="black-pos" class="player-pos">
<div id="black-bowl" class="bowl"><p>Pass?</p><div id="white-stone-image" class="stone-image"></div></div> <div id="black-bowl" class="bowl"><p>Pass?</p><div id="black-stone-image" class="stone-image"></div></div>
<div id="black-player-space" class="name-space"> <div id="black-player-space" class="name-space">
<h4 id="black-player-name">by Sorrel June</h4> <h4 id="black-player-name">by Sorrel June</h4>
<div id="black-caps-space" class="caps-space"><p>Resign?</p><p id="black-caps"></p></div> <div id="black-caps-space" class="caps-space"><p>Resign?</p><p id="black-caps"></p></div>

View file

@ -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) const gameState = { // pre-init values (render prior to any player input)
winner: null, 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 pass: null, // -1 represents state in which resignation has been submitted, not confirmed
komi: null, // komi depends on handicap stones + player rank komi: null, // komi depends on handicap stones + player rank
handicap: null, handicap: null,
boardSize: 9, boardSize: null,
playerState: { playerState: {
bCaptures: null, bCaptures: null,
wCaptures: null, wCaptures: null,
@ -64,12 +82,12 @@ const gameState = { // pre-init values (render prior to any player input)
playerMeta: { // editable during game playerMeta: { // editable during game
b: { b: {
name: null, name: null,
rank: 21, rank: null,
rankCertain: false rankCertain: false
}, },
w: { w: {
name: null, name: null,
rank: 21, rank: null,
rankCertain: false rankCertain: false
}, },
}, },
@ -77,9 +95,6 @@ const gameState = { // pre-init values (render prior to any player input)
gameRecord : [] gameRecord : []
} }
// deadShapes{}
// index represents handicap placement for different board-sizes, eg handiPlace['9][1] = { (3, 3), (7, 7) } // 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 // last array in each property also used for hoshi rendering
const HANDI_PLACE = { 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] ], [ [ 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 { class Point {
constructor(x, y) { constructor(x, y) {
@ -191,27 +196,31 @@ class Point {
continue; continue;
} }
} }
cycleTerritory = () => { cycleTerritory = () => {
if (this.stone) { if (this.stone) {
this.groupMembers.forEach(pt => pt.territory = pt.territory * -1); this.groupMembers.forEach(pt => pt.territory = pt.territory * -1);
} else { } else {
this.groupMembers.forEach(pt => { this.groupMembers.forEach(pt => {
switch (pt.territory) { switch (pt.territory) {
case 1: case 1:
pt.territory = -1; pt.territory = -1;
break; break;
case -1: case -1:
pt.territory = 'd'; pt.territory = 'd';
break; break;
case 'd': case 'd':
pt.territory = 1; pt.territory = 1;
break; break;
} }
}); });
}
} }
}
} }
/*----- app's state (variables) -----*/
let boardState = [];
/*----- cached element references -----*/ /*----- cached element references -----*/
const whiteCapsEl = document.getElementById('white-caps'); const whiteCapsEl = document.getElementById('white-caps');
const blackCapsEl = document.getElementById('black-caps'); const blackCapsEl = document.getElementById('black-caps');
@ -236,6 +245,7 @@ const handiDisplayEl = document.getElementById('handicap');
const boardEl = document.querySelector('#board tbody'); const boardEl = document.querySelector('#board tbody');
const gameStartEl = document.querySelector('input[name="game-start"]'); const gameStartEl = document.querySelector('input[name="game-start"]');
const komiSuggestEl = document.querySelector('input[name="komi-suggest"]'); const komiSuggestEl = document.querySelector('input[name="komi-suggest"]');
const soundPlayerEl = new Audio();
const boardSizeRadioEls = [ const boardSizeRadioEls = [
document.querySelectorAll('input[name="board-size"')[0], document.querySelectorAll('input[name="board-size"')[0],
document.querySelectorAll('input[name="board-size"')[1], document.querySelectorAll('input[name="board-size"')[1],
@ -260,11 +270,80 @@ gameHudEl.addEventListener('click', clickGameHud);
boardSizeEl.addEventListener('click', clickBoardSize); boardSizeEl.addEventListener('click', clickBoardSize);
gameStartEl.addEventListener('click', clickSubmitStart); gameStartEl.addEventListener('click', clickSubmitStart);
/*----- functions -----*/ /*----- FUNCTIONS ----------------------------------*/
/*----- init functions -----*/
init(); 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) { function findPointFromIdx(arr) {
console.log(arr);
return pointFromIdx = boardState.find( point => point.pos[0] === arr[0] && point.pos[1] === arr[1] ); 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) { if (evt.target.id) {
switch (evt.target.id) { switch (evt.target.id) {
case 'black-rank-up': case 'black-rank-up':
gameState.playerMeta.b.rank++; if (gameState.playerMeta.b.rank < RANKS.length - 1) gameState.playerMeta.b.rank++;
break; break;
case 'black-rank-down': case 'black-rank-down':
gameState.playerMeta.b.rank--; if (gameState.playerMeta.b.rank > 0) gameState.playerMeta.b.rank--;
break; break;
case 'white-rank-up': case 'white-rank-up':
gameState.playerMeta.w.rank++; if (gameState.playerMeta.w.rank < RANKS.length - 1) gameState.playerMeta.w.rank++;
break; break;
case 'white-rank-down': case 'white-rank-down':
gameState.playerMeta.w.rank--; if (gameState.playerMeta.w.rank > 0) gameState.playerMeta.w.rank--;
break; break;
} }
} }
@ -328,109 +407,54 @@ function clickKomiSuggestion(evt) {
renderMenu(); 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) { function clickCloseMenu(evt) {
evt.stopPropagation(); evt.stopPropagation();
if (evt.target.className === "modal" && gameState.gameMeta.start) modalEl.style.visibility = 'hidden'; if (evt.target.className === "modal" && gameState.gameMeta.start) modalEl.style.visibility = 'hidden';
} }
function clickResign(evt) { /*----- gameplay functions -----*/
if (evt.target.parentElement.id === `${STONES_DATA[gameState.turn]}-caps-space`) playerResign();
}
function playerResign() { function clickBoard(evt) {
// 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) {
evt.stopPropagation(); evt.stopPropagation();
if (gameState.pass > 1 || gameState.winner) return; if (gameState.pass > 1 || gameState.winner) return editTerritory(evt);
// renders preview stone if move is legal // checks for placement and pushes to cell
let hover = evt.target.closest('td').id.split('-'); let placement = [ parseInt(evt.target.closest('td').id.split('-')[0]), parseInt(evt.target.closest('td').id.split('-')[1]) ];
hover = [parseInt(hover[0]), parseInt(hover[1])] let point = findPointFromIdx(placement);
let point = findPointFromIdx(hover); //checks that this placement was marked as legal
if (checkLegal(point)) { if ( !checkLegal(point) ) return;
point.legal = true; // legal clearKo();
renderPreview(point); 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) { function checkKo(point, cap) {
if (!point.getLiberties().length && cap.checkNeighbors().filter(stone => stone.stone === gameState.turn * -1) if (!point.getLiberties().length && cap.checkNeighbors().filter(stone => stone.stone === gameState.turn * -1)
&& point.capturing.length === 1) return true; && point.capturing.length === 1) return true;
} }
function clickBoard(evt) { function playSound(point) { //plays louder sounds for tenuki and for captures
evt.stopPropagation(); if (point.capturing.length || (gameState.boardSize === 19 && gameState.gameRecord.length > 90 && point.groupMembers.length === 1)
if (gameState.pass > 1 || gameState.winner) return editTerritory(evt); || (gameState.boardSize === 13 && gameState.gameRecord.length > 40 && point.groupMembers.length === 1)) {
// checks for placement and pushes to cell soundPlayerEl.src = PLACEMENT_SOUNDS.loud[Math.floor(Math.random() * 5)];
let placement = [ parseInt(evt.target.closest('td').id.split('-')[0]), parseInt(evt.target.closest('td').id.split('-')[1]) ]; soundPlayerEl.play();
console.log(placement); } else {
console.log(evt); soundPlayerEl.src = PLACEMENT_SOUNDS.soft[Math.floor(Math.random() * 8)];
let point = findPointFromIdx(placement); soundPlayerEl.play();
//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 clearCaptures() { function clearCaptures() {
@ -520,71 +503,74 @@ function clearCaptures() {
} }
} }
function initBoard() { function clickPass(evt) {
let i = 0; if (evt.target.parentElement.id === `${STONES_DATA[gameState.turn]}-bowl`) playerPass();
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() { function playerPass() {
if (gameState.handicap < 2) return; // display confirmation message
HANDI_PLACE[gameState.boardSize][gameState.handicap].forEach(pt => { clearKo();
if (!pt) return; clearCaptures();
let handi = findPointFromIdx(pt); gameState.gameRecord.push(`${STONES_DATA[gameState.turn]}: pass`)
handi.stone = 1; gameState.pass++;
handi.joinGroup(); if (gameState.pass === 2) return endGame();
}) gameState.turn*= -1;
}
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();
renderGame(); renderGame();
} }
function renderGame() { function clickMenuOpen() {
if (gameState.winner || gameState.pass > 1) { modalEl.style.visibility = 'visible';
renderTerritory(); renderMenu();
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 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() { function renderBoardInit() {
renderClearBoard(); renderClearBoard();
renderBoardTableRows(); renderBoardTableRows();
@ -592,30 +578,11 @@ function renderBoardInit() {
renderBoardTableStyle(); 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() { function renderClearBoard() {
boardEl.innerHTML = ''; boardEl.innerHTML = '';
boardEl.classList = ''; 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() { function renderBoardTableRows() {
let i = 1; let i = 1;
while (i <= gameState.boardSize) { while (i <= gameState.boardSize) {
@ -628,6 +595,7 @@ function renderBoardTableRows() {
boardEl.classList = `board-${gameState.boardSize}x${gameState.boardSize}`; boardEl.classList = `board-${gameState.boardSize}x${gameState.boardSize}`;
} }
// iterator ^ becomes x index ̌
function renderBoardTableCells(x) { function renderBoardTableCells(x) {
let y = 1 let y = 1
let cells = ''; let cells = '';
@ -645,6 +613,69 @@ function renderBoardTableCells(x) {
return cells; 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() { function renderMessage() {
if (gameState.winner && gameState.pass < 2) { if (gameState.winner && gameState.pass < 2) {
gameHudEl.style.visibility = 'visible'; gameHudEl.style.visibility = 'visible';
@ -670,36 +701,40 @@ function renderTerritory() {
}) })
} }
function renderFirstTurn() { /*----- endgame functions -----*/
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');
}); function clickResign(evt) {
document.querySelectorAll(`.bowl`).forEach(bowl => bowl.toggleAttribute('data-turn')); if (evt.target.parentElement.id === `${STONES_DATA[gameState.turn]}-caps-space`) playerResign();
} }
function renderBoardState() { function playerResign() {
boardState.forEach(val => { // display confirmation message
let stoneElem = document.getElementById(`${val.pos[0]}-${val.pos[1]}`).getElementsByClassName('stone')[0]; gameState.pass = -1;
stoneElem.setAttribute("data-stone", STONES_DATA[val.stone]); gameHudEl.style.visibility = "visible";
}) gameHudEl.textContent = "Do you want to resign?";
} }
function renderCaps() { function clickGameHud() {
blackCapsEl.textContent = gameState.playerState.bCaptures; if (gameState.pass > 1 && !gameState.winner) calculateWinner();
whiteCapsEl.textContent = gameState.playerState.wCaptures; if (gameState.pass < 0) confirmResign();
} }
function renderPreview(hoverPoint) { function confirmResign() {
boardState.forEach(val => { gameState.gameRecord.push(`${STONES_DATA[gameState.turn]}: resign`);
let dot = document.getElementById(`${val.pos[0]}-${val.pos[1]}`).getElementsByClassName('dot')[0]; gameState.winner = STONES_DATA[gameState.turn * -1];
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]); 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() { function calculateWinner() {
@ -766,8 +801,3 @@ function emptyPointSetTerritory(emptyPoints) {
}) })
}); });
} }
function endGame() {
if (!gameState.winner) endGameSetTerritory()
renderGame();
}

View file

@ -1,7 +1,7 @@
# Browser Go # 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] displays well on mobile or desktop
- [x] initiates a game with suggested handicap and komi according to rank input - [x] initiates a game with suggested handicap and komi according to rank input
- [x] displays how to play in open screen - [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 - [x] allows users to submit finalized score to game record
- [ ] displays game record as string - [ ] displays game record as string
stretch goals additional features
- [x] uses stone placement GUI for resign and pass - [x] uses stone placement GUI for resign and pass
- [ ] maintains a one move game state history for 'undo mismove' - [ ] maintains a one move game state history for 'undo mismove'
- [ ] converts string to .sgf format - [ ] converts string to .sgf format
- [x] allows users to edit game info mid game - [x] allows users to edit game info mid game
- [ ] add stone placement sounds - [x] add stone placement sounds
superstretch goals
- [x] allows users to select board size (9x9, 13x13, 19x19) - [x] allows users to select board size (9x9, 13x13, 19x19)
- [ ] allows users to load .sgf main lines - [x] 9x9 games simply stretch with screen size
- [ ] allow for responsivity in the form of - [ ] timed game functionality
- - [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
- - [ ] 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
<!-- describe go with images of game--> <!-- describe go with images of game-->