You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
189 lines
5.4 KiB
189 lines
5.4 KiB
const size = 9;
|
|
const neighbors = (i) =>
|
|
[
|
|
i % size ? i - 1 : -1,
|
|
i % size < size - 1 ? i + 1 : -1,
|
|
i >= size ? i - size : -1,
|
|
i < size * (size - 1) ? i + size : -1,
|
|
].filter((v) => v >= 0);
|
|
function group(board, start) {
|
|
const stones = new Set([start]),
|
|
liberties = new Set(),
|
|
queue = [start],
|
|
color = board[start];
|
|
for (const i of queue)
|
|
for (const n of neighbors(i)) {
|
|
if (board[n] === 0) liberties.add(n);
|
|
else if (board[n] === color && !stones.has(n)) {
|
|
stones.add(n);
|
|
queue.push(n);
|
|
}
|
|
}
|
|
return { stones: [...stones], liberties };
|
|
}
|
|
function place(state, to, seat) {
|
|
if (!Number.isInteger(to) || to < 0 || to >= size * size || state.board[to])
|
|
throw new Error('Intersección ocupada o no válida.');
|
|
const board = [...state.board],
|
|
color = seat + 1;
|
|
board[to] = color;
|
|
let captured = 0;
|
|
for (const n of neighbors(to))
|
|
if (board[n] === 3 - color) {
|
|
const enemy = group(board, n);
|
|
if (!enemy.liberties.size) {
|
|
for (const i of enemy.stones) board[i] = 0;
|
|
captured += enemy.stones.length;
|
|
}
|
|
}
|
|
if (!group(board, to).liberties.size)
|
|
throw new Error('No puedes hacer una jugada suicida.');
|
|
if (state.history.includes(board.join('')))
|
|
throw new Error('Superko: no puedes repetir una posición anterior.');
|
|
return { board, captured };
|
|
}
|
|
function score(state) {
|
|
const board = state.board.map((v, i) => (state.dead.includes(i) ? 0 : v)),
|
|
seen = new Set(),
|
|
scores = [0, 6.5];
|
|
for (let i = 0; i < board.length; i++) {
|
|
if (board[i]) {
|
|
scores[board[i] - 1]++;
|
|
continue;
|
|
}
|
|
if (seen.has(i)) continue;
|
|
const empty = [i],
|
|
borders = new Set();
|
|
seen.add(i);
|
|
for (const p of empty)
|
|
for (const n of neighbors(p))
|
|
if (board[n]) borders.add(board[n]);
|
|
else if (!seen.has(n)) {
|
|
seen.add(n);
|
|
empty.push(n);
|
|
}
|
|
if (borders.size === 1) scores[[...borders][0] - 1] += empty.length;
|
|
}
|
|
return scores;
|
|
}
|
|
export const go = {
|
|
id: 'go',
|
|
version: '1.0.0',
|
|
minPlayers: 2,
|
|
maxPlayers: 2,
|
|
create() {
|
|
const board = Array(size * size).fill(0);
|
|
return {
|
|
board,
|
|
turn: 0,
|
|
phase: 'play',
|
|
passes: 0,
|
|
history: [board.join('')],
|
|
captures: [0, 0],
|
|
dead: [],
|
|
accepted: [false, false],
|
|
scores: null,
|
|
};
|
|
},
|
|
legal(state, seat) {
|
|
if (state.phase === 'scoring')
|
|
return [
|
|
...state.board.flatMap((v, to) => (v ? [{ type: 'mark', to }] : [])),
|
|
{ type: 'accept-score' },
|
|
{ type: 'resume' },
|
|
];
|
|
if (state.phase !== 'play' || state.turn !== seat) return [];
|
|
return [
|
|
...state.board.flatMap((v, to) => {
|
|
if (v) return [];
|
|
try {
|
|
place(state, to, seat);
|
|
return [{ type: 'move', to }];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}),
|
|
{ type: 'pass' },
|
|
];
|
|
},
|
|
apply(state, action, seat) {
|
|
if (
|
|
!this.legal(state, seat).some(
|
|
(a) =>
|
|
a.type === action.type && (a.to === undefined || a.to === action.to),
|
|
)
|
|
)
|
|
throw new Error('Esa acción no está disponible.');
|
|
const next = structuredClone(state);
|
|
let label = '',
|
|
sound = 'move',
|
|
result = null;
|
|
if (action.type === 'move') {
|
|
const move = place(state, action.to, seat);
|
|
next.board = move.board;
|
|
next.history.push(next.board.join(''));
|
|
next.captures[seat] += move.captured;
|
|
next.turn = 1 - seat;
|
|
next.passes = 0;
|
|
label = `Piedra en ${'ABCDEFGHJ'[action.to % size]}${size - Math.floor(action.to / size)}${move.captured ? ` · captura ${move.captured}` : ''}`;
|
|
sound = move.captured ? 'capture' : 'move';
|
|
}
|
|
if (action.type === 'pass') {
|
|
next.turn = 1 - seat;
|
|
next.passes++;
|
|
if (next.passes === 2) next.phase = 'scoring';
|
|
label =
|
|
next.phase === 'scoring'
|
|
? 'Segundo pase: acordad las piedras muertas y el resultado'
|
|
: 'Pasa';
|
|
sound = 'pass';
|
|
}
|
|
if (action.type === 'mark') {
|
|
const stones = group(next.board, action.to).stones,
|
|
marked = next.dead.includes(action.to);
|
|
next.dead = marked
|
|
? next.dead.filter((i) => !stones.includes(i))
|
|
: [...new Set([...next.dead, ...stones])];
|
|
next.accepted = [false, false];
|
|
label = marked ? 'Restaura un grupo vivo' : 'Marca un grupo muerto';
|
|
sound = 'move';
|
|
}
|
|
if (action.type === 'resume') {
|
|
next.phase = 'play';
|
|
next.passes = 0;
|
|
next.dead = [];
|
|
next.accepted = [false, false];
|
|
next.turn = seat;
|
|
label = 'Reanuda el juego';
|
|
sound = 'pass';
|
|
}
|
|
if (action.type === 'accept-score') {
|
|
next.accepted[seat] = true;
|
|
label = 'Acepta la propuesta de puntuación';
|
|
sound = 'pass';
|
|
if (next.accepted.every(Boolean)) {
|
|
next.scores = score(next);
|
|
next.phase = 'finished';
|
|
result = {
|
|
winner: next.scores[0] > next.scores[1] ? 0 : 1,
|
|
reason: `área: negras ${next.scores[0]}, blancas ${next.scores[1]} (komi 6,5)`,
|
|
};
|
|
}
|
|
}
|
|
return { state: next, label, sound, result };
|
|
},
|
|
view(state) {
|
|
return {
|
|
turn: state.turn,
|
|
grid: state.board,
|
|
size,
|
|
phase: state.phase,
|
|
captures: state.captures,
|
|
dead: state.dead,
|
|
accepted: state.accepted,
|
|
scores: state.scores ?? score(state),
|
|
komi: 6.5,
|
|
};
|
|
},
|
|
};
|