Fixed 267U.pre2
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
import { IsNull, LessThan, MoreThan } from "typeorm";
|
||||
import { initialSfen, makeSfen, parseSfen } from "shogiops/sfen";
|
||||
import { makeUsi, parseUsi } from "shogiops/util";
|
||||
import { redisClient } from "../../db/redis.js";
|
||||
import { genId } from "../../misc/gen-id.js";
|
||||
import { ShogiGames, Users } from "../../models/index.js";
|
||||
import { publishShogiGameStream, publishShogiStream } from "../stream.js";
|
||||
const INVITATION_TIMEOUT_MS = 1000 * 20;
|
||||
const RULES = "standard";
|
||||
async function getGame(id) {
|
||||
return await ShogiGames.findOne({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
relations: {
|
||||
user1: true,
|
||||
user2: true
|
||||
}
|
||||
});
|
||||
}
|
||||
async function publishGame(id, type, body) {
|
||||
publishShogiGameStream(id, type, body);
|
||||
}
|
||||
function colorUserId(game, color) {
|
||||
const senteUserId = game.sente === 2 ? game.user2Id : game.user1Id;
|
||||
const goteUserId = game.sente === 2 ? game.user1Id : game.user2Id;
|
||||
return color === "sente" ? senteUserId : goteUserId;
|
||||
}
|
||||
async function endGame(game, winnerId, surrenderedUserId) {
|
||||
await ShogiGames.update(game.id, {
|
||||
isEnded: true,
|
||||
endedAt: new Date(),
|
||||
winnerId,
|
||||
surrenderedUserId
|
||||
});
|
||||
const fresh = await getGame(game.id);
|
||||
if (fresh) {
|
||||
await publishGame(game.id, "ended", {
|
||||
winnerId,
|
||||
game: await ShogiGames.packDetail(fresh)
|
||||
});
|
||||
}
|
||||
}
|
||||
export async function matchSpecificUser(me, target, multiple = false) {
|
||||
if (!multiple) {
|
||||
const since = genId(new Date(Date.now() - 1000 * 60 * 3));
|
||||
const games = await ShogiGames.find({
|
||||
where: [
|
||||
{
|
||||
id: MoreThan(since),
|
||||
user1Id: me.id,
|
||||
user2Id: target.id,
|
||||
isStarted: false
|
||||
},
|
||||
{
|
||||
id: MoreThan(since),
|
||||
user1Id: target.id,
|
||||
user2Id: me.id,
|
||||
isStarted: false
|
||||
}
|
||||
],
|
||||
relations: {
|
||||
user1: true,
|
||||
user2: true
|
||||
},
|
||||
order: {
|
||||
id: "DESC"
|
||||
}
|
||||
});
|
||||
if (games.length > 0) return games[0];
|
||||
}
|
||||
const invitations = await redisClient.zrangebyscore(`shogi:matchSpecific:${me.id}`, Date.now() - INVITATION_TIMEOUT_MS, "+inf");
|
||||
if (invitations.includes(target.id)) {
|
||||
await redisClient.zrem(`shogi:matchSpecific:${me.id}`, target.id);
|
||||
return await matched(target.id, me.id);
|
||||
}
|
||||
const pipeline = redisClient.pipeline();
|
||||
pipeline.zadd(`shogi:matchSpecific:${target.id}`, Date.now(), me.id);
|
||||
pipeline.expire(`shogi:matchSpecific:${target.id}`, 120);
|
||||
await pipeline.exec();
|
||||
publishShogiStream(target.id, "invited", {
|
||||
user: await Users.pack(me.id, target, {
|
||||
detail: false
|
||||
})
|
||||
});
|
||||
return null;
|
||||
}
|
||||
export async function matchAnyUser(me, multiple = false) {
|
||||
if (!multiple) {
|
||||
const since = genId(new Date(Date.now() - 1000 * 60 * 3));
|
||||
const games = await ShogiGames.find({
|
||||
where: [
|
||||
{
|
||||
id: MoreThan(since),
|
||||
user1Id: me.id,
|
||||
isStarted: false
|
||||
},
|
||||
{
|
||||
id: MoreThan(since),
|
||||
user2Id: me.id,
|
||||
isStarted: false
|
||||
}
|
||||
],
|
||||
relations: {
|
||||
user1: true,
|
||||
user2: true
|
||||
},
|
||||
order: {
|
||||
id: "DESC"
|
||||
}
|
||||
});
|
||||
if (games.length > 0) return games[0];
|
||||
}
|
||||
const invitations = await redisClient.zrangebyscore(`shogi:matchSpecific:${me.id}`, Date.now() - INVITATION_TIMEOUT_MS, "+inf");
|
||||
if (invitations.length > 0) {
|
||||
const inviterId = invitations[Math.floor(Math.random() * invitations.length)];
|
||||
await redisClient.zrem(`shogi:matchSpecific:${me.id}`, inviterId);
|
||||
return await matched(inviterId, me.id);
|
||||
}
|
||||
const matchings = await redisClient.zrevrange("shogi:matchAny", 0, 1);
|
||||
const matchedUserId = matchings.find((id)=>id !== me.id);
|
||||
if (matchedUserId) {
|
||||
await redisClient.zrem("shogi:matchAny", me.id, matchedUserId);
|
||||
return await matched(matchedUserId, me.id);
|
||||
}
|
||||
const pipeline = redisClient.pipeline();
|
||||
pipeline.zadd("shogi:matchAny", Date.now(), me.id);
|
||||
pipeline.expire("shogi:matchAny", 15);
|
||||
await pipeline.exec();
|
||||
return null;
|
||||
}
|
||||
async function matched(parentId, childId) {
|
||||
const game = await ShogiGames.insert({
|
||||
id: genId(),
|
||||
startedAt: null,
|
||||
endedAt: null,
|
||||
user1Id: parentId,
|
||||
user2Id: childId,
|
||||
user1Ready: false,
|
||||
user2Ready: false,
|
||||
sente: null,
|
||||
isStarted: false,
|
||||
isEnded: false,
|
||||
winnerId: null,
|
||||
surrenderedUserId: null,
|
||||
sfen: initialSfen(RULES),
|
||||
logs: []
|
||||
}).then((x)=>getGame(x.identifiers[0].id));
|
||||
if (!game) throw new Error("failed to create shogi game");
|
||||
publishShogiStream(parentId, "matched", {
|
||||
game: await ShogiGames.packDetail(game)
|
||||
});
|
||||
return game;
|
||||
}
|
||||
export async function cancelMatch(user, userId) {
|
||||
if (userId) await redisClient.zrem(`shogi:matchSpecific:${userId}`, user.id);
|
||||
await redisClient.zrem("shogi:matchAny", user.id);
|
||||
}
|
||||
export async function getInvitations(user) {
|
||||
return await redisClient.zrangebyscore(`shogi:matchSpecific:${user.id}`, Date.now() - INVITATION_TIMEOUT_MS, "+inf");
|
||||
}
|
||||
export async function gameReady(gameId, user, ready) {
|
||||
const game = await getGame(gameId);
|
||||
if (!game || game.isStarted) return;
|
||||
if (game.user1Id !== user.id && game.user2Id !== user.id) return;
|
||||
const patch = game.user1Id === user.id ? {
|
||||
user1Ready: ready
|
||||
} : {
|
||||
user2Ready: ready
|
||||
};
|
||||
await ShogiGames.update(game.id, patch);
|
||||
const fresh = await getGame(game.id);
|
||||
if (!fresh) return;
|
||||
await publishGame(game.id, "changeReadyStates", {
|
||||
user1: fresh.user1Ready,
|
||||
user2: fresh.user2Ready
|
||||
});
|
||||
if (fresh.user1Ready && fresh.user2Ready) setTimeout(()=>startGame(fresh.id), 3000);
|
||||
}
|
||||
async function startGame(gameId) {
|
||||
const game = await getGame(gameId);
|
||||
if (!game || game.isStarted || game.isEnded || !game.user1Ready || !game.user2Ready) return;
|
||||
await ShogiGames.update(game.id, {
|
||||
startedAt: new Date(),
|
||||
isStarted: true,
|
||||
sente: Math.random() > 0.5 ? 1 : 2
|
||||
});
|
||||
const fresh = await getGame(game.id);
|
||||
if (!fresh) return;
|
||||
await publishGame(game.id, "started", {
|
||||
game: await ShogiGames.packDetail(fresh)
|
||||
});
|
||||
}
|
||||
export async function putMove(gameId, user, usi, id) {
|
||||
const game = await getGame(gameId);
|
||||
if (!game || !game.isStarted || game.isEnded || game.sente == null) return;
|
||||
if (game.user1Id !== user.id && game.user2Id !== user.id) return;
|
||||
const pos = parseSfen(RULES, game.sfen).unwrap();
|
||||
const myColor = colorUserId(game, pos.turn) === user.id ? pos.turn : null;
|
||||
if (myColor == null) return;
|
||||
const move = parseUsi(usi);
|
||||
if (!move || !pos.isLegal(move)) return;
|
||||
pos.play(move);
|
||||
const nextSfen = makeSfen(pos);
|
||||
const log = {
|
||||
id: id ?? null,
|
||||
at: Date.now(),
|
||||
userId: user.id,
|
||||
usi: makeUsi(move),
|
||||
sfen: nextSfen
|
||||
};
|
||||
const logs = [
|
||||
...game.logs ?? [],
|
||||
log
|
||||
];
|
||||
await ShogiGames.update(game.id, {
|
||||
sfen: nextSfen,
|
||||
logs
|
||||
});
|
||||
await publishGame(game.id, "log", log);
|
||||
const outcome = pos.outcome();
|
||||
const fresh = await getGame(game.id);
|
||||
if (!fresh) return;
|
||||
if (outcome) {
|
||||
const winnerId = outcome.winner ? colorUserId(fresh, outcome.winner) : null;
|
||||
await endGame(fresh, winnerId, null);
|
||||
}
|
||||
}
|
||||
export async function surrender(gameId, user) {
|
||||
const game = await getGame(gameId);
|
||||
if (!game || game.isEnded) return;
|
||||
if (game.user1Id !== user.id && game.user2Id !== user.id) return;
|
||||
await endGame(game, game.user1Id === user.id ? game.user2Id : game.user1Id, user.id);
|
||||
}
|
||||
export async function cancelGame(gameId, user) {
|
||||
const game = await getGame(gameId);
|
||||
if (!game || game.isStarted) return;
|
||||
if (game.user1Id !== user.id && game.user2Id !== user.id) return;
|
||||
await ShogiGames.delete(game.id);
|
||||
await publishGame(game.id, "canceled", {
|
||||
userId: user.id
|
||||
});
|
||||
}
|
||||
export async function cleanupOpenGames() {
|
||||
await ShogiGames.delete({
|
||||
id: LessThan(genId(new Date(Date.now() - 1000 * 60 * 10))),
|
||||
isStarted: false,
|
||||
startedAt: IsNull()
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user