Skip to content

Commit

Permalink
feat: append record
Browse files Browse the repository at this point in the history
  • Loading branch information
shiftpsh committed Jul 12, 2024
1 parent 17bbc53 commit 5af00fc
Show file tree
Hide file tree
Showing 13 changed files with 272 additions and 13 deletions.
8 changes: 8 additions & 0 deletions prisma/migrations/20240712122158_uma_precision/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:
- You are about to alter the column `uma` on the `UserScore` table. The data in that column could be lost. The data in that column will be cast from `Decimal(65,30)` to `Decimal(10,1)`.
*/
-- AlterTable
ALTER TABLE `UserScore` MODIFY `uma` DECIMAL(10, 1) NOT NULL;
2 changes: 1 addition & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ model UserScore {
gameId Int
initialSeat Wind
score Int
uma Decimal
uma Decimal @db.Decimal(10, 1)
rank Int
stars Int
createdAt DateTime @default(now())
Expand Down
8 changes: 2 additions & 6 deletions src/api/auth/login.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { RequestHandler } from "express";
import { Record, String } from "runtypes";
import db from "src/database/db";
import { findUserByLoginId } from "src/model/user/general";
import { safeHash } from "src/utils/hash";
import { UserToJWT } from "src/utils/jwt";

Expand All @@ -12,11 +12,7 @@ const Body = Record({
const login: RequestHandler = async (req, res) => {
const { loginId, password } = Body.check(req.body);

const user = await db.user.findUnique({
where: {
loginId,
},
});
const user = await findUserByLoginId(loginId);

if (!user) {
res.status(401).send({
Expand Down
2 changes: 2 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Router } from "express";
import authMiddleware from "src/middlewares/auth";
import auth from "./auth";
import record from "./record";

const router = Router();
router.use(authMiddleware);
Expand All @@ -10,5 +11,6 @@ router.get("/hello", (req, res) =>
);

router.use("/auth", auth);
router.use("/record", record);

export default router;
23 changes: 23 additions & 0 deletions src/api/record/append.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { RequestHandler } from "express";
import db from "src/database/db";
import { gameInputToGameDataCreateInput } from "src/model/game/general";
import { GameInputGuard } from "src/utils/guard";

const Body = GameInputGuard;

const append: RequestHandler = async (req, res) => {
const body = Body.check(req.body);

if (!req.user) {
res.sendStatus(401);
return;
}

await db.gameRecord.create({
data: gameInputToGameDataCreateInput(body),
});

res.sendStatus(200);
};

export default append;
9 changes: 9 additions & 0 deletions src/api/record/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Router } from "express";
import { wrap } from "src/utils/asyncWrapper";
import append from "./append";

const router = Router();

router.put("/append", wrap(append));

export default router;
57 changes: 57 additions & 0 deletions src/model/game/general.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Prisma, Wind } from "@prisma/client";
import { Static } from "runtypes";
import { GameInputGuard } from "src/utils/guard";
import { gameTypes } from "../gameType/general";

const windRank: Record<Wind, number> = {
[Wind.EAST]: 0,
[Wind.SOUTH]: 1,
[Wind.WEST]: 2,
[Wind.NORTH]: 3,
};

export const gameInputToGameDataCreateInput = (
gameInput: Static<typeof GameInputGuard>
): Prisma.GameRecordCreateInput => {
const { gameType, userScores } = gameInput;
userScores.sort((a, b) => {
if (a.score !== b.score) return b.score - a.score;
return windRank[a.initialSeat] - windRank[b.initialSeat];
});

const { uma, starsPerYakuman } = gameTypes[gameType];

return {
gameType,
userScores: {
create: userScores.map(
({ score, userId, initialSeat, yakuman: yakumanArray }, i) => {
const stars = yakumanArray
.flatMap((x) => x)
.reduce((acc, x) => acc + starsPerYakuman[x], 0);
return {
score,
user: {
connect: { userId },
},
rank: i + 1,
initialSeat,
stars,
uma: uma(score, i + 1),
userScoreYakumanRecords: {
create: yakumanArray.map((yakumans) => ({
starCount: yakumans.reduce(
(acc, x) => acc + starsPerYakuman[x],
0
),
userScoreYakuman: {
create: yakumans.map((yakuman) => ({ yakuman })),
},
})),
},
};
}
),
},
};
};
82 changes: 82 additions & 0 deletions src/model/gameType/general.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { GameType, Wind, Yakuman } from "@prisma/client";
import { LocalizedString } from "src/types/i18n";
import { YAKUMAN_STARS_DEFAULT } from "./yakuman";

const RANK_UMA_HANCHAN_4P = [30, 10, -10, -30];
const RANK_UMA_HANCHAN_3P = [30, 0, -30];

export interface GameTypeDetails {
players: number;
totalScore: number;
winds: Wind[];
starsPerYakuman: Record<Yakuman, number>;
uma: (score: number, rank: number) => number;
displayName: LocalizedString;
}

export const gameTypes: Record<GameType, GameTypeDetails> = {
P4_HANCHAN: {
players: 4,
totalScore: 100000,
winds: [Wind.EAST, Wind.SOUTH, Wind.WEST, Wind.NORTH],
starsPerYakuman: YAKUMAN_STARS_DEFAULT,
uma: (score, rank) => {
const scoreUma = (score - 25000) / 1000;
const rankUma = RANK_UMA_HANCHAN_4P[rank - 1];
return scoreUma + rankUma;
},
displayName: {
ko: "4인 반장전",
en: "4-player Hanchan",
ja: "四人半荘戦",
},
},
P3_HANCHAN: {
players: 3,
totalScore: 105000,
winds: [Wind.EAST, Wind.SOUTH, Wind.WEST],
starsPerYakuman: YAKUMAN_STARS_DEFAULT,
uma: (score, rank) => {
const scoreUma = (score - 35000) / 1000;
const rankUma = RANK_UMA_HANCHAN_3P[rank - 1];
return scoreUma + rankUma;
},
displayName: {
ko: "3인 반장전",
en: "3-player Hanchan",
ja: "三人半荘戦",
},
},
P4_TOUPUUSEN: {
players: 4,
totalScore: 100000,
winds: [Wind.EAST, Wind.SOUTH, Wind.WEST, Wind.NORTH],
starsPerYakuman: YAKUMAN_STARS_DEFAULT,
uma: (score, rank) => {
const scoreUma = (score - 25000) / 1000;
const rankUma = RANK_UMA_HANCHAN_4P[rank - 1] / 2;
return scoreUma + rankUma;
},
displayName: {
ko: "4인 통풍전",
en: "4-player Toupusen",
ja: "四人東風戦",
},
},
P3_TOUPUUSEN: {
players: 3,
totalScore: 105000,
winds: [Wind.EAST, Wind.SOUTH, Wind.WEST],
starsPerYakuman: YAKUMAN_STARS_DEFAULT,
uma: (score, rank) => {
const scoreUma = (score - 35000) / 1000;
const rankUma = RANK_UMA_HANCHAN_3P[rank - 1] / 2;
return scoreUma + rankUma;
},
displayName: {
ko: "3인 통풍전",
en: "3-player Toupusen",
ja: "三人東風戦",
},
},
};
19 changes: 19 additions & 0 deletions src/model/gameType/yakuman.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Yakuman } from "@prisma/client";

export const YAKUMAN_STARS_DEFAULT: Record<Yakuman, number> = {
TENHOU: 1,
CHIIHOU: 1,
SUUANKOU: 1,
SUUANKOU_TANKI: 2,
KOKUSHI_MUSOU: 1,
KOKUSHI_MUSOU_13MEN: 2,
CHUUREN_POUTOU: 1,
CHUUREN_POUTOU_JUNSEI: 2,
RYUUIISOU: 1,
TSUUIISOU: 1,
CHINROUTOU: 1,
DAISANGEN: 1,
SHOUSUUSHII: 1,
DAISUUSHII: 2,
SUUKANTSU: 1,
};
10 changes: 10 additions & 0 deletions src/model/user/general.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import db from "src/database/db";

export const findUserByLoginId = async (loginId: string) => {
const user = await db.user.findUnique({
where: {
loginId,
},
});
return user;
};
5 changes: 5 additions & 0 deletions src/types/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface LocalizedString {
ko: string;
en: string;
ja: string;
}
52 changes: 52 additions & 0 deletions src/utils/guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { GameType, Wind, Yakuman } from "@prisma/client";
import {
Array as ArrayType,
Number as NumberType,
Record as RecordType,
String as StringType,
} from "runtypes";
import { gameTypes } from "src/model/gameType/general";

export const GameTypeGuard = StringType.withConstraint<GameType>((x) =>
Object.keys(GameType).includes(x)
);

export const WindGuard = StringType.withConstraint<Wind>((x) =>
Object.keys(Wind).includes(x)
);

export const YakumanGuard = StringType.withConstraint<Yakuman>((x) =>
Object.keys(Yakuman).includes(x)
);

export const Integer = NumberType.withConstraint((x) => Number.isInteger(x));

export const GameInputGuard = RecordType({
gameType: GameTypeGuard,
userScores: ArrayType(
RecordType({
userId: Integer,
initialSeat: WindGuard,
score: Integer,
yakuman: ArrayType(ArrayType(YakumanGuard)),
})
),
}).withConstraint(({ gameType, userScores }) => {
const { players, totalScore, winds } = gameTypes[gameType];
if (userScores.length !== players) {
return false;
}
const sum = userScores.reduce((acc, { score }) => acc + score, 0);
if (sum !== totalScore) {
return false;
}
const playerWinds = new Set<Wind>();
userScores.forEach(({ initialSeat }) => playerWinds.add(initialSeat));
if (playerWinds.size !== players) {
return false;
}
if (Array.from(playerWinds).some((wind) => !winds.includes(wind))) {
return false;
}
return true;
});
8 changes: 2 additions & 6 deletions src/utils/jwt.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { User } from "@prisma/client";
import jwt from "jsonwebtoken";
import { Number, Record, String } from "runtypes";
import db from "src/database/db";
import { findUserByLoginId } from "src/model/user/general";
import config from "../config";

const TOKEN_VERSION = 1;
Expand Down Expand Up @@ -41,11 +41,7 @@ export async function JWTToUser(token: string): Promise<User> {
throw new CredentialError("Token has expired");
}

const user = await db.user.findUnique({
where: {
loginId,
},
});
const user = await findUserByLoginId(loginId);
if (!user) throw new CredentialError("Invalid credentials.");

return user;
Expand Down

0 comments on commit 5af00fc

Please sign in to comment.