Skip to content

Commit cfaebc5

Browse files
committed
Reapply "refactor: feature-based architecture with clean layer separation"
This reverts commit 443af99.
1 parent 7a67ba5 commit cfaebc5

48 files changed

Lines changed: 5282 additions & 8389 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
File renamed without changes.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
sqliteTable,
99
text,
1010
} from "drizzle-orm/sqlite-core";
11-
import type { Hai } from "../hai/types";
11+
import type { Hai } from "@/types/hai";
1212
import { user } from "./auth-schema";
1313

1414
export * from "./auth-schema";

app/features/auth/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { betterAuth } from "better-auth";
2+
import { drizzleAdapter } from "better-auth/adapters/drizzle";
3+
import { anonymous } from "better-auth/plugins";
4+
import { getDB } from "@/db";
5+
import * as schema from "@/db/schema";
6+
7+
export function getAuth(env: Env) {
8+
const auth = betterAuth({
9+
baseURL: env.BETTER_AUTH_URL,
10+
database: drizzleAdapter(getDB(env), {
11+
provider: "sqlite",
12+
schema: schema,
13+
}),
14+
plugins: [anonymous()],
15+
});
16+
return auth;
17+
}

app/features/game/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export { default as judgeAgari } from "@/lib/agari";
2+
export { sortTehai } from "@/lib/hai";
3+
export { getAgariScoreDelta } from "@/lib/score";
4+
export { calculateShanten } from "@/lib/shanten";
5+
export type * from "@/types/game";
6+
export { TILE_IMAGE_PATHS, TOTAL_TSUMO_PER_KYOKU } from "./constants";
7+
export * from "./service";

app/features/game/repository.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { desc, eq, notInArray, sql } from "drizzle-orm";
2+
import type { DrizzleD1Database } from "drizzle-orm/d1";
3+
import { gameState, haiyama, kyoku } from "@/db/schema";
4+
import type { Hai } from "@/types/hai";
5+
6+
export type GameStateValues = {
7+
kyoku: number;
8+
junme: number;
9+
remainTsumo: number;
10+
score: number;
11+
haiyama: Hai[];
12+
sutehai: Hai[];
13+
tehai: Hai[];
14+
tsumohai: Hai[];
15+
haiyamaId: string | null;
16+
};
17+
18+
export type KyokuInsert = {
19+
userId: string;
20+
haiyamaId: string;
21+
didAgari: boolean;
22+
agariJunme?: number | null;
23+
shanten: number;
24+
scoreDelta: number;
25+
};
26+
27+
export async function fetchGameState(db: DrizzleD1Database, userId: string) {
28+
const result = await db
29+
.select()
30+
.from(gameState)
31+
.where(eq(gameState.userId, userId))
32+
.get();
33+
return result ?? null;
34+
}
35+
36+
export async function upsertGameState(
37+
db: DrizzleD1Database,
38+
userId: string,
39+
values: GameStateValues,
40+
) {
41+
await db
42+
.insert(gameState)
43+
.values({ userId, ...values })
44+
.onConflictDoUpdate({ target: gameState.userId, set: values });
45+
}
46+
47+
export async function patchGameState(
48+
db: DrizzleD1Database,
49+
userId: string,
50+
values: Partial<GameStateValues>,
51+
) {
52+
await db.update(gameState).set(values).where(eq(gameState.userId, userId));
53+
}
54+
55+
export async function removeGameState(db: DrizzleD1Database, userId: string) {
56+
await db.delete(gameState).where(eq(gameState.userId, userId));
57+
}
58+
59+
export async function fetchPlayedHaiyamaIds(
60+
db: DrizzleD1Database,
61+
userId: string,
62+
): Promise<string[]> {
63+
const rows = await db
64+
.select({ haiyamaId: kyoku.haiyamaId })
65+
.from(kyoku)
66+
.where(eq(kyoku.userId, userId));
67+
return rows.map((r) => r.haiyamaId);
68+
}
69+
70+
export async function fetchRandomHaiyama(
71+
db: DrizzleD1Database,
72+
excludeIds: string[] = [],
73+
) {
74+
if (excludeIds.length > 0) {
75+
return db
76+
.select()
77+
.from(haiyama)
78+
.where(notInArray(haiyama.id, excludeIds))
79+
.orderBy(sql`RANDOM()`)
80+
.limit(1);
81+
}
82+
return db.select().from(haiyama).orderBy(sql`RANDOM()`).limit(1);
83+
}
84+
85+
export async function clearKyokuByUser(db: DrizzleD1Database, userId: string) {
86+
await db.delete(kyoku).where(eq(kyoku.userId, userId));
87+
}
88+
89+
export async function insertKyokuRecord(
90+
db: DrizzleD1Database,
91+
values: KyokuInsert,
92+
) {
93+
await db.insert(kyoku).values(values);
94+
}
95+
96+
export async function refreshHaiyamaStats(
97+
db: DrizzleD1Database,
98+
haiyamaId: string,
99+
) {
100+
await db
101+
.update(haiyama)
102+
.set({
103+
avgAgariJunme: sql`COALESCE(
104+
(SELECT AVG(${kyoku.agariJunme}) FROM ${kyoku} WHERE ${kyoku.haiyamaId} = ${haiyama.id}),
105+
0
106+
)`,
107+
})
108+
.where(eq(haiyama.id, haiyamaId));
109+
}
110+
111+
export async function fetchKyokuByUser(db: DrizzleD1Database, userId: string) {
112+
return db
113+
.select()
114+
.from(kyoku)
115+
.where(eq(kyoku.userId, userId))
116+
.orderBy(desc(kyoku.createdAt));
117+
}
118+
119+
export async function fetchAllUserScoreTotals(db: DrizzleD1Database) {
120+
return db
121+
.select({
122+
userId: kyoku.userId,
123+
totalScore: sql<number>`25000 + coalesce(sum(${kyoku.scoreDelta}), 0)`,
124+
})
125+
.from(kyoku)
126+
.groupBy(kyoku.userId)
127+
.orderBy(desc(sql<number>`25000 + coalesce(sum(${kyoku.scoreDelta}), 0)`));
128+
}
129+
130+
export async function fetchKyokuStatsByHaiyama(db: DrizzleD1Database) {
131+
return db
132+
.select({
133+
haiyamaId: kyoku.haiyamaId,
134+
playedCount: sql<number>`count(*)`,
135+
agariCount: sql<number>`coalesce(sum(case when ${kyoku.didAgari} then 1 else 0 end), 0)`,
136+
})
137+
.from(kyoku)
138+
.groupBy(kyoku.haiyamaId);
139+
}

0 commit comments

Comments
 (0)