enhance(reversi): tweak reversi
This commit is contained in:
parent
f86d0186d2
commit
b9a81edae5
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
|
import * as Reversi from 'misskey-reversi';
|
||||||
import type { MiChannel } from '@/models/Channel.js';
|
import type { MiChannel } from '@/models/Channel.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import type { MiUserProfile } from '@/models/UserProfile.js';
|
import type { MiUserProfile } from '@/models/UserProfile.js';
|
||||||
@ -179,12 +180,7 @@ export interface ReversiGameEventTypes {
|
|||||||
key: string;
|
key: string;
|
||||||
value: any;
|
value: any;
|
||||||
};
|
};
|
||||||
putStone: {
|
log: Reversi.Serializer.Log & { id: string | null };
|
||||||
at: number;
|
|
||||||
color: boolean;
|
|
||||||
pos: number;
|
|
||||||
next: boolean;
|
|
||||||
};
|
|
||||||
syncState: {
|
syncState: {
|
||||||
crc32: string;
|
crc32: string;
|
||||||
};
|
};
|
||||||
|
@ -235,11 +235,14 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||||||
|
|
||||||
const map = freshGame.map != null ? freshGame.map : getRandomMap();
|
const map = freshGame.map != null ? freshGame.map : getRandomMap();
|
||||||
|
|
||||||
|
const crc32 = CRC32.str(JSON.stringify(freshGame.logs)).toString();
|
||||||
|
|
||||||
await this.reversiGamesRepository.update(game.id, {
|
await this.reversiGamesRepository.update(game.id, {
|
||||||
startedAt: new Date(),
|
startedAt: new Date(),
|
||||||
isStarted: true,
|
isStarted: true,
|
||||||
black: bw,
|
black: bw,
|
||||||
map: map,
|
map: map,
|
||||||
|
crc32,
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
|
//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
|
||||||
@ -309,7 +312,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async putStoneToGame(game: MiReversiGame, user: MiUser, pos: number) {
|
public async putStoneToGame(game: MiReversiGame, user: MiUser, pos: number, id?: string | null) {
|
||||||
if (!game.isStarted) return;
|
if (!game.isStarted) return;
|
||||||
if (game.isEnded) return;
|
if (game.isEnded) return;
|
||||||
if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return;
|
if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return;
|
||||||
@ -319,56 +322,58 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||||||
? true
|
? true
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
const o = new Reversi.Game(game.map, {
|
const engine = Reversi.Serializer.restoreGame({
|
||||||
|
map: game.map,
|
||||||
isLlotheo: game.isLlotheo,
|
isLlotheo: game.isLlotheo,
|
||||||
canPutEverywhere: game.canPutEverywhere,
|
canPutEverywhere: game.canPutEverywhere,
|
||||||
loopedBoard: game.loopedBoard,
|
loopedBoard: game.loopedBoard,
|
||||||
|
logs: game.logs,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 盤面の状態を再生
|
if (engine.turn !== myColor) return;
|
||||||
for (const log of game.logs) {
|
if (!engine.canPut(myColor, pos)) return;
|
||||||
o.put(log.color, log.pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (o.turn !== myColor) return;
|
engine.putStone(pos);
|
||||||
|
|
||||||
if (!o.canPut(myColor, pos)) return;
|
|
||||||
o.put(myColor, pos);
|
|
||||||
|
|
||||||
let winner;
|
let winner;
|
||||||
if (o.isEnded) {
|
if (engine.isEnded) {
|
||||||
if (o.winner === true) {
|
if (engine.winner === true) {
|
||||||
winner = game.black === 1 ? game.user1Id : game.user2Id;
|
winner = game.black === 1 ? game.user1Id : game.user2Id;
|
||||||
} else if (o.winner === false) {
|
} else if (engine.winner === false) {
|
||||||
winner = game.black === 1 ? game.user2Id : game.user1Id;
|
winner = game.black === 1 ? game.user2Id : game.user1Id;
|
||||||
} else {
|
} else {
|
||||||
winner = null;
|
winner = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logs = Reversi.Serializer.deserializeLogs(game.logs);
|
||||||
|
|
||||||
const log = {
|
const log = {
|
||||||
at: Date.now(),
|
time: Date.now(),
|
||||||
color: myColor,
|
player: myColor,
|
||||||
|
operation: 'put',
|
||||||
pos,
|
pos,
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()).toString();
|
logs.push(log);
|
||||||
|
|
||||||
game.logs.push(log);
|
const serializeLogs = Reversi.Serializer.serializeLogs(logs);
|
||||||
|
|
||||||
|
const crc32 = CRC32.str(JSON.stringify(serializeLogs)).toString();
|
||||||
|
|
||||||
await this.reversiGamesRepository.update(game.id, {
|
await this.reversiGamesRepository.update(game.id, {
|
||||||
crc32,
|
crc32,
|
||||||
isEnded: o.isEnded,
|
isEnded: engine.isEnded,
|
||||||
winnerId: winner,
|
winnerId: winner,
|
||||||
logs: game.logs,
|
logs: serializeLogs,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.globalEventService.publishReversiGameStream(game.id, 'putStone', {
|
this.globalEventService.publishReversiGameStream(game.id, 'log', {
|
||||||
...log,
|
...log,
|
||||||
next: o.turn,
|
id: id ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (o.isEnded) {
|
if (engine.isEnded) {
|
||||||
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
||||||
winnerId: winner ?? null,
|
winnerId: winner ?? null,
|
||||||
game: await this.reversiGameEntityService.packDetail(game.id, user),
|
game: await this.reversiGameEntityService.packDetail(game.id, user),
|
||||||
|
@ -55,11 +55,7 @@ export class ReversiGameEntityService {
|
|||||||
isLlotheo: game.isLlotheo,
|
isLlotheo: game.isLlotheo,
|
||||||
canPutEverywhere: game.canPutEverywhere,
|
canPutEverywhere: game.canPutEverywhere,
|
||||||
loopedBoard: game.loopedBoard,
|
loopedBoard: game.loopedBoard,
|
||||||
logs: game.logs.map(log => ({
|
logs: game.logs,
|
||||||
at: log.at,
|
|
||||||
color: log.color,
|
|
||||||
pos: log.pos,
|
|
||||||
})),
|
|
||||||
map: game.map,
|
map: game.map,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -76,11 +76,7 @@ export class MiReversiGame {
|
|||||||
@Column('jsonb', {
|
@Column('jsonb', {
|
||||||
default: [],
|
default: [],
|
||||||
})
|
})
|
||||||
public logs: {
|
public logs: number[][];
|
||||||
at: number;
|
|
||||||
color: boolean;
|
|
||||||
pos: number;
|
|
||||||
}[];
|
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
array: true, length: 64,
|
array: true, length: 64,
|
||||||
@ -117,9 +113,6 @@ export class MiReversiGame {
|
|||||||
})
|
})
|
||||||
public form2: any | null;
|
public form2: any | null;
|
||||||
|
|
||||||
/**
|
|
||||||
* ログのposを文字列としてすべて連結したもののCRC32値
|
|
||||||
*/
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 32, nullable: true,
|
length: 32, nullable: true,
|
||||||
})
|
})
|
||||||
|
@ -204,22 +204,8 @@ export const packedReversiGameDetailedSchema = {
|
|||||||
type: 'array',
|
type: 'array',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
items: {
|
items: {
|
||||||
type: 'object',
|
type: 'array',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
properties: {
|
|
||||||
at: {
|
|
||||||
type: 'number',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
pos: {
|
|
||||||
type: 'number',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
map: {
|
map: {
|
||||||
|
@ -45,7 +45,7 @@ class ReversiGameChannel extends Channel {
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case 'ready': this.ready(body); break;
|
case 'ready': this.ready(body); break;
|
||||||
case 'updateSettings': this.updateSettings(body.key, body.value); break;
|
case 'updateSettings': this.updateSettings(body.key, body.value); break;
|
||||||
case 'putStone': this.putStone(body.pos); break;
|
case 'putStone': this.putStone(body.pos, body.id); break;
|
||||||
case 'syncState': this.syncState(body.crc32); break;
|
case 'syncState': this.syncState(body.crc32); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -72,14 +72,14 @@ class ReversiGameChannel extends Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async putStone(pos: number) {
|
private async putStone(pos: number, id: string) {
|
||||||
if (this.user == null) return;
|
if (this.user == null) return;
|
||||||
|
|
||||||
// TODO: キャッシュしたい
|
// TODO: キャッシュしたい
|
||||||
const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! });
|
const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! });
|
||||||
if (game == null) throw new Error('game not found');
|
if (game == null) throw new Error('game not found');
|
||||||
|
|
||||||
this.reversiService.putStoneToGame(game, this.user, pos);
|
this.reversiService.putStoneToGame(game, this.user, pos, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -13,12 +13,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<Mfm :key="'turn:' + turnUser.id" :text="i18n.tsx._reversi.turnOf({ name: turnUser.name ?? turnUser.username })" :plain="true" :customEmojis="turnUser.emojis"/>
|
<Mfm :key="'turn:' + turnUser.id" :text="i18n.tsx._reversi.turnOf({ name: turnUser.name ?? turnUser.username })" :plain="true" :customEmojis="turnUser.emojis"/>
|
||||||
<MkEllipsis/>
|
<MkEllipsis/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="(logPos !== logs.length) && turnUser" class="turn">
|
<div v-if="(logPos !== game.logs.length) && turnUser" class="turn">
|
||||||
<Mfm :key="'past-turn-of:' + turnUser.id" :text="i18n.tsx._reversi.pastTurnOf({ name: turnUser.name ?? turnUser.username })" :plain="true" :customEmojis="turnUser.emojis"/>
|
<Mfm :key="'past-turn-of:' + turnUser.id" :text="i18n.tsx._reversi.pastTurnOf({ name: turnUser.name ?? turnUser.username })" :plain="true" :customEmojis="turnUser.emojis"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="iAmPlayer && !game.isEnded && !isMyTurn" class="turn1">{{ i18n.ts._reversi.opponentTurn }}<MkEllipsis/></div>
|
<div v-if="iAmPlayer && !game.isEnded && !isMyTurn" class="turn1">{{ i18n.ts._reversi.opponentTurn }}<MkEllipsis/></div>
|
||||||
<div v-if="iAmPlayer && !game.isEnded && isMyTurn" class="turn2" style="animation: tada 1s linear infinite both;">{{ i18n.ts._reversi.myTurn }}</div>
|
<div v-if="iAmPlayer && !game.isEnded && isMyTurn" class="turn2" style="animation: tada 1s linear infinite both;">{{ i18n.ts._reversi.myTurn }}</div>
|
||||||
<div v-if="game.isEnded && logPos == logs.length" class="result">
|
<div v-if="game.isEnded && logPos == game.logs.length" class="result">
|
||||||
<template v-if="game.winner">
|
<template v-if="game.winner">
|
||||||
<Mfm :key="'won'" :text="i18n.tsx._reversi.won({ name: game.winner.name ?? game.winner.username })" :plain="true" :customEmojis="game.winner.emojis"/>
|
<Mfm :key="'won'" :text="i18n.tsx._reversi.won({ name: game.winner.name ?? game.winner.username })" :plain="true" :customEmojis="game.winner.emojis"/>
|
||||||
<span v-if="game.surrendered != null"> ({{ i18n.ts._reversi.surrendered }})</span>
|
<span v-if="game.surrendered != null"> ({{ i18n.ts._reversi.surrendered }})</span>
|
||||||
@ -69,12 +69,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="game.isEnded" class="_panel _gaps_s" style="padding: 16px;">
|
<div v-if="game.isEnded" class="_panel _gaps_s" style="padding: 16px;">
|
||||||
<div>{{ logPos }} / {{ logs.length }}</div>
|
<div>{{ logPos }} / {{ game.logs.length }}</div>
|
||||||
<div v-if="!autoplaying" class="_buttonsCenter">
|
<div v-if="!autoplaying" class="_buttonsCenter">
|
||||||
<MkButton :disabled="logPos === 0" @click="logPos = 0"><i class="ti ti-chevrons-left"></i></MkButton>
|
<MkButton :disabled="logPos === 0" @click="logPos = 0"><i class="ti ti-chevrons-left"></i></MkButton>
|
||||||
<MkButton :disabled="logPos === 0" @click="logPos--"><i class="ti ti-chevron-left"></i></MkButton>
|
<MkButton :disabled="logPos === 0" @click="logPos--"><i class="ti ti-chevron-left"></i></MkButton>
|
||||||
<MkButton :disabled="logPos === logs.length" @click="logPos++"><i class="ti ti-chevron-right"></i></MkButton>
|
<MkButton :disabled="logPos === game.logs.length" @click="logPos++"><i class="ti ti-chevron-right"></i></MkButton>
|
||||||
<MkButton :disabled="logPos === logs.length" @click="logPos = logs.length"><i class="ti ti-chevrons-right"></i></MkButton>
|
<MkButton :disabled="logPos === game.logs.length" @click="logPos = game.logs.length"><i class="ti ti-chevrons-right"></i></MkButton>
|
||||||
</div>
|
</div>
|
||||||
<MkButton style="margin: auto;" :disabled="autoplaying" @click="autoplay()"><i class="ti ti-player-play"></i></MkButton>
|
<MkButton style="margin: auto;" :disabled="autoplaying" @click="autoplay()"><i class="ti ti-player-play"></i></MkButton>
|
||||||
</div>
|
</div>
|
||||||
@ -115,18 +115,15 @@ const props = defineProps<{
|
|||||||
const showBoardLabels = true;
|
const showBoardLabels = true;
|
||||||
const autoplaying = ref<boolean>(false);
|
const autoplaying = ref<boolean>(false);
|
||||||
const game = ref<Misskey.entities.ReversiGameDetailed>(deepClone(props.game));
|
const game = ref<Misskey.entities.ReversiGameDetailed>(deepClone(props.game));
|
||||||
const logs = ref<Misskey.entities.ReversiLog[]>(game.value.logs);
|
const logPos = ref<number>(game.value.logs.length);
|
||||||
const logPos = ref<number>(logs.value.length);
|
const engine = shallowRef<Reversi.Game>(Reversi.Serializer.restoreGame({
|
||||||
const engine = shallowRef<Reversi.Game>(new Reversi.Game(game.value.map, {
|
map: game.value.map,
|
||||||
isLlotheo: game.value.isLlotheo,
|
isLlotheo: game.value.isLlotheo,
|
||||||
canPutEverywhere: game.value.canPutEverywhere,
|
canPutEverywhere: game.value.canPutEverywhere,
|
||||||
loopedBoard: game.value.loopedBoard,
|
loopedBoard: game.value.loopedBoard,
|
||||||
|
logs: game.value.logs,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
for (const log of game.value.logs) {
|
|
||||||
engine.value.put(log.color, log.pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
const iAmPlayer = computed(() => {
|
const iAmPlayer = computed(() => {
|
||||||
return game.value.user1Id === $i.id || game.value.user2Id === $i.id;
|
return game.value.user1Id === $i.id || game.value.user2Id === $i.id;
|
||||||
});
|
});
|
||||||
@ -177,60 +174,76 @@ const cellsStyle = computed(() => {
|
|||||||
|
|
||||||
watch(logPos, (v) => {
|
watch(logPos, (v) => {
|
||||||
if (!game.value.isEnded) return;
|
if (!game.value.isEnded) return;
|
||||||
const _o = new Reversi.Game(game.value.map, {
|
engine.value = Reversi.Serializer.restoreGame({
|
||||||
|
map: game.value.map,
|
||||||
isLlotheo: game.value.isLlotheo,
|
isLlotheo: game.value.isLlotheo,
|
||||||
canPutEverywhere: game.value.canPutEverywhere,
|
canPutEverywhere: game.value.canPutEverywhere,
|
||||||
loopedBoard: game.value.loopedBoard,
|
loopedBoard: game.value.loopedBoard,
|
||||||
|
logs: game.value.logs.slice(0, v),
|
||||||
});
|
});
|
||||||
for (const log of logs.value.slice(0, v)) {
|
|
||||||
_o.put(log.color, log.pos);
|
|
||||||
}
|
|
||||||
engine.value = _o;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (game.value.isStarted && !game.value.isEnded) {
|
if (game.value.isStarted && !game.value.isEnded) {
|
||||||
useInterval(() => {
|
useInterval(() => {
|
||||||
if (game.value.isEnded) return;
|
if (game.value.isEnded) return;
|
||||||
const crc32 = CRC32.str(logs.value.map(x => x.pos.toString()).join(''));
|
const crc32 = CRC32.str(JSON.stringify(game.value.logs)).toString();
|
||||||
|
if (_DEV_) console.log('crc32', crc32);
|
||||||
props.connection.send('syncState', {
|
props.connection.send('syncState', {
|
||||||
crc32: crc32,
|
crc32: crc32,
|
||||||
});
|
});
|
||||||
}, 5000, { immediate: false, afterMounted: true });
|
}, 10000, { immediate: false, afterMounted: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const appliedOps: string[] = [];
|
||||||
|
|
||||||
function putStone(pos) {
|
function putStone(pos) {
|
||||||
if (game.value.isEnded) return;
|
if (game.value.isEnded) return;
|
||||||
if (!iAmPlayer.value) return;
|
if (!iAmPlayer.value) return;
|
||||||
if (!isMyTurn.value) return;
|
if (!isMyTurn.value) return;
|
||||||
if (!engine.value.canPut(myColor.value!, pos)) return;
|
if (!engine.value.canPut(myColor.value!, pos)) return;
|
||||||
|
|
||||||
engine.value.put(myColor.value!, pos);
|
engine.value.putStone(pos);
|
||||||
|
|
||||||
triggerRef(engine);
|
triggerRef(engine);
|
||||||
|
|
||||||
// サウンドを再生する
|
// サウンドを再生する
|
||||||
//sound.play(myColor.value ? 'reversiPutBlack' : 'reversiPutWhite');
|
//sound.play(myColor.value ? 'reversiPutBlack' : 'reversiPutWhite');
|
||||||
|
|
||||||
|
const id = Math.random().toString(36).slice(2);
|
||||||
props.connection.send('putStone', {
|
props.connection.send('putStone', {
|
||||||
pos: pos,
|
pos: pos,
|
||||||
|
id,
|
||||||
});
|
});
|
||||||
|
appliedOps.push(id);
|
||||||
|
|
||||||
checkEnd();
|
checkEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPutStone(x) {
|
function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) {
|
||||||
logs.value.push(x);
|
game.value.logs = Reversi.Serializer.serializeLogs([
|
||||||
logPos.value++;
|
...Reversi.Serializer.deserializeLogs(game.value.logs),
|
||||||
engine.value.put(x.color, x.pos);
|
log,
|
||||||
triggerRef(engine);
|
]);
|
||||||
checkEnd();
|
|
||||||
|
|
||||||
// サウンドを再生する
|
logPos.value++;
|
||||||
if (x.color !== myColor.value) {
|
|
||||||
//sound.play(x.color ? 'reversiPutBlack' : 'reversiPutWhite');
|
if (log.id == null || !appliedOps.includes(log.id)) {
|
||||||
|
switch (log.operation) {
|
||||||
|
case 'put': {
|
||||||
|
engine.value.putStone(log.pos);
|
||||||
|
triggerRef(engine);
|
||||||
|
checkEnd();
|
||||||
|
//sound.play(x.color ? 'reversiPutBlack' : 'reversiPutWhite');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEnded(x) {
|
function onStreamEnded(x) {
|
||||||
game.value = deepClone(x.game);
|
game.value = deepClone(x.game);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,23 +263,20 @@ function checkEnd() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRescue(_game) {
|
function onStreamRescue(_game) {
|
||||||
|
console.log('rescue');
|
||||||
|
|
||||||
game.value = deepClone(_game);
|
game.value = deepClone(_game);
|
||||||
|
|
||||||
engine.value = new Reversi.Game(game.value.map, {
|
engine.value = Reversi.Serializer.restoreGame({
|
||||||
|
map: game.value.map,
|
||||||
isLlotheo: game.value.isLlotheo,
|
isLlotheo: game.value.isLlotheo,
|
||||||
canPutEverywhere: game.value.canPutEverywhere,
|
canPutEverywhere: game.value.canPutEverywhere,
|
||||||
loopedBoard: game.value.loopedBoard,
|
loopedBoard: game.value.loopedBoard,
|
||||||
|
logs: game.value.logs,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const log of game.value.logs) {
|
logPos.value = game.value.logs.length;
|
||||||
engine.value.put(log.color, log.pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
triggerRef(engine);
|
|
||||||
|
|
||||||
logs.value = game.value.logs;
|
|
||||||
logPos.value = logs.value.length;
|
|
||||||
|
|
||||||
checkEnd();
|
checkEnd();
|
||||||
}
|
}
|
||||||
@ -280,21 +290,22 @@ function surrender() {
|
|||||||
function autoplay() {
|
function autoplay() {
|
||||||
autoplaying.value = true;
|
autoplaying.value = true;
|
||||||
logPos.value = 0;
|
logPos.value = 0;
|
||||||
|
const logs = Reversi.Serializer.deserializeLogs(game.value.logs);
|
||||||
|
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
logPos.value = 1;
|
logPos.value = 1;
|
||||||
|
|
||||||
let i = 1;
|
let i = 1;
|
||||||
let previousLog = game.value.logs[0];
|
let previousLog = logs[0];
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
const log = game.value.logs[i];
|
const log = logs[i];
|
||||||
const time = new Date(log.at).getTime() - new Date(previousLog.at).getTime();
|
const time = log.time - previousLog.time;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
i++;
|
i++;
|
||||||
logPos.value++;
|
logPos.value++;
|
||||||
previousLog = log;
|
previousLog = log;
|
||||||
|
|
||||||
if (i < game.value.logs.length) {
|
if (i < logs.length) {
|
||||||
tick();
|
tick();
|
||||||
} else {
|
} else {
|
||||||
autoplaying.value = false;
|
autoplaying.value = false;
|
||||||
@ -307,15 +318,15 @@ function autoplay() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
props.connection.on('putStone', onPutStone);
|
props.connection.on('log', onStreamLog);
|
||||||
props.connection.on('rescue', onRescue);
|
props.connection.on('rescue', onStreamRescue);
|
||||||
props.connection.on('ended', onEnded);
|
props.connection.on('ended', onStreamEnded);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
props.connection.off('putStone', onPutStone);
|
props.connection.off('log', onStreamLog);
|
||||||
props.connection.off('rescue', onRescue);
|
props.connection.off('rescue', onStreamRescue);
|
||||||
props.connection.off('ended', onEnded);
|
props.connection.off('ended', onStreamEnded);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -389,6 +400,7 @@ $gap: 4px;
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
|
||||||
&.boardCell_empty {
|
&.boardCell_empty {
|
||||||
border: solid 2px var(--divider);
|
border: solid 2px var(--divider);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* version: 2023.12.2
|
* version: 2023.12.2
|
||||||
* generatedAt: 2024-01-19T11:00:07.160Z
|
* generatedAt: 2024-01-20T01:28:01.779Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { SwitchCaseResponseType } from '../api.js';
|
import type { SwitchCaseResponseType } from '../api.js';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* version: 2023.12.2
|
* version: 2023.12.2
|
||||||
* generatedAt: 2024-01-19T11:00:07.158Z
|
* generatedAt: 2024-01-20T01:28:01.777Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* version: 2023.12.2
|
* version: 2023.12.2
|
||||||
* generatedAt: 2024-01-19T11:00:07.156Z
|
* generatedAt: 2024-01-20T01:28:01.775Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { operations } from './types.js';
|
import { operations } from './types.js';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* version: 2023.12.2
|
* version: 2023.12.2
|
||||||
* generatedAt: 2024-01-19T11:00:07.155Z
|
* generatedAt: 2024-01-20T01:28:01.774Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { components } from './types.js';
|
import { components } from './types.js';
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* version: 2023.12.2
|
* version: 2023.12.2
|
||||||
* generatedAt: 2024-01-19T11:00:07.077Z
|
* generatedAt: 2024-01-20T01:28:01.695Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -4517,11 +4517,7 @@ export type components = {
|
|||||||
isLlotheo: boolean;
|
isLlotheo: boolean;
|
||||||
canPutEverywhere: boolean;
|
canPutEverywhere: boolean;
|
||||||
loopedBoard: boolean;
|
loopedBoard: boolean;
|
||||||
logs: {
|
logs: unknown[][];
|
||||||
at: number;
|
|
||||||
color: boolean;
|
|
||||||
pos: number;
|
|
||||||
}[];
|
|
||||||
map: string[];
|
map: string[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,22 @@
|
|||||||
{
|
{
|
||||||
|
"type": "module",
|
||||||
"name": "misskey-reversi",
|
"name": "misskey-reversi",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"main": "./built/index.js",
|
"exports": {
|
||||||
"types": "./built/index.d.ts",
|
".": {
|
||||||
|
"import": "./built/esm/index.js",
|
||||||
|
"types": "./built/dts/index.d.ts"
|
||||||
|
},
|
||||||
|
"./*": {
|
||||||
|
"import": "./built/esm/*",
|
||||||
|
"types": "./built/dts/*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "npm run ts",
|
||||||
|
"ts": "npm run ts-esm && npm run ts-dts",
|
||||||
|
"ts-esm": "tsc --outDir built/esm",
|
||||||
|
"ts-dts": "tsc --outDir built/dts --declaration true --emitDeclarationOnly true --declarationMap true",
|
||||||
"watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run build\"",
|
"watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run build\"",
|
||||||
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
@ -16,6 +28,7 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "6.19.0",
|
"@typescript-eslint/eslint-plugin": "6.19.0",
|
||||||
"@typescript-eslint/parser": "6.19.0",
|
"@typescript-eslint/parser": "6.19.0",
|
||||||
"eslint": "8.56.0",
|
"eslint": "8.56.0",
|
||||||
|
"nodemon": "3.0.2",
|
||||||
"typescript": "5.3.3"
|
"typescript": "5.3.3"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
@ -46,7 +46,7 @@ export class Game {
|
|||||||
|
|
||||||
constructor(map: string[], opts: Options) {
|
constructor(map: string[], opts: Options) {
|
||||||
//#region binds
|
//#region binds
|
||||||
this.put = this.put.bind(this);
|
this.putStone = this.putStone.bind(this);
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Options
|
//#region Options
|
||||||
@ -88,7 +88,10 @@ export class Game {
|
|||||||
return x + (y * this.mapWidth);
|
return x + (y * this.mapWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
public put(color: Color, pos: number) {
|
public putStone(pos: number) {
|
||||||
|
const color = this.turn;
|
||||||
|
if (color == null) return;
|
||||||
|
|
||||||
this.prevPos = pos;
|
this.prevPos = pos;
|
||||||
this.prevColor = color;
|
this.prevColor = color;
|
||||||
|
|
||||||
|
@ -3,10 +3,6 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Game } from './game.js';
|
export { Game } from './game.js';
|
||||||
|
export * as Serializer from './serializer.js';
|
||||||
export {
|
|
||||||
Game,
|
|
||||||
};
|
|
||||||
|
|
||||||
export * as maps from './maps.js';
|
export * as maps from './maps.js';
|
||||||
|
98
packages/misskey-reversi/src/serializer.ts
Normal file
98
packages/misskey-reversi/src/serializer.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Game } from './game.js';
|
||||||
|
|
||||||
|
export type Log = {
|
||||||
|
time: number;
|
||||||
|
player: boolean;
|
||||||
|
operation: 'put';
|
||||||
|
pos: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SerializedLog = number[];
|
||||||
|
|
||||||
|
export function serializeLogs(logs: Log[]) {
|
||||||
|
const _logs: number[][] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < logs.length; i++) {
|
||||||
|
const log = logs[i];
|
||||||
|
const timeDelta = i === 0 ? log.time : log.time - logs[i - 1].time;
|
||||||
|
|
||||||
|
switch (log.operation) {
|
||||||
|
case 'put':
|
||||||
|
_logs.push([timeDelta, log.player ? 1 : 0, 0, log.pos]);
|
||||||
|
break;
|
||||||
|
//case 'surrender':
|
||||||
|
// _logs.push([timeDelta, log.player, 1]);
|
||||||
|
// break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _logs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deserializeLogs(logs: SerializedLog[]) {
|
||||||
|
const _logs: Log[] = [];
|
||||||
|
|
||||||
|
let time = 0;
|
||||||
|
|
||||||
|
for (const log of logs) {
|
||||||
|
const timeDelta = log[0];
|
||||||
|
time += timeDelta;
|
||||||
|
|
||||||
|
const player = log[1];
|
||||||
|
const operation = log[2];
|
||||||
|
|
||||||
|
switch (operation) {
|
||||||
|
case 0:
|
||||||
|
_logs.push({
|
||||||
|
time,
|
||||||
|
player: player === 1,
|
||||||
|
operation: 'put',
|
||||||
|
pos: log[3],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
//case 1:
|
||||||
|
// _logs.push({
|
||||||
|
// time,
|
||||||
|
// player: player === 1,
|
||||||
|
// operation: 'surrender',
|
||||||
|
// });
|
||||||
|
// break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _logs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restoreGame(env: {
|
||||||
|
map: string[];
|
||||||
|
isLlotheo: boolean;
|
||||||
|
canPutEverywhere: boolean;
|
||||||
|
loopedBoard: boolean;
|
||||||
|
logs: SerializedLog[];
|
||||||
|
}) {
|
||||||
|
const logs = deserializeLogs(env.logs);
|
||||||
|
|
||||||
|
const game = new Game(env.map, {
|
||||||
|
isLlotheo: env.isLlotheo,
|
||||||
|
canPutEverywhere: env.canPutEverywhere,
|
||||||
|
loopedBoard: env.loopedBoard,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const log of logs) {
|
||||||
|
switch (log.operation) {
|
||||||
|
case 'put':
|
||||||
|
game.putStone(log.pos);
|
||||||
|
break;
|
||||||
|
//case 'surrender':
|
||||||
|
// game.surrender(log.player);
|
||||||
|
// break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return game;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user