pull in client-side go app

modify build script to create working .xdc
This commit is contained in:
sorrel 2024-06-22 18:52:55 -04:00
parent f8cfd28213
commit ee9a6222fa
21 changed files with 1451 additions and 65 deletions

6
build.sh Executable file
View file

@ -0,0 +1,6 @@
#!/bin/sh
rm dist/webxdc-go.xdc 2> /dev/null
# idk why zipping from project dir retains src/ prefix but it does
# and this makes the resultant .xdc unreadable to client
(cd src; zip -9 --recurse-paths "../dist/webxdc-go.xdc" .)

BIN
dist/myapp.xdc vendored

Binary file not shown.

BIN
dist/webxdc-go.xdc vendored Normal file

Binary file not shown.

View file

@ -3,7 +3,7 @@
"webxdc-dev": "^0.9.0" "webxdc-dev": "^0.9.0"
}, },
"scripts": { "scripts": {
"build": "zip -9 --recurse-paths dist/myapp.xdc src", "build": "./build.sh",
"dev": "webxdc-dev run --port 4000 src" "dev": "webxdc-dev run --port 4000 src"
} }
} }

BIN
src/audio/go_loud.wav Normal file

Binary file not shown.

BIN
src/audio/go_loud_2.wav Normal file

Binary file not shown.

BIN
src/audio/go_loud_3.wav Normal file

Binary file not shown.

BIN
src/audio/go_loud_4.wav Normal file

Binary file not shown.

BIN
src/audio/go_soft.wav Normal file

Binary file not shown.

BIN
src/audio/go_soft_2.wav Normal file

Binary file not shown.

BIN
src/audio/go_soft_3.wav Normal file

Binary file not shown.

BIN
src/audio/go_soft_4.wav Normal file

Binary file not shown.

BIN
src/audio/go_soft_5.wav Normal file

Binary file not shown.

BIN
src/audio/go_soft_6.wav Normal file

Binary file not shown.

BIN
src/audio/go_soft_7.wav Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
src/images/board.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View file

@ -1,67 +1,103 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<title>Hello</title> <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"/> <meta http-equiv="X-UA-Compatible" content="ie=edge">
<script src="webxdc.js"></script> <link rel="stylesheet" href="style.css" type="text/css" />
<style type="text/css"> <script src="webxdc.js"></script>
body { <script defer src="main.js"></script>
font-family: Helvetica, Arial, sans-serif; <title>webxdc go</title>
} </head>
</style> <body>
</head> <div class="modal">
<body> <div>
<h1>Hello</h1> <form id="menu">
<form> <div id="game-meta">
<input id="input" type="text" placeholder="Message" autofocus required /> <h1 class="menu-heading">webxdc Go</h1>
<input type="submit" onclick="sendMsg(); return false;" value="Send" /> <div class="menu-subblock"><span class="menu-heading">Date:</span><span id="date"></span></div>
</form> <div class="menu-subblock"><span class="menu-heading">Komi:</span><span id="komi"></span></div>
<p id="output"></p> <input type="range" min="-21.5" max="7.5" step="1" value="5.5" name="komi-slider">
<p><em><small id="deviceName"></small></em></p> <div class="menu-subblock"><span class="menu-heading">Handicap:</span><span id="handicap"></span></div>
<script> <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">9 x 9<br>
<input type="radio" name="board-size" value="13">13 x 13<br>
<input type="radio" name="board-size" value="19" checked>19 x 19
</div>
</div>
<div id="player-meta">
<div data-player-meta="black">
<h4 class="menu-heading">Black</h4>
<span class="menu-heading">Name:</span><input type="text" name="black-name">
<div class="menu-line">
<span class="menu-heading">Rank:</span><span id="black-rank">9k</span><input type="button" id="black-rank-up" value="▲"><input type="button" id="black-rank-down" value="▼">
</div>
<div class="menu-line">
<input type="checkbox" name="black-rank-certain"><label for="black-rank-certain">Rank Certainty</label>
</div>
</div>
<div data-player-meta="white">
<h4 class="menu-heading">White</h4>
<span class="menu-heading">Name:</span><input type="text" name="white-name">
<div class="line">
<span class="menu-heading">Rank:</span><span id="white-rank">9k</span><input type="button" id="white-rank-up" value="▲"><input type="button" id="white-rank-down" value="▼">
</div>
<div class="menu-line">
<input type="checkbox" name="white-rank-certain"><label for="white-rank-certain">Rank Certainty</label>
</div>
</div>
</div>
<div id="game-record-space">
<p id="instructions">
<span class="menu-heading">Welcome to webxdc Go!</span>
<br><br>
If this is your first time playing Go, please see
<a href="https://www.youtube.com/watch?v=gECcsSeRcNo" target="_blank">this great tutorial video.</a><br><br>
To begin a game enter player names and ranks above, then click "Suggest Komi" Browser Go will calculate the appropriate komi based on AGA guidelines.
To override Browser Go's suggestion, use the sliders above. Be sure to check the 'rank certainty' box if you're club-rated.<br><br>
When the game begins, click on a legal point on the board to make a move. The active player's bowl will be highlighted. To pass, click on your bowl.
This will only be possible on your turn. To resign click on your capture tray. After the game ends, groups and territory will display Browser Go's estimate for final state.
Simply click on a group to change a group between live and dead, or a point between territory and dame.
</p>
<p id="game-record"></p>
</div>
<div id="game-update-space">
<input type="submit" name="komi-suggest" value="Suggest Komi"><input type="submit" name="game-start" value="Start!">
</div>
var El = function (tag, text) { </form>
var el = document.createElement(tag); </div>
el.innerText = text || ''; </div>
return el; <content>
}; <div id="white-pos" class="player-pos">
<div id="white-bowl" class="bowl"><p>Pass?</p><div id="white-stone-image" class="stone-image"></div></div>
// handle past and future state updates <div id="white-player-space" class="name-space">
window.webxdc.setUpdateListener(function (update) { <h4 id="white-player-name">Browser Go</h4>
var output = document.getElementById('output'); <div id="white-caps-space" class="caps-space"><p>Resign?</p><p id="white-caps"></p></div>
// when appending content to an element with output.innerHTML += </div>
// that content is implicitly parsed, making it possible for messages <div id="game-hud">
// to be interpreted as scripts. Creating elements directly, <p></p>
// injecting content as plain text, and appending them to the DOM </div>
// is a much safer practice. </div>
[ <div id="board-container">
El('strong', update.payload.name + ':'), <div id="board-space">
El('span', update.payload.msg), <table id="board">
El('br'), <tbody>
].forEach(function (item) { </tbody>
output.appendChild(item); </table>
}); </div>
}); </div>
<div id="black-pos" class="player-pos">
function sendMsg() { <div id="black-bowl" class="bowl"><p>Pass?</p><div id="black-stone-image" class="stone-image"></div></div>
msg = document.getElementById("input").value; <div id="black-player-space" class="name-space">
info = 'someone typed "' + msg + '"'; <h4 id="black-player-name">by oxaliq</h4>
document.getElementById("input").value = ''; <div id="black-caps-space" class="caps-space"><p>Resign?</p><p id="black-caps"></p></div>
</div>
// send new updates <div id="kifu">
window.webxdc.sendUpdate({ </div>
payload: { </div>
name: window.webxdc.selfName, </content>
msg, </body>
},
info,
}, info);
}
(function () {
window.deviceName.innerText = 'this is ' + window.webxdc.selfName;
})()
</script>
</body>
</html> </html>

834
src/main.js Normal file
View file

@ -0,0 +1,834 @@
/*----- constants -----*/
const STONES_DATA = {
'-1': 'white',
'0': 'none',
'1': 'black',
'k': 'ko'
};
const DOTS_DATA = {
'-1': 'white',
'0': 'none',
'1': 'black',
'd': 'dame',
};
const RANKS = [
'30k', '29k', '28k', '27k', '26k', '25k', '24k', '23k', '22k', '21k', '20k',
'19k', '18k', '17k', '16k', '15k', '14k', '13k', '12k', '11k', '10k',
'9k', '8k', '7k', '6k', '5k', '4k', '3k', '2k', '1k',
'1d', '2d', '3d', '4d', '5d', '6d', '7d', '8d', '9d'
];
// index corresponds to difference in player rank
const KOMI_REC = {
'9': [
5.5, 2.5, -0.5, -3.5, -6.5, -9.5, 12.5, 15.5, 18.5, 21.5
],
'13': [
5.5, 0.5, -5.5, 0.5, -5.5, 0.5, -5.5, 0.5, -5.5, 0.5
],
'19': [
7.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5
]
};
const HANDI_REC = {
'9': [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
],
'13': [
0, 0, 0, 2, 2, 3, 3, 4, 4, 5
],
'19': [
0, 0, 2, 3, 4, 5, 6, 7, 8, 9
]
};
const PLACEMENT_SOUNDS = {
soft: [
'audio/go_soft.wav',
'audio/go_soft_2.wav',
'audio/go_soft_3.wav',
'audio/go_soft_4.wav',
'audio/go_soft_5.wav',
'audio/go_soft_6.wav',
'audio/go_soft_7.wav'
],
loud: [
'audio/go_loud.wav',
'audio/go_loud_2.wav',
'audio/go_loud_3.wav',
'audio/go_loud_4.wav',
]
};
const gameState = { // pre-init values (render prior to any player input)
winner: null,
turn: null, // turn logic depends on handicap stones
pass: null, // -1 represents state in which resignation has been submitted, not confirmed
komi: null, // komi depends on handicap stones + player rank
handicap: null,
boardSize: null,
playerState: {
bCaptures: null,
wCaptures: null,
bScore: null,
wScore: null
},
gameMeta: { // declared at game start and not editable after
date: null, // contains metadata
start: false
},
playerMeta: { // editable during game
b: {
name: null,
rank: null,
rankCertain: false
},
w: {
name: null,
rank: null,
rankCertain: false
},
},
groups: {},
gameRecord : []
};
// index represents handicap placement for different board-sizes, eg handiPlace['9][1] = { (3, 3), (7, 7) }
// last array in each property also used for hoshi rendering
const HANDI_PLACE = {
'9' : [
0, 0,
[[ 7, 3 ], [ 3, 7 ] ],
[ [ 7, 7 ], [ 7, 3 ], [ 3, 7 ] ],
[ [ 3, 3 ], [ 7, 7 ], [ 3, 7 ], [ 7, 3 ] ]
],
'13' : [
0, 0,
[ [ 4, 10 ], [ 10, 4 ] ],
[ [ 10, 10 ], [ 4, 10 ], [ 10, 4] ],
[ [ 4, 4 ], [ 10, 10 ], [ 4, 10 ], [ 10, 4] ],
[ [ 7, 7 ], [ 4, 4 ], [ 10, 10 ], [ 4, 10 ], [ 10, 4] ],
[ [ 7, 4 ], [ 4, 7 ], [ 4, 4 ], [ 10, 10 ], [ 4, 10 ], [ 10, 4] ],
[ [ 7, 7 ], [ 7, 4 ], [ 4, 7 ], [ 4, 4 ], [ 10, 10 ], [ 4, 10 ], [ 10, 4] ],
[ [ 10, 7 ], [ 7, 4 ], [ 7, 10 ], [ 4, 7 ], [ 4, 4 ], [ 10, 10 ], [ 4, 10 ], [ 10, 4] ],
[ [ 7, 7 ], [ 10, 7 ], [ 7, 4 ], [ 7, 10 ], [ 4, 7 ], [ 4, 4 ], [ 10, 10 ], [ 4, 10 ], [ 10, 4] ],
],
'19' : [
0, 0,
[ [ 4, 16 ], [ 16, 4 ] ],
[ [ 16, 16 ], [ 4, 16 ], [ 16, 4] ],
[ [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ],
[ [ 10, 10 ], [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ],
[ [ 10, 4 ], [ 4, 10 ], [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ],
[ [ 10, 10 ], [ 10, 4 ], [ 4, 10 ], [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ],
[ [ 16, 10 ], [ 10, 4 ], [ 10, 16 ], [ 4, 10 ], [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ],
[ [ 10, 10 ], [ 16, 10 ], [ 10, 4 ], [ 10, 16 ], [ 4, 10 ], [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ],
]
};
class Point {
constructor(x, y) {
this.pos = [ x, y ];
this.stone = 0; // this is where move placement will go 0, 1, -1, also contains ko: 'k'
this.legal = true;
this.territory = null;
this.capturing = [];
this.groupMembers = [ this ];
this.neighbors = {
top: {},
btm: {},
lft: {},
rgt: {}
};
this.neighbors.top = x > 1 ? [ x - 1, y ] : null;
this.neighbors.btm = x < gameState.boardSize ? [ x + 1, y ] : null;
this.neighbors.rgt = y < gameState.boardSize ? [ x, y + 1 ] : null;
this.neighbors.lft = y > 1 ? [ x, y - 1 ] : null;
}
checkNeighbors = () => {
let neighborsArr = [];
for (let neighbor in this.neighbors) {
let nbr = this.neighbors[neighbor];
// neighbor exists it's point is stored as { rPos, cPos}
if ( nbr !== null ) {
neighborsArr.push(boardState.find(pt => pt.pos[0] === nbr[0] && pt.pos[1] === nbr[1]));
}
};
// returns array of existing neighbors to calling function
return neighborsArr;
}
getLiberties = () => {
let neighborsArr = this.checkNeighbors().filter(pt => pt.stone === 0);
return neighborsArr;
}
joinGroup = () => {
this.groupMembers = this.groupMembers.filter(grp => grp.stone === this.stone);
this.groupMembers.push(this);
let frns = this.checkNeighbors().filter(nbr => nbr.stone === this.stone);
for (let frn of frns) {
this.groupMembers.push(frn);
}
this.groupMembers = Array.from(new Set(this.groupMembers));
for (let grpMem in this.groupMembers) {
this.groupMembers = Array.from(new Set(this.groupMembers.concat(this.groupMembers[grpMem].groupMembers)));
}
for (let grpMem in this.groupMembers) {
this.groupMembers[grpMem].groupMembers = Array.from(new Set(this.groupMembers[grpMem].groupMembers.concat(this.groupMembers)));
}
}
checkCapture = () => {
let opps = this.checkNeighbors().filter(nbr => nbr.stone === gameState.turn * -1
&& nbr.getLiberties().every(liberty => liberty === this));
for (let opp of opps) {
if (opp.groupMembers.every(stone => stone.getLiberties().filter(liberty => liberty !== this).length === 0)) {
this.capturing = this.capturing.concat(opp.groupMembers);
};
}
this.capturing = Array.from(new Set(this.capturing));
return this.capturing;
}
checkGroup = () => { // liberty is true when called by move false when called by check Capture
let frns = this.checkNeighbors().filter(nbr => nbr.stone === gameState.turn);
for (let frn in frns) {
if (frns[frn].groupMembers.find(stone => stone.getLiberties().find(liberty => liberty !== this))) return true;
continue;
}
}
cycleTerritory = () => {
if (this.stone) {
this.groupMembers.forEach(pt => pt.territory = pt.territory * -1);
} else {
this.groupMembers.forEach(pt => {
switch (pt.territory) {
case 1:
pt.territory = -1;
break;
case -1:
pt.territory = 'd';
break;
case 'd':
pt.territory = 1;
break;
}
});
}
}
}
/*----- app's state (variables) -----*/
let boardState = [];
/*----- cached element references -----*/
const whiteCapsEl = document.getElementById('white-caps');
const blackCapsEl = document.getElementById('black-caps');
const modalEl = document.querySelector('.modal');
const komiSliderEl = document.querySelector('input[name="komi-slider"]');
const handiSliderEl = document.querySelector('input[name="handicap-slider"]');
const blackRankEl = document.getElementById('black-rank');
const blackRankUpEl = document.getElementById('black-rank-up');
const blackRankDownEl = document.getElementById('black-rank-down');
const whiteRankEl = document.getElementById('white-rank');
const whiteRankUpEl = document.getElementById('black-rank-up');
const whiteRankDownEl = document.getElementById('black-rank-down');
const blackNameInputEl = document.querySelector('input[name="black-name"]');
const whiteNameInputEl = document.querySelector('input[name="white-name"]');
const blackNameDisplayEl = document.querySelector('h4#black-player-name');
const whiteNameDisplayEl = document.querySelector('h4#white-player-name');
const gameHudEl = document.querySelector('#game-hud p');
const dateEl = document.getElementById('date');
const boardSizeEl = document.getElementById('board-size-radio');
const komiDisplayEl = document.getElementById('komi');
const handiDisplayEl = document.getElementById('handicap');
const boardEl = document.querySelector('#board tbody');
const gameStartEl = document.querySelector('input[name="game-start"]');
const komiSuggestEl = document.querySelector('input[name="komi-suggest"]');
const soundPlayerEl = new Audio();
const boardSizeRadioEls = [
document.querySelectorAll('input[name="board-size"')[0],
document.querySelectorAll('input[name="board-size"')[1],
document.querySelectorAll('input[name="board-size"')[2]
];
/*----- event listeners -----*/
document.getElementById('board').addEventListener('mousemove', hoverPreview);
document.getElementById('board').addEventListener('click', clickBoard);
document.getElementById('white-bowl').addEventListener('click',clickPass);
document.getElementById('black-bowl').addEventListener('click',clickPass);
document.getElementById('kifu').addEventListener('click', clickMenuOpen);
document.getElementById('white-caps-space').addEventListener('click', clickResign);
document.getElementById('black-caps-space').addEventListener('click', clickResign);
modalEl.addEventListener('click', clickCloseMenu);
komiSliderEl.addEventListener('change', changeUpdateKomi);
handiSliderEl.addEventListener('change', changeUpdateHandicap);
document.getElementById('player-meta').addEventListener('click', clickUpdatePlayerMeta);
document.getElementById('player-meta').addEventListener('change', clickUpdatePlayerMeta);
komiSuggestEl.addEventListener('click', clickKomiSuggestion);
gameHudEl.addEventListener('click', clickGameHud);
boardSizeEl.addEventListener('click', clickBoardSize);
gameStartEl.addEventListener('click', clickSubmitStart);
/*----- FUNCTIONS ----------------------------------*/
/*----- init functions -----*/
init();
function init() {
gameState.gameMeta.date = getDate();
gameState.komi = 5.5;
gameState.handicap = 0;
gameState.winner = null;
gameState.pass = null;
gameState.boardSize = 19;
gameState.playerState.bCaptures = 0;
gameState.playerMeta.b.rank = 21;
gameState.playerState.wCaptures = 0;
gameState.playerMeta.w.rank = 21;
gameState.gameRecord = [];
boardState = [];
gameState.gameMeta.start = false;
startMenu();
};
function getDate() {
let d = new Date;
return `${d.getFullYear()}-${String(d.getMonth()+1).charAt(-1)||0}${String(d.getMonth()+1).charAt(-0)}-${String(d.getDate()).charAt(-1)||0}${String(d.getDate()+1).charAt(-0)}`;
}
function startMenu() {
modalEl.style.visibility = 'visible';
renderMenu();
}
function clickSubmitStart(evt) {
if (gameState.gameMeta.start) return init();
evt.preventDefault();
evt.stopPropagation();
gameState.playerMeta.b.name = blackNameInputEl.value || 'black';
gameState.playerMeta.w.name = whiteNameInputEl.value || 'white';
modalEl.style.visibility = 'hidden';
return initGame();
}
function initGame() {
gameState.winner = null;
gameState.pass = null;
gameState.turn = gameState.handicap ? -1 : 1;
gameState.gameMeta.start = true;
initBoard();
renderBoardInit();
renderGame();
}
function initBoard() {
let i = 0;
while (i < gameState.boardSize * gameState.boardSize) {
let point = new Point( Math.floor(i / gameState.boardSize) + 1, i % gameState.boardSize + 1);
boardState.push(point);
i++;
}
initHandi();
}
function initHandi() {
if (gameState.handicap < 2) return;
HANDI_PLACE[gameState.boardSize][gameState.handicap].forEach(pt => {
if (!pt) return;
let handi = findPointFromIdx(pt);
handi.stone = 1;
handi.joinGroup();
});
}
/*----- meta functions -----*/
// include save function
// unpack existing gamerecords
// globalGameRecord = JSON.parse(localStorage.getItem('browser-go-saved-games'));
// append current game record to globalGameRecord - globalGameRecord.gameName = gameState
// stringify all stored gamerecords JSON.stringify(globalGameRecord)
// localStorage.clear()
// localStorage.setItem('browser-go-saved-games')
// load function
// unpack existing gamerecords - globalGameRecord = JSON.parse(localStorage.getItem('browser-))
// display each game record name - Object.keys(globalGameRecord).forEach( ... )
// upon user selection initgame from gameState meta data
// undo last move
// set up gameState var to track number of 'misclicks'
// after every move JSON.stringify(localGameRecord)
// localStorage.clear()
// localStorage.setItem()
// on undo click - load JSON.parse(localStorage.getItem())
// reset gameState
// plus general purpose
function findPointFromIdx(arr) {
return pointFromIdx = boardState.find( point => point.pos[0] === arr[0] && point.pos[1] === arr[1] );
}
function changeUpdateKomi(evt) {
evt.stopPropagation();
komiDisplayEl.textContent = komiSliderEl.value;
gameState.komi = komiSliderEl.value;
renderMenu();
}
function changeUpdateHandicap(evt) {
evt.stopPropagation();
handiDisplayEl.textContent = handiSliderEl.value !== 1 ? handiSliderEl.value : 0;
gameState.handicap = handiSliderEl.value !== 1 ? handiSliderEl.value : 0;
renderMenu();
}
function clickUpdatePlayerMeta(evt) {
evt.stopPropagation();
if (evt.target.id) {
switch (evt.target.id) {
case 'black-rank-up':
if (gameState.playerMeta.b.rank < RANKS.length - 1) gameState.playerMeta.b.rank++;
break;
case 'black-rank-down':
if (gameState.playerMeta.b.rank > 0) gameState.playerMeta.b.rank--;
break;
case 'white-rank-up':
if (gameState.playerMeta.w.rank < RANKS.length - 1) gameState.playerMeta.w.rank++;
break;
case 'white-rank-down':
if (gameState.playerMeta.w.rank > 0) gameState.playerMeta.w.rank--;
break;
}
}
if (evt.target.name == 'black-rank-certain') gameState.playerMeta.b.rankCertain = !gameState.playerMeta.b.rankCertain;
if (evt.target.name == 'white-rank-certain') gameState.playerMeta.w.rankCertain = !gameState.playerMeta.w.rankCertain;
renderMenu();
}
function clickBoardSize(evt) {
evt.stopPropagation();
gameState.boardSize = boardSizeRadioEls.find(el => el.checked === true).value;
renderMenu();
}
function clickKomiSuggestion(evt) {
evt.preventDefault();
evt.stopPropagation();
if (gameState.gameMeta.start) {
gameState.playerMeta.b.name = blackNameInputEl.value || 'black';
gameState.playerMeta.w.name = whiteNameInputEl.value || 'white';
modalEl.style.visibility = 'hidden';
return;
}
let sugg = KOMI_REC[gameState.boardSize][Math.abs(gameState.playerMeta.w.rank - gameState.playerMeta.b.rank)];
let handi = HANDI_REC[gameState.boardSize][Math.abs(gameState.playerMeta.w.rank - gameState.playerMeta.b.rank)];
gameState.komi = sugg;
gameState.handicap = handi;
renderMenu();
}
function clickCloseMenu(evt) {
evt.stopPropagation();
if (evt.target.className === "modal" && gameState.gameMeta.start) modalEl.style.visibility = 'hidden';
}
/*----- gameplay functions -----*/
function clickBoard(evt) {
evt.stopPropagation();
if (gameState.pass > 1 || gameState.winner) return editTerritory(evt);
// checks for placement and pushes to cell
let placement = [ parseInt(evt.target.closest('td').id.split('-')[0]), parseInt(evt.target.closest('td').id.split('-')[1]) ];
let point = findPointFromIdx(placement);
//checks that this placement was marked as legal
if ( !checkLegal(point) ) return null;
clearKo();
clearPass();
resolveCaptures(point);
point.stone = gameState.turn;
point.joinGroup();
playSound(point);
clearCaptures();
gameState.gameRecord.push(`${STONES_DATA[gameState.turn]}: ${point.pos}`);
gameState.turn*= -1;
renderGame();
}
function clearKo() {
for (let point in boardState) {
point = boardState[point];
point.stone = point.stone === 'k' ? 0 : point.stone;
}
}
function clearPass() {
gameState.pass = 0;
}
function resolveCaptures(point) {
if(!point.capturing.length) {
point.checkCapture();
}
if(point.capturing.length) {
point.capturing.forEach(cap => {
gameState.playerState[gameState.turn > 0 ? 'bCaptures' : 'wCaptures']++;
cap.stone = checkKo(point) ? 'k' : 0;
cap.groupMembers = [];
});
}
}
function checkLegal(point) {
clearOverlay();
// first step in logic: is point occupied, or in ko
if (point.stone) return false;
// if point is not empty check if liberties
if (point.getLiberties().length < 1) {
//if no liberties check if enemy group has liberties
if ( point.checkCapture().length ) return true;
//if neighboring point is not empty check if friendly group is alive
if (point.checkGroup()) return true;
return false;
}
return true;
}
function clearOverlay() {
for (let point in boardState) {
point = boardState[point];
point.legal = false;
}
}
//
function checkKo(point) { // currently prevents snapback // capturing point has no liberties and is only capturing one stone and
if (!point.getLiberties().length && point.capturing.length === 1 && !point.checkNeighbors().some(stone => stone.stone === gameState.turn)) return true;
return false;
}
function playSound(point) { //plays louder sounds for tenuki and for captures
if (point.capturing.length || (gameState.boardSize === 19 && gameState.gameRecord.length > 90 && point.groupMembers.length === 1)
|| (gameState.boardSize === 13 && gameState.gameRecord.length > 40 && point.groupMembers.length === 1)) {
soundPlayerEl.src = PLACEMENT_SOUNDS.loud[Math.floor(Math.random() * 5)];
soundPlayerEl.play();
} else {
soundPlayerEl.src = PLACEMENT_SOUNDS.soft[Math.floor(Math.random() * 8)];
soundPlayerEl.play();
}
}
function clearCaptures() {
for (let point in boardState) {
point = boardState[point];
point.capturing = [];
}
}
function clickPass(evt) {
if (evt.target.parentElement.id === `${STONES_DATA[gameState.turn]}-bowl`) playerPass();
}
function playerPass() {
// display confirmation message
clearKo();
clearCaptures();
gameState.gameRecord.push(`${STONES_DATA[gameState.turn]}: pass`);
gameState.pass++;
if (gameState.pass === 2) return endGame();
gameState.turn*= -1;
return renderGame();
}
function clickMenuOpen() {
modalEl.style.visibility = 'visible';
renderMenu();
}
function hoverPreview(evt) {
evt.stopPropagation();
if (gameState.pass > 1 || gameState.winner) return;
// renders preview stone if move is legal
let hover = evt.target.closest('td').id.split('-');
hover = [parseInt(hover[0]), parseInt(hover[1])];
let point = findPointFromIdx(hover);
if (checkLegal(point)) {
point.legal = true; // legal
renderPreview(point);
}
}
/*----- render functions ----------------------*/
/*----- meta render -----*/
function renderMenu() {
dateEl.textContent = gameState.gameMeta.date;
if (gameState.gameMeta.start) {
gameStartEl.value = "New Game";
komiSuggestEl.value = "Close Menu";
}
renderKomiSlider();
renderHandiSlider();
renderBoardSizeRadio();
blackRankEl.textContent = RANKS[gameState.playerMeta.b.rank];
whiteRankEl.textContent = RANKS[gameState.playerMeta.w.rank];
}
function renderKomiSlider() {
komiSliderEl.value = gameState.komi;
komiDisplayEl.textContent = gameState.komi;
if (gameState.gameMeta.start) komiSliderEl.setAttribute('disabled', true);
}
function renderHandiSlider() {
handiSliderEl.value = gameState.handicap;
handiDisplayEl.textContent = gameState.handicap;
if (gameState.gameMeta.start) handiSliderEl.setAttribute('disabled', true);
}
function renderBoardSizeRadio() {
boardSizeEl.value = gameState.boardSize;
if (gameState.gameMeta.start) boardSizeRadioEls.forEach(el => el.setAttribute('disabled', true));
}
/*----- game render -----*/
function renderBoardInit() {
renderClearBoard();
renderBoardTableRows();
renderHoshi();
renderBoardTableStyle();
}
function renderClearBoard() {
boardEl.innerHTML = '';
boardEl.classList = '';
}
function renderBoardTableRows() {
let i = 1;
while (i <= gameState.boardSize) {
let tableRow = document.createElement('tr');
tableRow.id = `row-${i}`;
tableRow.innerHTML = renderBoardTableCells(i);
boardEl.appendChild(tableRow);
i++;
}
boardEl.classList = `board-${gameState.boardSize}x${gameState.boardSize}`;
}
// iterator ^ becomes x index ̌
function renderBoardTableCells(x) {
let y = 1;
let cells = '';
while (y <= gameState.boardSize) {
let newCell = `
<td id="${x}-${y}">
<div class="stone">
<div class="dot"></div>
</div>
</td>
`;
cells = cells + newCell;
y++;
}
return cells;
}
function renderHoshi() { // gets hoshi placement from handiplace const and adds a class to dot elem
let hoshi = HANDI_PLACE[gameState.boardSize].slice(-1);
hoshi = hoshi[0];
hoshi.forEach(star => {
let boardPt = document.getElementById(`${star[0]}-${star[1]}`).getElementsByClassName('stone')[0];
boardPt.className += ' hoshi';
});
}
function renderBoardTableStyle() {
document.querySelectorAll('#board-space td[id^="1-"]').forEach(pt => pt.className += 'top ');
document.querySelectorAll(`#board-space td[id^="${gameState.boardSize}-"]`).forEach(pt => pt.className += 'btm ');
document.querySelectorAll('#board-space td[id$="-1"]').forEach(pt => pt.className += 'lft ');
document.querySelectorAll(`#board-space td[id$="-${gameState.boardSize}"]`).forEach(pt => pt.className += 'rgt ');
}
function renderGame() {
if (gameState.winner || gameState.pass > 1) {
renderTerritory();
renderMessage();
}
blackNameDisplayEl.textContent =
`${gameState.playerMeta.b.name},
${RANKS[gameState.playerMeta.b.rank]}`;
whiteNameDisplayEl.textContent =
`${gameState.playerMeta.w.name},
${RANKS[gameState.playerMeta.w.rank]}`;
gameState.gameRecord.length ? renderTurn() : renderFirstTurn();
renderBoardState();
renderCaps();
}
function renderFirstTurn() {
document.getElementById(`${STONES_DATA[gameState.turn]}-bowl`).toggleAttribute('data-turn');
}
function renderTurn() {
if (gameState.winner || gameState.pass > 1) document.querySelectorAll(`.bowl`).forEach(bowl => {
bowl.removeAttribute('data-turn');
bowl.toggleAttribute('data-turn');
});
document.querySelectorAll(`.bowl`).forEach(bowl => bowl.toggleAttribute('data-turn'));
}
function renderBoardState() {
boardState.forEach(val => {
let stoneElem = document.getElementById(`${val.pos[0]}-${val.pos[1]}`).getElementsByClassName('stone')[0];
stoneElem.setAttribute("data-stone", STONES_DATA[val.stone]);
});
}
function renderCaps() {
blackCapsEl.textContent = gameState.playerState.bCaptures;
whiteCapsEl.textContent = gameState.playerState.wCaptures;
}
function renderPreview(hoverPoint) {
boardState.forEach(val => {
let dot = document.getElementById(`${val.pos[0]}-${val.pos[1]}`).getElementsByClassName('dot')[0];
dot.setAttribute("data-dot", val.legal === true && val.pos[0] === hoverPoint.pos[0] && val.pos[1] === hoverPoint.pos[1]
? DOTS_DATA[gameState.turn] : DOTS_DATA[0]);
});
}
function renderMessage() {
if (gameState.winner && gameState.pass < 2) {
gameHudEl.style.visibility = 'visible';
gameHudEl.style.cursor = 'default';
gameHudEl.textContent = `${gameState.playerMeta[gameState.winner === 1 ? 'b' : 'w'].name} won by resignation`;
}
else if (gameState.winner && gameState.pass > 1) {
gameHudEl.style.visibility = 'visible';
gameHudEl.style.cursor = 'default';
gameHudEl.textContent = `${gameState.playerMeta[gameState.winner === 1 ? 'b' : 'w'].name || STONES_DATA[gameState.winner]} won by ${Math.abs(gameState.playerState.wScore - gameState.playerState.bScore)}`;
} else if (gameState.pass > 1) {
gameHudEl.style.visibility = 'visible';
gameHudEl.textContent = 'finalize game';
} else {
gameHudEl.style.visibility = 'hidden';
}
}
function renderTerritory() {
boardState.forEach(val => {
let stoneElem = document.getElementById(`${val.pos[0]}-${val.pos[1]}`).getElementsByClassName('dot')[0];
stoneElem.setAttribute("data-dot", DOTS_DATA[val.territory]);
});
}
/*----- endgame functions -----*/
function clickResign(evt) {
if (evt.target.parentElement.id === `${STONES_DATA[gameState.turn]}-caps-space`) playerResign();
}
function playerResign() {
// display confirmation message
gameState.pass = -1;
gameHudEl.style.visibility = "visible";
gameHudEl.textContent = "Do you want to resign?";
}
function clickGameHud() {
if (gameState.pass > 1 && !gameState.winner) calculateWinner();
if (gameState.pass < 0) confirmResign();
}
function confirmResign() {
gameState.gameRecord.push(`${STONES_DATA[gameState.turn]}: resign`);
gameState.winner = STONES_DATA[gameState.turn * -1];
endGame();
}
function endGame() {
if (!gameState.winner) endGameSetTerritory();
renderGame();
}
function editTerritory(evt) {
let placement = [ parseInt(evt.target.closest('td').id.split('-')[0]), parseInt(evt.target.closest('td').id.split('-')[1]) ];
let point = findPointFromIdx(placement);
point.cycleTerritory();
renderGame();
}
function calculateWinner() {
let whiteTerritory = boardState.reduce((acc, pt) => {
if (pt.territory === -1 && pt.stone !== -1) {
return acc = acc + (pt.stone === 0 ? 1 : 2);
}
return acc;
}, 0);
let blackTerritory = boardState.reduce((acc, pt) => {
if (pt.territory === 1 && pt.stone !== 1) {
return acc + (pt.stone === 0 ? 1 : 2);
}
return acc;
}, 0);
gameState.playerState.wScore =
gameState.playerState.wCaptures
+ (gameState.komi < 0 ? gameState.komi * -1 : 0)
+ whiteTerritory;
gameState.playerState.bScore =
gameState.playerState.bCaptures
+ (gameState.komi > 0 ? gameState.komi : 0)
+ blackTerritory;
gameState.winner = gameState.playerState.wScore > gameState.playerState.bScore ? -1 : 1;
gameState.gameRecord.push(`${STONES_DATA[gameState.winner]}: +${Math.abs(gameState.playerState.wScore - gameState.playerState.bScore)}`);
renderGame();
}
function endGameSetTerritory() {
let emptyPoints = boardState.filter(pt => !pt.stone);
emptyPoints.forEach(pt => pt.joinGroup());
emptyPointSetTerritory(emptyPoints);
groupsMarkDeadLive();
// reviseTerritory();
}
function groupsMarkDeadLive() {
boardState.filter(pt => (!pt.territory ))
.forEach(pt => {
if (pt.groupMembers.some(grpMem => {
return grpMem.checkNeighbors().some(nbr => nbr.territory === pt.stone && nbr.stone === 0);
})) {
pt.groupMembers.forEach(grpMem => grpMem.territory = pt.stone);
}
});
boardState.filter(pt => (!pt.territory)).forEach(pt => {
pt.territory = pt.stone * -1;
});
}
function emptyPointSetTerritory(emptyPoints) {
emptyPoints.filter(pt => !pt.territory && pt.checkNeighbors().filter(nbr => nbr.stone !== 0))
.forEach(pt => {
let b = pt.groupMembers.reduce((acc, grpMem) => {
let bNbr = grpMem.checkNeighbors().filter(nbr => nbr.stone === 1).length;
return acc + bNbr;
}, 0);
let w = pt.groupMembers.reduce((acc, grpMem) => {
let wNbr = grpMem.checkNeighbors().filter(nbr => nbr.stone === -1).length;
return acc + wNbr;
}, 0);
pt.groupMembers.forEach(grp => {
if (Math.abs(b - w) < 4 && b && w) grp.territory = 'd';
else grp.territory = b > w ? 1 : -1;
});
});
}
function reviseTerritory() {
// count eyes
// for each group marked live get liberties
//
}

510
src/style.css Normal file
View file

@ -0,0 +1,510 @@
* {
box-sizing: border-box;
margin: 0;
vertical-align: middle;
font-family: arial, sans-serif;
}
html {
font-size: 12px;
background: radial-gradient(farthest-corner at 55% 40%, rgb(150, 200, 220) 0%, rgb(97, 166, 194) 65%, rgb(70,100,120) 90%, rgb(40, 80, 90) 100%);
}
body {
height: vh;
width: vw;
display: flex;
justify-content: center;
}
.modal {
display: flex;
position: fixed;
z-index: 2;
/* display: none; */
width: 100vw;
height: 100vh;
background-color: rgba(0,0,0,0.3);
align-items: flex-start;
justify-content: center;
visibility: hidden;
overflow-y: scroll;
}
#menu {
position: relative;
background-color: rgb(250, 2250, 255, 0.9);
padding: 1vmin;
display: grid;
grid-template-columns: 60vw;
grid-template-rows: auto auto 60vw auto;
grid-template-areas:
"meta"
"player"
"record"
"submit";
font-family: 'La Belle Aurore', cursive;
min-height: 0;
max-height: 100vh;
z-index: 3;
}
#menu .menu-subblock {
display: flex;
justify-content: space-between;
margin: .25em;
}
#game-meta {
grid-area: meta;
display: flex;
flex-direction: column;
}
.menu-heading, content, #instructions, div[data-player-meta] label {
font-family: 'Raleway', sans-serif;
}
h1 {
font-size: 140%;
font-weight: 600;
}
h4 {
font-weight: 600;
margin: .25em;
font-size: 110%;
}
#player-meta {
grid-area: player;
display: flex;
justify-items: stretch;
flex-direction: column;
}
#player-meta span[id$="rank"] {
margin: 0 2em;
}
#player-meta input[type="button"] {
margin: .25em;
}
#player-meta * .menu-line {
display: flex;
flex-flow: row nowrap;
align-items: baseline;
justify-items: flex-start;
}
div[data-player-meta] {
width: 100%;
justify-self: stretch;
}
div[data-player-meta] input[type="text"] {
width: 90%;
justify-self: stretch;
}
div[data-player-meta] input {
margin: 1vmin;
}
#confirm {
visibility: hidden;
}
div[data-player-meta] label {
margin: .25em;
font-size: 100%;
}
#game-record-space {
grid-area: record;
}
#instructions {
padding: .5em;
line-height: 1.5;
overflow: scroll;
height: 100%;
width: 100%
}
#instructions, #game-record{
border: 2px solid black;
height: 1;
}
#game-record {
visibility: hidden;
}
#game-update-space {
grid-area: submit;
margin: .5em;
display: flex;
justify-content: space-between;
}
content {
display: flex !important;
flex-direction: column;
justify-content: space-between !important;
height: 100vh;
width: 100vw;
}
.player-pos {
display: flex;
align-items: flex-end;
justify-content: space-around;
flex: 5;
height: 9vmin;
}
#game-hud p {
font-size: 130%;
width: 100%;
order: 0;
width: 10vh;
background-color: rgba(0,0,0,0.3);
padding: 1vh;
color: #fff;
cursor: pointer;
visibility: hidden;
}
.player-pos#black-pos {
align-items: flex-start;
flex-direction: row-reverse;
justify-self: flex-end;
}
#kifu {
order: 0;
height: 10vh;
width: 8vh;
background-color: #FFF;
transform: rotate(-20deg);
}
.bowl {
order: -1;
margin: 4vh;
height: 15vh;
width: 15vh;
/* border: solid black; */
border-radius: 50%;
background-color: rgb(116, 48, 17);
background: radial-gradient(farthest-corner at 48% 54%, rgba(30, 5, 0, 0.25) 0%, rgba(30, 5, 0, 0.45) 2%, rgba(30, 5, 0, 0.75) 32%, rgb(0,0,0,0.85)35%, rgb(116,48,17) 48%, rgb(140, 60, 40) 52%, rgb(100, 40, 5) 55%, rgb(116, 48, 17) 58%, rgb(140,60,40) 65%, rgb(100, 40, 5) 80%, rgb(80, 20, 0) 90%);
box-shadow: -1vmin 2vmin 1.5vmin rgba(83, 53, 35, 0.61);
display: flex;
align-items: center;
justify-content: center;
}
.bowl p {
display: none;
}
.stone-image {
height: 100%;
width: 100%;
border-radius: 50%;
background-size: cover;
z-index: -1;
}
#white-stone-image {
background-image: url(../images/white-stones-bowl.jpg);
}
#black-stone-image {
background-image: url(../images/black-stones-bowl.jpg);
}
.bowl[data-turn]:hover p {
display: block;
color: #FFF;
background-color: rgba(0,0,0,0.3);
padding: .5em;
cursor: grab;
}
.bowl[data-turn] {
box-shadow: 0 0 3vh 3vh rgb(255, 175, 2);
}
.caps-space {
color: #FFF;
margin: 1vh;
height: 10vh;
width: 10vh;
border-radius: 50%;
background: radial-gradient(farthest-side at 49% 52%, rgb(150, 75, 50) 0%, rgb(116,48,17) 35%, rgb(116,48,17) 64%, rgb(80, 20, 0) 65%, rgb(175, 140, 95) 70%, rgb(120, 50, 40) 80%, rgb(80, 20, 0) 95%, rgb(175, 140, 95) 100%);
box-shadow: -0.5vmin 1vmin 1vmin rgba(83, 53, 35, 0.61);
display: flex;
align-items: center;
justify-content: center;
}
.caps-space :first-child {
display: none;
}
.bowl[data-turn] + .name-space .caps-space:hover :first-child {
display: block;
position: absolute;
background-color: rgba(0,0,0,0.7);
padding: .5em;
cursor: grab;
}
.name-space {
order: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.name-space h4 {
font-size: 120%;
color: rgb(255,240,230);
background-color: rgba(0,0,0,0.7);
padding: 0.25em;
z-index: 1;
}
#board-container {
width: 100%;
display: grid;
grid-template-areas: 100%;
grid-template-columns: 100%;
grid-template-areas:
"board";
}
#board-space tbody {
background: radial-gradient(farthest-corner at 55% 40%, rgba(244, 230, 120, 0.75) 0%, rgba(234, 178, 78, 0.5) 65%, rgba(200, 160, 90, 0.45) 90%, rgba(200, 140, 90, 0.45) 100%);
background-size: cover;
padding: 1vmin;
}
#board-space {
grid-area: board;
margin: 0 auto;
display: flex;
flex-direction: column;
background-image: url(../images/board.png);
z-index: 1;
box-shadow: -2vmin 4vmin 3vmin rgba(145, 92, 23, 0.5);
flex: 1;
}
#board-space table {
display: flex;
align-items: stretch;
justify-content: space-between;
margin: auto;
}
#board-space td {
background: conic-gradient(#000 0%, rgba(0,0,0,0) 1%, rgba(0,0,0,0) 24%, #000 25%, rgba(0,0,0,0) 26%, rgba(0,0,0,0) 49%, #000 50%, rgba(0,0,0,0) 51%, rgba(0,0,0,0) 74%, #000 75%, rgba(0,0,0,0) 76%, rgba(0,0,0,0) 99%, #000 100%);
border-radius: 50% solid black;
color: black;
margin: auto;
padding: 0;
vertical-align: middle;
}
#board-space .board-9x9 td {
height: 9vmin;
width: 9vmin;
}
#board-space .board-13x13 td {
height: 7vmin;
width: 7vmin;
}
#board-space .board-19x19 td {
height: 5vmin;
width: 5vmin;
}
#board-space td.top {
background: conic-gradient( rgba(0,0,0,0) 24%, #000 25%, rgba(0,0,0,0) 26%, rgba(0,0,0,0) 49%, #000 50%, rgba(0,0,0,0) 51%, rgba(0,0,0,0) 74%, #000 75%, rgba(0,0,0,0) 76%);
}
#board-space td.btm {
background: conic-gradient(#000 0%, rgba(0,0,0,0) 1%, rgba(0,0,0,0) 24%, #000 25%, rgba(0,0,0,0) 26%, rgba(0,0,0,0) 74%, #000 75%, rgba(0,0,0,0) 76%, rgba(0,0,0,0) 99%, #000 100%);
}
#board-space td.lft {
background: conic-gradient(#000 0%, rgba(0,0,0,0) 1%, rgba(0,0,0,0) 24%, #000 25%, rgba(0,0,0,0) 26%, rgba(0,0,0,0) 49%, #000 50%, rgba(0,0,0,0) 51%, rgba(0,0,0,0) 99%, #000 100%);
}
#board-space td.rgt {
background: conic-gradient(#000 0%, rgba(0,0,0,0) 1%, rgba(0,0,0,0) 49%, #000 50%, rgba(0,0,0,0) 51%, rgba(0,0,0,0) 74%, #000 75%, rgba(0,0,0,0) 76%, rgba(0,0,0,0) 99%, #000 100%);
}
#board-space td.top.lft {
background: conic-gradient( rgba(0,0,0,0) 24%, #000 25%, rgba(0,0,0,0) 26%, rgba(0,0,0,0) 49%, #000 50%, rgba(0,0,0,0) 51%);
}
#board-space td.top.rgt {
background: conic-gradient( rgba(0,0,0,0) 49%, #000 50%, rgba(0,0,0,0) 51%, rgba(0,0,0,0) 74%, #000 75%, rgba(0,0,0,0) 76% );
}
#board-space td.btm.lft {
background: conic-gradient(#000 0%, rgba(0,0,0,0) 1%, rgba(0,0,0,0) 24%, #000 25%, rgba(0,0,0,0) 26%, rgba(0,0,0,0) 99%, #000 100%);
}
#board-space td.btm.rgt {
background: conic-gradient(#000 0%, rgba(0,0,0,0) 1%, rgba(0,0,0,0) 74%, #000 75%, rgba(0,0,0,0) 76%, rgba(0,0,0,0) 99%, #000 100%);
}
.stone.hoshi {
background: radial-gradient(circle farthest-corner at center, #000 0%, #000 14%, rgba(0,0,0,0) 15%);
z-index: 3;
}
td .stone {
width: 85%;
height: 85%;
border-radius: 50%;
margin: auto;
vertical-align: middle;
display: flex;
flex-direction: column;
justify-content: center;
}
td .stone .dot {
width: 35%;
height: 35%;
border-radius: 50%;
margin: auto;
vertical-align: middle;
}
td .stone[data-stone="ko"] {
background-color: transparent;
border: 1vmin solid rgba(200,20,50,0.8);
border-radius: 0%;
}
td .stone[data-stone="white"] {
background: radial-gradient(farthest-side at 55% 40%, white 0%, rgb(200,200,200) 65%, rgb(100,100,100) 90%, rgb(68, 50, 0) 100%);
box-shadow: -.25vmin .5vmin .5vmin rgba(145, 92, 23, 0.5);
}
td .stone[data-stone="black"] {
background-color: black;
background: radial-gradient(farthest-side at 55% 40%, rgb(220,220,220) 0%, rgb(60,60,60) 45%, rgb(15,15,15) 90%, rgb(5, 5, 0) 100%);
box-shadow: -.25vmin .5vmin .5vmin rgba(145, 92, 23, 0.75);
}
td .stone[data-stone="none"] {
background-color: transparent;
}
td .dot[data-dot="white"] {
background-color: white;
}
td .dot[data-dot="black"] {
background-color: black;
}
td .dot[data-dot="none"] {
background-color: transparent;
}
td .dot[data-dot="dame"] {
background-color: purple;
}
@media only screen and (min-width: 591px) {
#player-meta {
flex-direction: row;
}
div[data-player-meta] {
width: 50%;
}
}
/* Responsive Design */
@media only screen and (min-width: 500px) {
html {
font-size: 14px;
}
.player-pos {
height: 14vh;
}
}
@media only screen and (min-width: 590px) {
#board-space .board-19x19 td {
height: 3.5vh;
width: 3.5vh;
}
}
@media only screen and (min-width: 570px) {
#board-space .board-9x9 td {
height: 7.5vh;
width: 7.5vh;
}
#board-space .board-13x13 td {
height: 5vh;
width: 5vh;
}
.bowl {
order: -1;
margin: 3vh;
height: 10vh;
width: 10vh;
}
.caps-space {
color: #FFF;
margin: 2vh;
height: 7vh;
width: 7vh;
}
}
@media only screen and (min-width: 700px) {
content {
width: 700px;
}
#menu {
grid-template-columns: 50vw;
grid-template-rows: auto auto 50vw auto;
}
}
@media only screen and (min-width: 900px) {
#menu {
grid-template-columns: 55vh;
grid-template-rows: auto auto 55vh auto;
}
}