Files
FrozenFriendsYume/packages/backend/built/services/reversi/engine.js
T
2026-07-26 18:25:37 +09:00

184 lines
5.2 KiB
JavaScript

export const standardMap = [
"--------",
"--------",
"--------",
"---wb---",
"---bw---",
"--------",
"--------",
"--------"
];
export class ReversiEngine {
map;
mapWidth;
mapHeight;
board;
turn = true;
prevColor = null;
opts;
constructor(map, opts){
this.opts = {
isLlotheo: opts.isLlotheo ?? false,
canPutEverywhere: opts.canPutEverywhere ?? false,
loopedBoard: opts.loopedBoard ?? false
};
this.mapWidth = map[0]?.length ?? 0;
this.mapHeight = map.length;
const data = map.join("");
this.board = data.split("").map((d)=>d === "-" ? null : d === "b" ? true : d === "w" ? false : undefined);
this.map = data.split("").map((d)=>d === "-" || d === "b" || d === "w" ? "empty" : "null");
if (!this.canPutSomewhere(true)) this.turn = this.canPutSomewhere(false) ? false : null;
}
get blackCount() {
return this.board.filter((x)=>x === true).length;
}
get whiteCount() {
return this.board.filter((x)=>x === false).length;
}
posToXy(pos) {
return [
pos % this.mapWidth,
Math.floor(pos / this.mapWidth)
];
}
xyToPos(x, y) {
return x + y * this.mapWidth;
}
mapDataGet(pos) {
const [x, y] = this.posToXy(pos);
return x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight ? "null" : this.map[pos];
}
getPuttablePlaces(color) {
return Array.from(this.board.keys()).filter((i)=>this.canPut(color, i));
}
canPutSomewhere(color) {
return this.getPuttablePlaces(color).length > 0;
}
canPut(color, pos) {
if (this.board[pos] !== null) return false;
if (this.opts.canPutEverywhere) return this.mapDataGet(pos) === "empty";
return this.effects(color, pos).length !== 0;
}
effects(color, initPos) {
const enemyColor = !color;
const diffVectors = [
[
0,
-1
],
[
1,
-1
],
[
1,
0
],
[
1,
1
],
[
0,
1
],
[
-1,
1
],
[
-1,
0
],
[
-1,
-1
]
];
return diffVectors.flatMap(([dx, dy])=>{
const found = [];
let [x, y] = this.posToXy(initPos);
while(true){
x += dx;
y += dy;
if (this.opts.loopedBoard && this.xyToPos(x = (x % this.mapWidth + this.mapWidth) % this.mapWidth, y = (y % this.mapHeight + this.mapHeight) % this.mapHeight) === initPos) {
return found;
}
if (x === -1 || y === -1 || x === this.mapWidth || y === this.mapHeight) return [];
const pos = this.xyToPos(x, y);
if (this.mapDataGet(pos) === "null") return [];
const stone = this.board[pos];
if (stone === null) return [];
if (stone === enemyColor) found.push(pos);
if (stone === color) return found;
}
});
}
putStone(pos) {
const color = this.turn;
if (color == null) return;
this.board[pos] = color;
for (const effect of this.effects(color, pos)){
this.board[effect] = color;
}
this.prevColor = color;
this.turn = this.canPutSomewhere(!color) ? !color : this.canPutSomewhere(color) ? color : null;
}
get isEnded() {
return this.turn == null;
}
get winner() {
if (!this.isEnded) return null;
if (this.blackCount === this.whiteCount) return null;
return this.opts.isLlotheo === this.blackCount > this.whiteCount ? false : true;
}
calcCrc32() {
let hash = 0;
const text = JSON.stringify({
board: this.board,
turn: this.turn
});
for(let i = 0; i < text.length; i++){
hash = hash * 31 + text.charCodeAt(i) | 0;
}
return hash.toString();
}
}
export function serializeLogs(logs) {
const serialized = [];
for(let i = 0; i < logs.length; i++){
const log = logs[i];
const timeDelta = i === 0 ? log.time : log.time - logs[i - 1].time;
serialized.push([
timeDelta,
log.player ? 1 : 0,
0,
log.pos
]);
}
return serialized;
}
export function deserializeLogs(logs) {
const deserialized = [];
let time = 0;
for (const log of logs){
time += log[0];
if (log[2] === 0) {
deserialized.push({
time,
player: log[1] === 1,
operation: "put",
pos: log[3]
});
}
}
return deserialized;
}
export function restoreGame(env) {
const game = new ReversiEngine(env.map, env);
for (const log of deserializeLogs(env.logs)){
if (log.operation === "put") game.putStone(log.pos);
}
return game;
}