2019-08-02 21:14:23 +00:00
/*----- constants -----*/
2019-08-03 06:42:49 +00:00
const STONES _DATA = {
2019-08-03 04:19:31 +00:00
'-1' : 'white' ,
2019-08-03 06:42:49 +00:00
'0' : 'none' ,
2019-08-03 04:19:31 +00:00
'1' : 'black' ,
'k' : 'ko'
}
2019-08-03 06:42:49 +00:00
const DOTS _DATA = {
2019-08-03 04:19:31 +00:00
'-1' : 'white' ,
2019-08-03 06:42:49 +00:00
'0' : 'none' ,
2019-08-03 04:19:31 +00:00
'1' : 'black' ,
'd' : 'dame' ,
}
2019-08-07 06:13:23 +00:00
const RANKS = [
'30k' , '29k' , '28k' , '27k' , '26k' , '25k' , '24k' , '23k' , '22k' , '21k' , '20k' ,
2019-08-06 21:13:51 +00:00
'19k' , '18k' , '17k' , '16k' , '15k' , '14k' , '13k' , '12k' , '11k' , '10k' ,
'9k' , '8k' , '7k' , '6k' , '5k' , '4k' , '3k' , '2k' , '1k' ,
2019-08-07 06:13:23 +00:00
'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
]
}
2019-08-06 21:13:51 +00:00
2019-08-09 05:39:12 +00:00
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' ,
]
}
2019-08-08 18:45:03 +00:00
const gameState = { // pre-init values (render prior to any player input)
2019-08-02 21:14:23 +00:00
winner : null ,
2019-08-09 05:39:12 +00:00
turn : null , // turn logic depends on handicap stones
2019-08-07 19:45:58 +00:00
pass : null , // -1 represents state in which resignation has been submitted, not confirmed
2019-08-06 18:22:43 +00:00
komi : null , // komi depends on handicap stones + player rank
2019-08-02 21:14:23 +00:00
handicap : null ,
2019-08-09 05:39:12 +00:00
boardSize : null ,
2019-08-02 21:14:23 +00:00
playerState : {
bCaptures : null ,
2019-08-07 19:45:58 +00:00
wCaptures : null ,
bScore : null ,
wScore : null
2019-08-02 21:14:23 +00:00
} ,
gameMeta : { // declared at game start and not editable after
2019-08-08 00:03:04 +00:00
date : null , // contains metadata
start : false
2019-08-02 21:14:23 +00:00
} ,
playerMeta : { // editable during game
b : {
name : null ,
2019-08-09 05:39:12 +00:00
rank : null ,
2019-08-03 04:19:31 +00:00
rankCertain : false
2019-08-02 21:14:23 +00:00
} ,
w : {
name : null ,
2019-08-09 05:39:12 +00:00
rank : null ,
2019-08-02 21:14:23 +00:00
rankCertain : false
} ,
} ,
2019-08-03 04:19:31 +00:00
groups : { } ,
2019-08-02 21:14:23 +00:00
gameRecord : [ ]
}
2019-07-26 17:17:23 +00:00
2019-08-08 18:45:03 +00:00
// 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
2019-08-07 06:13:23 +00:00
const HANDI _PLACE = {
'9' : [
0 , 0 ,
2019-08-08 19:22:33 +00:00
[ [ 7 , 3 ] , [ 3 , 7 ] ] ,
2019-08-07 06:13:23 +00:00
[ [ 7 , 7 ] , [ 7 , 3 ] , [ 3 , 7 ] ] ,
[ [ 3 , 3 ] , [ 7 , 7 ] , [ 3 , 7 ] , [ 7 , 3 ] ]
] ,
'13' : [
0 , 0 ,
2019-08-08 19:37:13 +00:00
[ [ 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 ] ] ,
2019-08-07 06:13:23 +00:00
] ,
'19' : [
0 , 0 ,
[ [ 4 , 16 ] , [ 16 , 4 ] ] ,
[ [ 16 , 16 ] , [ 4 , 16 ] , [ 16 , 4 ] ] ,
[ [ 4 , 4 ] , [ 16 , 16 ] , [ 4 , 16 ] , [ 16 , 4 ] ] ,
2019-08-08 19:37:13 +00:00
[ [ 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 ] ] ,
2019-08-07 06:13:23 +00:00
]
} ;
2019-08-03 22:01:50 +00:00
2019-08-03 06:42:49 +00:00
class Point {
constructor ( x , y ) {
this . pos = [ x , y ]
2019-08-08 18:45:03 +00:00
this . stone = 0 ; // this is where move placement will go 0, 1, -1, also contains ko: 'k'
2019-08-04 02:21:02 +00:00
this . legal ;
2019-08-06 21:13:51 +00:00
this . territory ;
2019-08-05 17:50:06 +00:00
this . capturing = [ ] ;
2019-08-07 02:11:51 +00:00
this . groupMembers = [ this ] ;
2019-08-03 06:42:49 +00:00
this . neighbors = {
top : { } ,
btm : { } ,
lft : { } ,
rgt : { }
2019-08-03 21:15:17 +00:00
}
2019-08-03 06:42:49 +00:00
this . neighbors . top = x > 1 ? [ x - 1 , y ] : null ;
this . neighbors . btm = x < gameState . boardSize ? [ x + 1 , y ] : null ;
2019-08-07 02:11:51 +00:00
this . neighbors . rgt = y < gameState . boardSize ? [ x , y + 1 ] : null ;
this . neighbors . lft = y > 1 ? [ x , y - 1 ] : null ;
2019-08-03 21:15:17 +00:00
}
checkNeighbors = ( ) => {
let neighborsArr = [ ] ;
2019-08-07 02:11:51 +00:00
for ( let neighbor in this . neighbors ) {
2019-08-03 21:15:17 +00:00
let nbr = this . neighbors [ neighbor ] ;
// neighbor exists it's point is stored as { rPos, cPos}
if ( nbr !== null ) {
2019-08-07 02:11:51 +00:00
neighborsArr . push ( boardState . find ( pt => pt . pos [ 0 ] === nbr [ 0 ] && pt . pos [ 1 ] === nbr [ 1 ] ) )
2019-08-03 21:15:17 +00:00
}
} ;
// returns array of existing neighbors to calling function
return neighborsArr ;
}
2019-08-05 17:50:06 +00:00
getLiberties = ( ) => {
2019-08-06 06:44:07 +00:00
let neighborsArr = this . checkNeighbors ( ) . filter ( pt => pt . stone === 0 ) ;
2019-08-08 18:45:03 +00:00
return neighborsArr ;
2019-08-03 21:15:17 +00:00
}
2019-08-06 06:44:07 +00:00
joinGroup = ( ) => {
2019-08-07 02:11:51 +00:00
this . groupMembers = this . groupMembers . filter ( grp => grp . stone === this . stone ) ;
2019-08-06 06:44:07 +00:00
this . groupMembers . push ( this ) ;
let frns = this . checkNeighbors ( ) . filter ( nbr => nbr . stone === this . stone ) ;
for ( let frn of frns ) {
this . groupMembers . push ( frn ) ;
}
2019-08-07 02:11:51 +00:00
this . groupMembers = Array . from ( new Set ( this . groupMembers ) ) ;
2019-08-06 06:44:07 +00:00
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 ) ) ) ;
2019-08-05 17:50:06 +00:00
}
2019-08-06 06:44:07 +00:00
}
checkCapture = ( ) => {
let opps = this . checkNeighbors ( ) . filter ( nbr => nbr . stone === gameState . turn * - 1
&& nbr . getLiberties ( ) . every ( liberty => liberty === this ) ) ;
2019-08-05 20:56:54 +00:00
for ( let opp of opps ) {
2019-08-06 06:44:07 +00:00
if ( opp . groupMembers . every ( stone => stone . getLiberties ( ) . filter ( liberty => liberty !== this ) . length === 0 ) ) {
this . capturing = this . capturing . concat ( opp . groupMembers ) ;
} ;
2019-08-05 20:56:54 +00:00
}
2019-08-06 21:13:51 +00:00
this . capturing = Array . from ( new Set ( this . capturing ) ) ;
2019-08-05 17:50:06 +00:00
return this . capturing ;
2019-08-03 23:59:56 +00:00
}
2019-08-05 17:50:06 +00:00
checkGroup = ( ) => { // liberty is true when called by move false when called by check Capture
let frns = this . checkNeighbors ( ) . filter ( nbr => nbr . stone === gameState . turn ) ;
2019-08-06 06:44:07 +00:00
for ( let frn in frns ) {
2019-08-07 02:11:51 +00:00
if ( frns [ frn ] . groupMembers . find ( stone => stone . getLiberties ( ) . find ( liberty => liberty !== this ) ) ) return true ;
continue ;
2019-08-06 06:44:07 +00:00
}
}
2019-08-09 05:39:12 +00:00
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 ;
}
} ) ;
2019-08-07 06:13:23 +00:00
}
2019-08-09 05:39:12 +00:00
}
2019-08-03 21:15:17 +00:00
}
2019-08-09 05:39:12 +00:00
/*----- app's state (variables) -----*/
let boardState = [ ] ;
2019-08-03 22:01:50 +00:00
/*----- cached element references -----*/
2019-08-07 01:13:34 +00:00
const whiteCapsEl = document . getElementById ( 'white-caps' ) ;
const blackCapsEl = document . getElementById ( 'black-caps' ) ;
2019-08-05 23:13:23 +00:00
const modalEl = document . querySelector ( '.modal' ) ;
2019-08-06 18:22:43 +00:00
const komiSliderEl = document . querySelector ( 'input[name="komi-slider"]' ) ;
const handiSliderEl = document . querySelector ( 'input[name="handicap-slider"]' ) ;
2019-08-07 01:13:34 +00:00
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' ) ;
2019-08-07 23:26:24 +00:00
const dateEl = document . getElementById ( 'date' ) ;
2019-08-08 18:45:03 +00:00
const boardSizeEl = document . getElementById ( 'board-size-radio' ) ;
const komiDisplayEl = document . getElementById ( 'komi' ) ;
const handiDisplayEl = document . getElementById ( 'handicap' ) ;
const boardEl = document . querySelector ( '#board tbody' ) ;
2019-08-08 22:49:51 +00:00
const gameStartEl = document . querySelector ( 'input[name="game-start"]' ) ;
const komiSuggestEl = document . querySelector ( 'input[name="komi-suggest"]' ) ;
2019-08-09 05:39:12 +00:00
const soundPlayerEl = new Audio ( ) ;
2019-08-08 06:52:13 +00:00
const boardSizeRadioEls = [
document . querySelectorAll ( 'input[name="board-size"' ) [ 0 ] ,
document . querySelectorAll ( 'input[name="board-size"' ) [ 1 ] ,
document . querySelectorAll ( 'input[name="board-size"' ) [ 2 ]
] ;
2019-08-03 22:01:50 +00:00
/*----- event listeners -----*/
2019-08-03 23:59:56 +00:00
document . getElementById ( 'board' ) . addEventListener ( 'mousemove' , hoverPreview ) ;
2019-08-06 21:13:51 +00:00
document . getElementById ( 'board' ) . addEventListener ( 'click' , clickBoard ) ;
2019-08-05 23:13:23 +00:00
document . getElementById ( 'white-bowl' ) . addEventListener ( 'click' , clickPass ) ;
document . getElementById ( 'black-bowl' ) . addEventListener ( 'click' , clickPass ) ;
2019-08-08 00:03:04 +00:00
document . getElementById ( 'kifu' ) . addEventListener ( 'click' , clickMenuOpen ) ;
2019-08-05 23:13:23 +00:00
document . getElementById ( 'white-caps-space' ) . addEventListener ( 'click' , clickResign ) ;
document . getElementById ( 'black-caps-space' ) . addEventListener ( 'click' , clickResign ) ;
modalEl . addEventListener ( 'click' , clickCloseMenu ) ;
2019-08-06 18:22:43 +00:00
komiSliderEl . addEventListener ( 'change' , changeUpdateKomi ) ;
handiSliderEl . addEventListener ( 'change' , changeUpdateHandicap ) ;
2019-08-06 22:25:58 +00:00
document . getElementById ( 'player-meta' ) . addEventListener ( 'click' , clickUpdatePlayerMeta ) ;
document . getElementById ( 'player-meta' ) . addEventListener ( 'change' , clickUpdatePlayerMeta ) ;
2019-08-08 22:49:51 +00:00
komiSuggestEl . addEventListener ( 'click' , clickKomiSuggestion ) ;
2019-08-07 19:45:58 +00:00
gameHudEl . addEventListener ( 'click' , clickGameHud ) ;
2019-08-07 23:26:24 +00:00
boardSizeEl . addEventListener ( 'click' , clickBoardSize ) ;
2019-08-08 19:37:13 +00:00
gameStartEl . addEventListener ( 'click' , clickSubmitStart ) ;
2019-08-05 23:13:23 +00:00
2019-08-09 05:39:12 +00:00
/*----- FUNCTIONS ----------------------------------*/
/*----- init functions -----*/
2019-08-03 06:42:49 +00:00
init ( ) ;
2019-08-09 05:39:12 +00:00
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
2019-08-08 19:22:33 +00:00
function findPointFromIdx ( arr ) {
return pointFromIdx = boardState . find ( point => point . pos [ 0 ] === arr [ 0 ] && point . pos [ 1 ] === arr [ 1 ] ) ;
}
2019-08-03 04:19:31 +00:00
2019-08-08 00:46:56 +00:00
function changeUpdateKomi ( evt ) {
2019-08-08 00:03:04 +00:00
evt . stopPropagation ( ) ;
2019-08-07 23:26:24 +00:00
komiDisplayEl . textContent = komiSliderEl . value ;
gameState . komi = komiSliderEl . value ;
renderMenu ( ) ;
2019-08-06 18:22:43 +00:00
}
2019-08-08 00:46:56 +00:00
function changeUpdateHandicap ( evt ) {
2019-08-08 00:03:04 +00:00
evt . stopPropagation ( ) ;
2019-08-07 23:26:24 +00:00
handiDisplayEl . textContent = handiSliderEl . value !== 1 ? handiSliderEl . value : 0 ;
gameState . handicap = handiSliderEl . value !== 1 ? handiSliderEl . value : 0 ;
renderMenu ( ) ;
2019-08-06 18:22:43 +00:00
}
2019-08-06 22:25:58 +00:00
function clickUpdatePlayerMeta ( evt ) {
2019-08-08 00:03:04 +00:00
evt . stopPropagation ( ) ;
2019-08-07 06:13:23 +00:00
if ( evt . target . id ) {
switch ( evt . target . id ) {
case 'black-rank-up' :
2019-08-09 05:39:12 +00:00
if ( gameState . playerMeta . b . rank < RANKS . length - 1 ) gameState . playerMeta . b . rank ++ ;
2019-08-07 06:13:23 +00:00
break ;
case 'black-rank-down' :
2019-08-09 05:39:12 +00:00
if ( gameState . playerMeta . b . rank > 0 ) gameState . playerMeta . b . rank -- ;
2019-08-06 22:25:58 +00:00
break ;
2019-08-07 06:13:23 +00:00
case 'white-rank-up' :
2019-08-09 05:39:12 +00:00
if ( gameState . playerMeta . w . rank < RANKS . length - 1 ) gameState . playerMeta . w . rank ++ ;
2019-08-07 06:13:23 +00:00
break ;
case 'white-rank-down' :
2019-08-09 05:39:12 +00:00
if ( gameState . playerMeta . w . rank > 0 ) gameState . playerMeta . w . rank -- ;
2019-08-07 06:13:23 +00:00
break ;
}
2019-08-06 22:25:58 +00:00
}
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 ;
2019-08-07 23:26:24 +00:00
renderMenu ( ) ;
2019-08-06 23:36:39 +00:00
}
2019-08-08 00:03:04 +00:00
function clickBoardSize ( evt ) {
evt . stopPropagation ( ) ;
2019-08-08 06:52:13 +00:00
gameState . boardSize = boardSizeRadioEls . find ( el => el . checked === true ) . value ;
2019-08-07 23:26:24 +00:00
renderMenu ( ) ;
}
function clickKomiSuggestion ( evt ) {
2019-08-08 00:03:04 +00:00
evt . preventDefault ( ) ;
2019-08-07 23:26:24 +00:00
evt . stopPropagation ( ) ;
2019-08-08 22:49:51 +00:00
if ( gameState . gameMeta . start ) {
gameState . playerMeta . b . name = blackNameInputEl . value || 'black' ;
gameState . playerMeta . w . name = whiteNameInputEl . value || 'white' ;
modalEl . style . visibility = 'hidden' ;
return
}
2019-08-07 23:26:24 +00:00
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 ) ] ;
2019-08-07 06:13:23 +00:00
gameState . komi = sugg ;
gameState . handicap = handi ;
renderMenu ( ) ;
2019-08-06 22:25:58 +00:00
}
2019-08-09 05:39:12 +00:00
function clickCloseMenu ( evt ) {
2019-08-08 00:03:04 +00:00
evt . stopPropagation ( ) ;
2019-08-09 05:39:12 +00:00
if ( evt . target . className === "modal" && gameState . gameMeta . start ) modalEl . style . visibility = 'hidden' ;
2019-08-06 23:36:39 +00:00
}
2019-08-07 06:13:23 +00:00
2019-08-09 05:39:12 +00:00
/*----- gameplay functions -----*/
2019-08-05 23:13:23 +00:00
2019-08-09 05:39:12 +00:00
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 ;
2019-08-05 23:13:23 +00:00
clearKo ( ) ;
2019-08-09 05:39:12 +00:00
clearPass ( ) ;
resolveCaptures ( point ) ;
point . stone = gameState . turn ;
point . joinGroup ( ) ;
playSound ( point ) ;
2019-08-05 23:13:23 +00:00
clearCaptures ( ) ;
2019-08-09 05:39:12 +00:00
gameState . gameRecord . push ( ` ${ STONES _DATA [ gameState . turn ] } : ${ point . pos } ` )
2019-08-05 23:13:23 +00:00
gameState . turn *= - 1 ;
2019-08-07 23:26:24 +00:00
renderGame ( ) ;
2019-08-05 23:13:23 +00:00
}
2019-08-09 05:39:12 +00:00
function clearKo ( ) {
for ( let point in boardState ) {
point = boardState [ point ] ;
point . stone = point . stone === 'k' ? 0 : point . stone ;
}
2019-08-07 19:45:58 +00:00
}
2019-08-06 23:36:39 +00:00
2019-08-09 05:39:12 +00:00
function clearPass ( ) {
gameState . pass = 0 ;
2019-08-05 23:13:23 +00:00
}
2019-08-09 05:39:12 +00:00
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 ;
} )
2019-08-03 23:59:56 +00:00
}
}
function checkLegal ( point ) {
2019-08-04 02:21:02 +00:00
clearOverlay ( ) ;
2019-08-03 21:15:17 +00:00
// first step in logic: is point occupied, or in ko
2019-08-03 23:59:56 +00:00
if ( point . stone ) return false ;
2019-08-05 17:50:06 +00:00
// if point is not empty check if liberties
2019-08-06 06:44:07 +00:00
if ( point . getLiberties ( ) . length < 1 ) {
2019-08-05 17:50:06 +00:00
//if no liberties check if enemy group has liberties
2019-08-06 06:44:07 +00:00
if ( point . checkCapture ( ) . length ) return true ;
2019-08-03 23:59:56 +00:00
//if neighboring point is not empty check if friendly group is alive
2019-08-06 06:44:07 +00:00
if ( point . checkGroup ( ) ) return true ;
2019-08-04 02:21:02 +00:00
return false ;
2019-08-03 21:15:17 +00:00
}
2019-08-03 23:59:56 +00:00
return true ;
2019-08-03 04:19:31 +00:00
}
2019-08-08 18:45:03 +00:00
function clearOverlay ( ) {
2019-08-04 02:21:02 +00:00
for ( let point in boardState ) {
2019-08-04 04:17:23 +00:00
point = boardState [ point ] ;
point . legal = false ;
2019-08-04 02:21:02 +00:00
}
}
2019-08-06 07:00:00 +00:00
function checkKo ( point , cap ) {
2019-08-06 21:13:51 +00:00
if ( ! point . getLiberties ( ) . length && cap . checkNeighbors ( ) . filter ( stone => stone . stone === gameState . turn * - 1 )
&& point . capturing . length === 1 ) return true ;
2019-08-04 04:17:23 +00:00
}
2019-08-09 05:39:12 +00:00
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 ( ) ;
}
2019-08-03 04:19:31 +00:00
}
2019-08-04 04:17:23 +00:00
2019-08-09 05:39:12 +00:00
function clearCaptures ( ) {
2019-08-04 04:17:23 +00:00
for ( let point in boardState ) {
point = boardState [ point ] ;
2019-08-09 05:39:12 +00:00
point . capturing = [ ] ;
2019-08-04 04:17:23 +00:00
}
}
2019-08-09 05:39:12 +00:00
function clickPass ( evt ) {
if ( evt . target . parentElement . id === ` ${ STONES _DATA [ gameState . turn ] } -bowl ` ) playerPass ( ) ;
2019-08-05 23:13:23 +00:00
}
2019-08-09 05:39:12 +00:00
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 ( ) ;
2019-08-05 17:50:06 +00:00
}
2019-08-09 05:39:12 +00:00
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 ) ;
2019-08-07 06:13:23 +00:00
}
}
2019-08-09 05:39:12 +00:00
/*----- 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 ] ;
2019-08-07 06:13:23 +00:00
}
2019-08-09 05:39:12 +00:00
function renderKomiSlider ( ) {
komiSliderEl . value = gameState . komi ;
komiDisplayEl . textContent = gameState . komi ;
if ( gameState . gameMeta . start ) komiSliderEl . setAttribute ( 'disabled' , true ) ;
2019-08-06 22:25:58 +00:00
}
2019-08-07 06:13:23 +00:00
2019-08-09 05:39:12 +00:00
function renderHandiSlider ( ) {
handiSliderEl . value = gameState . handicap ;
handiDisplayEl . textContent = gameState . handicap ;
if ( gameState . gameMeta . start ) handiSliderEl . setAttribute ( 'disabled' , true ) ;
2019-08-07 23:26:24 +00:00
}
2019-08-09 05:39:12 +00:00
function renderBoardSizeRadio ( ) {
boardSizeEl . value = gameState . boardSize ;
if ( gameState . gameMeta . start ) boardSizeRadioEls . forEach ( el => el . setAttribute ( 'disabled' , true ) ) ;
2019-08-05 18:37:03 +00:00
}
2019-08-09 05:39:12 +00:00
/*----- game render -----*/
2019-08-08 06:52:13 +00:00
function renderBoardInit ( ) {
2019-08-08 19:22:33 +00:00
renderClearBoard ( ) ;
2019-08-08 06:52:13 +00:00
renderBoardTableRows ( ) ;
2019-08-08 17:09:59 +00:00
renderHoshi ( ) ;
2019-08-08 06:52:13 +00:00
renderBoardTableStyle ( ) ;
}
2019-08-08 19:22:33 +00:00
function renderClearBoard ( ) {
2019-08-08 06:52:13 +00:00
boardEl . innerHTML = '' ;
2019-08-08 18:45:03 +00:00
boardEl . classList = '' ;
2019-08-08 06:52:13 +00:00
}
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 ++
}
2019-08-08 18:45:03 +00:00
boardEl . classList = ` board- ${ gameState . boardSize } x ${ gameState . boardSize } ` ;
2019-08-08 06:52:13 +00:00
}
2019-08-09 05:39:12 +00:00
// iterator ^ becomes x index ̌
2019-08-08 06:52:13 +00:00
function renderBoardTableCells ( x ) {
let y = 1
let cells = '' ;
while ( y <= gameState . boardSize ) {
let newCell = `
< td id = "${x}-${y}" >
< div class = "stone" >
2019-08-08 18:45:03 +00:00
< div class = "dot" > < / d i v >
2019-08-08 06:52:13 +00:00
< / d i v >
< / t d >
` ;
cells = cells + newCell ;
y ++ ;
}
return cells ;
}
2019-08-09 05:39:12 +00:00
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 ] ) ;
} )
}
2019-08-06 23:36:39 +00:00
function renderMessage ( ) {
2019-08-07 06:13:23 +00:00
if ( gameState . winner && gameState . pass < 2 ) {
2019-08-07 19:45:58 +00:00
gameHudEl . style . visibility = 'visible' ;
gameHudEl . style . cursor = 'default' ;
2019-08-07 06:13:23 +00:00
gameHudEl . textContent = ` ${ gameState . playerMeta [ gameState . winner === 1 ? 'b' : 'w' ] . name } won by resignation ` ;
}
else if ( gameState . winner && gameState . pass > 1 ) {
2019-08-07 19:45:58 +00:00
gameHudEl . style . visibility = 'visible' ;
gameHudEl . style . cursor = 'default' ;
2019-08-07 23:26:24 +00:00
gameHudEl . textContent = ` ${ gameState . playerMeta [ gameState . winner === 1 ? 'b' : 'w' ] . name || STONES _DATA [ gameState . winner ] } won by ${ Math . abs ( gameState . playerState . wScore - gameState . playerState . bScore ) } ` ;
2019-08-07 19:45:58 +00:00
} else if ( gameState . pass > 1 ) {
gameHudEl . style . visibility = 'visible' ;
2019-08-08 22:56:51 +00:00
gameHudEl . textContent = 'finalize game'
2019-08-07 19:45:58 +00:00
} else {
gameHudEl . style . visibility = 'hidden' ;
2019-08-07 06:13:23 +00:00
}
2019-08-06 23:36:39 +00:00
}
2019-08-06 21:13:51 +00:00
function renderTerritory ( ) {
boardState . forEach ( val => {
2019-08-08 06:52:13 +00:00
let stoneElem = document . getElementById ( ` ${ val . pos [ 0 ] } - ${ val . pos [ 1 ] } ` ) . getElementsByClassName ( 'dot' ) [ 0 ] ;
2019-08-06 21:13:51 +00:00
stoneElem . setAttribute ( "data-dot" , DOTS _DATA [ val . territory ] ) ;
} )
}
2019-08-09 05:39:12 +00:00
/*----- endgame functions -----*/
function clickResign ( evt ) {
if ( evt . target . parentElement . id === ` ${ STONES _DATA [ gameState . turn ] } -caps-space ` ) playerResign ( ) ;
2019-08-05 18:37:03 +00:00
}
2019-08-06 21:13:51 +00:00
2019-08-09 05:39:12 +00:00
function playerResign ( ) {
// display confirmation message
gameState . pass = - 1 ;
gameHudEl . style . visibility = "visible" ;
gameHudEl . textContent = "Do you want to resign?" ;
2019-08-03 06:42:49 +00:00
}
2019-08-09 05:39:12 +00:00
function clickGameHud ( ) {
if ( gameState . pass > 1 && ! gameState . winner ) calculateWinner ( ) ;
if ( gameState . pass < 0 ) confirmResign ( ) ;
2019-08-03 06:42:49 +00:00
}
2019-08-09 05:39:12 +00:00
function confirmResign ( ) {
gameState . gameRecord . push ( ` ${ STONES _DATA [ gameState . turn ] } : resign ` ) ;
gameState . winner = STONES _DATA [ gameState . turn * - 1 ] ;
endGame ( ) ;
2019-08-04 04:38:31 +00:00
}
2019-08-09 05:39:12 +00:00
function endGame ( ) {
if ( ! gameState . winner ) endGameSetTerritory ( )
renderGame ( ) ;
}
2019-08-03 06:42:49 +00:00
2019-08-09 05:39:12 +00:00
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 ( ) ;
2019-08-05 23:13:23 +00:00
}
2019-08-07 06:13:23 +00:00
function calculateWinner ( ) {
2019-08-07 19:45:58 +00:00
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 ) } ` )
2019-08-07 23:26:24 +00:00
renderGame ( ) ;
2019-08-07 06:13:23 +00:00
}
2019-08-06 21:13:51 +00:00
function endGameSetTerritory ( ) {
2019-08-07 06:13:23 +00:00
let emptyPoints = boardState . filter ( pt => ! pt . stone ) ;
emptyPoints . forEach ( pt => pt . joinGroup ( ) ) ;
emptyPointSetTerritory ( emptyPoints ) ;
2019-08-07 22:08:55 +00:00
groupsMarkDeadLive ( ) ;
}
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 ;
} ) ;
2019-08-06 21:13:51 +00:00
}
2019-08-07 06:13:23 +00:00
function emptyPointSetTerritory ( emptyPoints ) {
2019-08-07 19:45:58 +00:00
emptyPoints . filter ( pt => ! pt . territory && pt . checkNeighbors ( ) . filter ( nbr => nbr . stone !== 0 ) )
. forEach ( pt => {
2019-08-07 22:08:55 +00:00
let b = pt . groupMembers . reduce ( ( acc , grpMem ) => {
let bNbr = grpMem . checkNeighbors ( ) . filter ( nbr => nbr . stone === 1 ) . length ;
2019-08-07 19:45:58 +00:00
return acc + bNbr ;
} , 0 ) ;
2019-08-07 22:08:55 +00:00
let w = pt . groupMembers . reduce ( ( acc , grpMem ) => {
let wNbr = grpMem . checkNeighbors ( ) . filter ( nbr => nbr . stone === - 1 ) . length ;
2019-08-07 19:45:58 +00:00
return acc + wNbr ;
} , 0 ) ;
pt . groupMembers . forEach ( grp => {
2019-08-07 23:26:24 +00:00
if ( Math . abs ( b - w ) < 4 && b && w ) grp . territory = 'd'
2019-08-07 19:45:58 +00:00
else grp . territory = b > w ? 1 : - 1 ;
} )
} ) ;
2019-08-07 06:13:23 +00:00
}