diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..55712c1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} \ No newline at end of file diff --git a/prisma/migrations/20240716180540_added_game_records/migration.sql b/prisma/migrations/20240716180540_added_game_records/migration.sql new file mode 100644 index 0000000..cf240bf --- /dev/null +++ b/prisma/migrations/20240716180540_added_game_records/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - Added the required column `addedByUserId` to the `GameRecord` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE `GameRecord` ADD COLUMN `addedByUserId` INTEGER NOT NULL; + +-- AddForeignKey +ALTER TABLE `GameRecord` ADD CONSTRAINT `GameRecord_addedByUserId_fkey` FOREIGN KEY (`addedByUserId`) REFERENCES `User`(`userId`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20240716183224_p3_taboo/migration.sql b/prisma/migrations/20240716183224_p3_taboo/migration.sql new file mode 100644 index 0000000..79e305d --- /dev/null +++ b/prisma/migrations/20240716183224_p3_taboo/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `GameRecord` MODIFY `gameType` ENUM('P4_TOUPUUSEN', 'P3_TOUPUUSEN', 'P4_HANCHAN', 'P3_HANCHAN', 'P3_TABOO') NOT NULL; diff --git a/prisma/migrations/20240716185547_user_last_game_at/migration.sql b/prisma/migrations/20240716185547_user_last_game_at/migration.sql new file mode 100644 index 0000000..efdcd0b --- /dev/null +++ b/prisma/migrations/20240716185547_user_last_game_at/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `User` ADD COLUMN `lastGameAt` DATETIME(3) NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cc0de83..a541d49 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,6 +12,7 @@ enum GameType { P3_TOUPUUSEN P4_HANCHAN P3_HANCHAN + P3_TABOO } enum Wind { @@ -46,19 +47,23 @@ model User { hashedPassword String? isHost Boolean @default(false) firstGameAt DateTime? + lastGameAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - userScores UserScore[] + userScores UserScore[] + addedGameRecords GameRecord[] } model GameRecord { - gameId Int @id @default(autoincrement()) - gameType GameType - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - userScores UserScore[] + gameId Int @id @default(autoincrement()) + gameType GameType + addedByUserId Int + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + addedByUser User @relation(fields: [addedByUserId], references: [userId]) + userScores UserScore[] @@index([gameType]) @@index([createdAt]) diff --git a/src/api/index.ts b/src/api/index.ts index 3f5b31c..aa1b056 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -2,6 +2,7 @@ import { Router } from "express"; import authMiddleware from "src/middlewares/auth"; import auth from "./auth"; import record from "./record"; +import site from "./site"; const router = Router(); router.use(authMiddleware); @@ -12,5 +13,6 @@ router.get("/hello", (req, res) => router.use("/auth", auth); router.use("/record", record); +router.use("/site", site); export default router; diff --git a/src/api/record/$put.ts b/src/api/record/$put.ts index 2895904..4b8e601 100644 --- a/src/api/record/$put.ts +++ b/src/api/record/$put.ts @@ -14,7 +14,21 @@ const handler: RequestHandler = async (req, res) => { } await db.gameRecord.create({ - data: gameInputToGameDataCreateInput(body), + data: { + ...gameInputToGameDataCreateInput(body), + addedByUserId: req.user.userId, + }, + }); + + await db.user.updateMany({ + where: { + userId: { + in: body.userScores.map((x) => x.userId), + }, + }, + data: { + lastGameAt: new Date(), + }, }); res.sendStatus(200); diff --git a/src/api/site/game_types/$get.ts b/src/api/site/game_types/$get.ts new file mode 100644 index 0000000..3d74f33 --- /dev/null +++ b/src/api/site/game_types/$get.ts @@ -0,0 +1,15 @@ +import { RequestHandler } from "express"; +import { gameTypes } from "src/model/gameType/general"; +import { toGameTypeDetailsResponse } from "src/model/gameType/types"; + +const handler: RequestHandler = (req, res) => { + res.send( + Object.entries(gameTypes) + .sort((a, b) => { + return a[1].index - b[1].index; + }) + .map(([k, v]) => toGameTypeDetailsResponse(k, v)) + ); +}; + +export default handler; diff --git a/src/api/site/game_types/index.ts b/src/api/site/game_types/index.ts new file mode 100644 index 0000000..3ccf234 --- /dev/null +++ b/src/api/site/game_types/index.ts @@ -0,0 +1,8 @@ +import { Router } from "express"; +import $get from "./$get"; + +const router = Router(); + +router.get("/", $get); + +export default router; diff --git a/src/api/site/index.ts b/src/api/site/index.ts new file mode 100644 index 0000000..8c83116 --- /dev/null +++ b/src/api/site/index.ts @@ -0,0 +1,10 @@ +import { Router } from "express"; +import game_types from "./game_types"; +import jyanshis from "./jyanshis"; + +const router = Router(); + +router.use("/game_types", game_types); +router.use("/jyanshis", jyanshis); + +export default router; diff --git a/src/api/site/jyanshis/$get.ts b/src/api/site/jyanshis/$get.ts new file mode 100644 index 0000000..6fb89cb --- /dev/null +++ b/src/api/site/jyanshis/$get.ts @@ -0,0 +1,18 @@ +import { RequestHandler } from "express"; +import db from "src/database/db"; +import { toUserResponse } from "src/model/user/types"; + +const handler: RequestHandler = async (req, res) => { + const users = await db.user.findMany({ + orderBy: { + lastGameAt: { + sort: "desc", + nulls: "last", + }, + }, + }); + + res.send(users.map((u) => toUserResponse(u))); +}; + +export default handler; diff --git a/src/api/site/jyanshis/index.ts b/src/api/site/jyanshis/index.ts new file mode 100644 index 0000000..599e4a6 --- /dev/null +++ b/src/api/site/jyanshis/index.ts @@ -0,0 +1,9 @@ +import { Router } from "express"; +import { wrap } from "src/utils/asyncWrapper"; +import $get from "./$get"; + +const router = Router(); + +router.get("/", wrap($get)); + +export default router; diff --git a/src/model/game/general.ts b/src/model/game/general.ts index 86abc11..cdcbbfc 100644 --- a/src/model/game/general.ts +++ b/src/model/game/general.ts @@ -12,8 +12,13 @@ const windRank: Record = { export const gameInputToGameDataCreateInput = ( gameInput: Static -): Prisma.GameRecordCreateInput => { - const { gameType, userScores } = gameInput; +): Omit => { + const { + gameType, + userScores, + createdAt: createdAtInput = new Date(), + } = gameInput; + const createdAt = new Date(createdAtInput); userScores.sort((a, b) => { if (a.score !== b.score) return b.score - a.score; return windRank[a.initialSeat] - windRank[b.initialSeat]; @@ -45,13 +50,23 @@ export const gameInputToGameDataCreateInput = ( 0 ), userScoreYakuman: { - create: yakumans.map((yakuman) => ({ yakuman })), + create: yakumans.map((yakuman) => ({ + yakuman, + createdAt, + updatedAt: createdAt, + })), }, + createdAt, + updatedAt: createdAt, })), }, + createdAt, + updatedAt: createdAt, }; } ), }, + createdAt, + updatedAt: createdAt, }; }; diff --git a/src/model/gameType/general.ts b/src/model/gameType/general.ts index 2315496..a905949 100644 --- a/src/model/gameType/general.ts +++ b/src/model/gameType/general.ts @@ -6,8 +6,10 @@ const RANK_UMA_HANCHAN_4P = [30, 10, -10, -30]; const RANK_UMA_HANCHAN_3P = [30, 0, -30]; export interface GameTypeDetails { + index: number; players: number; totalScore: number; + scoreType: "score" | "chips"; winds: Wind[]; starsPerYakuman: Record; uma: (score: number, rank: number) => number; @@ -16,8 +18,10 @@ export interface GameTypeDetails { export const gameTypes: Record = { P4_HANCHAN: { + index: 0, players: 4, totalScore: 100000, + scoreType: "score", winds: [Wind.EAST, Wind.SOUTH, Wind.WEST, Wind.NORTH], starsPerYakuman: YAKUMAN_STARS_DEFAULT, uma: (score, rank) => { @@ -31,9 +35,27 @@ export const gameTypes: Record = { ja: "四人半荘戦", }, }, + P3_TABOO: { + index: 1, + players: 3, + totalScore: 0, + scoreType: "chips", + winds: [Wind.EAST, Wind.SOUTH, Wind.WEST], + starsPerYakuman: YAKUMAN_STARS_DEFAULT, + uma: (score) => { + return score; + }, + displayName: { + ko: "3인 금기전", + en: "3-player Taboo", + ja: "三人禁忌戦", + }, + }, P3_HANCHAN: { + index: 100, players: 3, totalScore: 105000, + scoreType: "score", winds: [Wind.EAST, Wind.SOUTH, Wind.WEST], starsPerYakuman: YAKUMAN_STARS_DEFAULT, uma: (score, rank) => { @@ -48,8 +70,10 @@ export const gameTypes: Record = { }, }, P4_TOUPUUSEN: { + index: 10000, players: 4, totalScore: 100000, + scoreType: "score", winds: [Wind.EAST, Wind.SOUTH, Wind.WEST, Wind.NORTH], starsPerYakuman: YAKUMAN_STARS_DEFAULT, uma: (score, rank) => { @@ -64,8 +88,10 @@ export const gameTypes: Record = { }, }, P3_TOUPUUSEN: { + index: 10100, players: 3, totalScore: 105000, + scoreType: "score", winds: [Wind.EAST, Wind.SOUTH, Wind.WEST], starsPerYakuman: YAKUMAN_STARS_DEFAULT, uma: (score, rank) => { diff --git a/src/model/gameType/types.ts b/src/model/gameType/types.ts new file mode 100644 index 0000000..90c0c3a --- /dev/null +++ b/src/model/gameType/types.ts @@ -0,0 +1,15 @@ +import { GameTypeDetails } from "./general"; + +export const toGameTypeDetailsResponse = ( + type: string, + { players, totalScore, scoreType, winds, displayName }: GameTypeDetails +) => { + return { + type, + players, + totalScore, + scoreType, + winds, + displayName, + }; +}; diff --git a/src/model/user/types.ts b/src/model/user/types.ts index 11e914c..dc3fc26 100644 --- a/src/model/user/types.ts +++ b/src/model/user/types.ts @@ -5,11 +5,13 @@ export const toUserResponse = ({ loginId, displayName, isHost, + lastGameAt, }: User) => { return { userId, loginId, displayName, isHost, + lastGameAt: lastGameAt?.toISOString() || null, }; }; diff --git a/src/utils/guard.ts b/src/utils/guard.ts index 59d213f..a2ba592 100644 --- a/src/utils/guard.ts +++ b/src/utils/guard.ts @@ -35,7 +35,8 @@ export const GameInputGuard = RecordType({ yakuman: ArrayType(ArrayType(YakumanGuard)), }) ), -}).withConstraint(({ gameType, userScores }) => { + createdAt: StringType.optional(), +}).withConstraint(({ gameType, userScores, createdAt }) => { const { players, totalScore, winds } = gameTypes[gameType]; if (userScores.length !== players) { return false; @@ -52,5 +53,8 @@ export const GameInputGuard = RecordType({ if (Array.from(playerWinds).some((wind) => !winds.includes(wind))) { return false; } + if (createdAt && Number.isNaN(new Date(createdAt).getTime())) { + return false; + } return true; });