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

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,
};
},
};

Powered by TurnKey Linux.