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
##### 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. -->
<!-- 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";
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;
}
}

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>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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="css/reset.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="js/main.js"></script>
<title>Browser Go</title>
<!-- <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/main.css" type="text/css" />
<!-- <script defer src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script> -->
<script defer src="js/main.js"></script>
<title>Browser Go</title>
</head>
<body>
<div class="modal">
@ -25,9 +25,9 @@
<input type="range" min="0" max="9" step="1" value="0" name="handicap-slider">
<div id="board-size-radio" class="menu-subblock">
<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="19">19 x 19
<input type="radio" name="board-size" value="19" checked>19 x 19
</div>
</div>
<div id="player-meta">
@ -2293,7 +2293,7 @@
</div>
</div>
<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">
<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>

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)
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 = {
@ -113,16 +128,6 @@ const HANDI_PLACE = {
]
};
const BOARD_POINT_SIZE = {
'9' : '9vmin',
'13': '6vmin',
'19' : '4vmin'
}
/*----- app's state (variables) -----*/
let boardState = [];
class Point {
constructor(x, y) {
this.pos = [ 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();
}

View file

@ -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
<!-- describe go with images of game-->