Skip to content

Commit 96c3e38

Browse files
committed
Add Guandan support and UI/layout polish
Introduce Guandan game support across backend, game logic, AI and frontend. - DB: add TABLES for doudizhu/guandan, create both tables at init, expose TABLES and tableFor, and pass gameType when recording and fetching scores. - Server: wire up GuandanSuggest, add gameType-aware scoring/seat logic, new timeout constants for play/step, call recordResultToDb for Guandan, and make /api/score endpoints accept gameType and include it in responses. - Game logic: refactor guandan-game sequence checks (SEQUENCE_ORDER, generic canBuildUnitSequence) and simplify pair/triple/straight builders. - Frontend: update static assets and layout (style.css and index.html) — responsive layout tweaks, new UI components (landscape lock, stats panel, guandan board), adjusted timers/placements, and bumped style.css version. Add new static/js/guandan-suggest.js (AI suggestion for Guandan). These changes enable Guandan play, persist separate scores, improve AI suggestions and refine UI/responsiveness.
1 parent dd82e56 commit 96c3e38

7 files changed

Lines changed: 1145 additions & 269 deletions

File tree

db.js

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* 斗地主积分持久化(与 Discuz 共库)
2+
* 雀阁分玩法积分持久化(与 Discuz 共库)
33
*
44
* 配置(环境变量):
55
* DB_HOST 数据库主机(默认 127.0.0.1)
@@ -10,11 +10,15 @@
1010
* DB_TABLE_PREFIX Discuz 表前缀(默认 pre_)
1111
* DB_DISABLE 设为 1 时关闭持久化(仅内存)
1212
*
13-
* 启动时会自动建表(如果不存在):<前缀>doudizhu_score
13+
* 启动时会自动建表(如果不存在):<前缀>doudizhu_score / <前缀>guandan_score
1414
*/
1515

1616
const TABLE_PREFIX = process.env.DB_TABLE_PREFIX || 'pre_';
17-
const TABLE = `${TABLE_PREFIX}doudizhu_score`;
17+
const TABLES = {
18+
doudizhu: `${TABLE_PREFIX}doudizhu_score`,
19+
guandan: `${TABLE_PREFIX}guandan_score`,
20+
};
21+
const TABLE = TABLES.doudizhu;
1822

1923
let pool = null;
2024
let ready = false;
@@ -46,8 +50,9 @@ async function init() {
4650
connectionLimit: 5,
4751
charset: 'utf8mb4',
4852
});
49-
await pool.query(`
50-
CREATE TABLE IF NOT EXISTS \`${TABLE}\` (
53+
for (const tableName of Object.values(TABLES)) {
54+
await pool.query(`
55+
CREATE TABLE IF NOT EXISTS \`${tableName}\` (
5156
\`uid\` INT UNSIGNED NOT NULL PRIMARY KEY,
5257
\`username\` VARCHAR(64) NOT NULL DEFAULT '',
5358
\`score\` INT NOT NULL DEFAULT 0,
@@ -60,8 +65,9 @@ async function init() {
6065
KEY \`idx_score\` (\`score\`)
6166
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
6267
`);
68+
}
6369
ready = true;
64-
console.log(`[db] 已连接 MySQL,使用表 ${TABLE}`);
70+
console.log(`[db] 已连接 MySQL,使用表 ${Object.values(TABLES).join(' / ')}`);
6571
return true;
6672
} catch (err) {
6773
console.error('[db] 初始化失败:', err && err.message);
@@ -72,6 +78,9 @@ async function init() {
7278
}
7379

7480
function isReady() { return ready && pool; }
81+
function tableFor(gameType) {
82+
return TABLES[gameType] || TABLES.doudizhu;
83+
}
7584

7685
/**
7786
* 记录一名玩家的对局结果。
@@ -84,14 +93,15 @@ function isReady() { return ready && pool; }
8493
*/
8594
async function recordPlayer(p) {
8695
if (!isReady() || !p || !p.uid) return;
96+
const tableName = tableFor(p.gameType);
8797
const now = Math.floor(Date.now() / 1000);
8898
const win = p.win ? 1 : 0;
8999
const loss = p.win ? 0 : 1;
90100
const lord = p.isLandlord ? 1 : 0;
91101
const lordWin = (p.isLandlord && p.win) ? 1 : 0;
92102
try {
93103
await pool.query(
94-
`INSERT INTO \`${TABLE}\`
104+
`INSERT INTO \`${tableName}\`
95105
(uid, username, score, games, wins, losses, landlord_games, landlord_wins, updated_at)
96106
VALUES (?, ?, ?, 1, ?, ?, ?, ?, ?)
97107
ON DUPLICATE KEY UPDATE
@@ -110,12 +120,13 @@ async function recordPlayer(p) {
110120
}
111121
}
112122

113-
async function getUserScore(uid) {
123+
async function getUserScore(uid, gameType = 'doudizhu') {
114124
if (!isReady() || !uid) return null;
125+
const tableName = tableFor(gameType);
115126
try {
116127
const [rows] = await pool.query(
117128
`SELECT uid, username, score, games, wins, losses, landlord_games, landlord_wins
118-
FROM \`${TABLE}\` WHERE uid = ? LIMIT 1`,
129+
FROM \`${tableName}\` WHERE uid = ? LIMIT 1`,
119130
[uid]
120131
);
121132
return rows[0] || { uid, username: '', score: 0, games: 0, wins: 0, losses: 0, landlord_games: 0, landlord_wins: 0 };
@@ -125,13 +136,14 @@ async function getUserScore(uid) {
125136
}
126137
}
127138

128-
async function getTopScores(limit = 20) {
139+
async function getTopScores(limit = 20, gameType = 'doudizhu') {
129140
if (!isReady()) return [];
141+
const tableName = tableFor(gameType);
130142
try {
131143
const n = Math.max(1, limit | 0);
132144
const [rows] = await pool.query(
133145
`SELECT uid, username, score, games, wins, losses
134-
FROM \`${TABLE}\` ORDER BY score DESC, wins DESC LIMIT ?`,
146+
FROM \`${tableName}\` ORDER BY score DESC, wins DESC LIMIT ?`,
135147
[n]
136148
);
137149
return rows;
@@ -141,4 +153,4 @@ async function getTopScores(limit = 20) {
141153
}
142154
}
143155

144-
module.exports = { init, isReady, recordPlayer, getUserScore, getTopScores, TABLE };
156+
module.exports = { init, isReady, recordPlayer, getUserScore, getTopScores, TABLE, TABLES };

guandan-game.js

Lines changed: 19 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const HEART_TYPE = 2;
2+
const SEQUENCE_ORDER = [14, 15, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
23

34
function randomIndex(max) {
45
return Math.floor(Math.random() * (max + 1));
@@ -60,15 +61,6 @@ function sameSuit(cards) {
6061
return cards.every(card => card.type === cards[0].type && card.value < 16);
6162
}
6263

63-
function isSequence(values) {
64-
const arr = values.slice().sort((a, b) => a - b);
65-
if (arr.some(v => v >= 15)) return false;
66-
for (let i = 1; i < arr.length; i++) {
67-
if (arr[i] !== arr[i - 1] + 1) return false;
68-
}
69-
return true;
70-
}
71-
7264
function canBuildSameKind(cards, wildCount, size) {
7365
const normals = cards.filter(c => !c.__wild);
7466
const counts = countValues(normals);
@@ -81,66 +73,33 @@ function canBuildSameKind(cards, wildCount, size) {
8173
return { ok: counts[v] + wildCount >= size, key: v };
8274
}
8375

84-
function canBuildPairsSequence(cards, wildCount, pairCount) {
76+
function canBuildUnitSequence(cards, wildCount, unitSize, unitCount) {
8577
const normals = cards.filter(c => !c.__wild);
8678
const counts = countValues(normals);
87-
const values = Object.keys(counts).map(Number).filter(v => v < 15);
88-
if (!values.length) return { ok: false };
89-
const min = Math.min.apply(Math, values);
90-
const max = Math.max.apply(Math, values);
91-
for (let start = Math.max(3, max - pairCount + 1); start <= min; start++) {
92-
const seq = [];
93-
for (let i = 0; i < pairCount; i++) seq.push(start + i);
79+
const values = Object.keys(counts).map(Number);
80+
if (normals.some(c => c.value >= 16)) return { ok: false };
81+
if (values.some(v => counts[v] > unitSize)) return { ok: false };
82+
for (let start = 0; start <= SEQUENCE_ORDER.length - unitCount; start++) {
83+
const seq = SEQUENCE_ORDER.slice(start, start + unitCount);
9484
let need = 0;
95-
let valid = true;
96-
seq.forEach(v => {
97-
if (v >= 15) valid = false;
98-
need += Math.max(0, 2 - (counts[v] || 0));
99-
});
100-
if (valid && need <= wildCount && values.every(v => seq.includes(v))) {
85+
seq.forEach(v => { need += Math.max(0, unitSize - (counts[v] || 0)); });
86+
if (need <= wildCount && values.every(v => seq.includes(v))) {
10187
return { ok: true, key: start };
10288
}
10389
}
10490
return { ok: false };
10591
}
10692

93+
function canBuildPairsSequence(cards, wildCount, pairCount) {
94+
return canBuildUnitSequence(cards, wildCount, 2, pairCount);
95+
}
96+
10797
function canBuildTriplesSequence(cards, wildCount, tripleCount) {
108-
const normals = cards.filter(c => !c.__wild);
109-
const counts = countValues(normals);
110-
const values = Object.keys(counts).map(Number).filter(v => v < 15);
111-
if (!values.length) return { ok: false };
112-
const min = Math.min.apply(Math, values);
113-
const max = Math.max.apply(Math, values);
114-
for (let start = Math.max(3, max - tripleCount + 1); start <= min; start++) {
115-
const seq = [];
116-
for (let i = 0; i < tripleCount; i++) seq.push(start + i);
117-
let need = 0;
118-
let valid = true;
119-
seq.forEach(v => {
120-
if (v >= 15) valid = false;
121-
need += Math.max(0, 3 - (counts[v] || 0));
122-
});
123-
if (valid && need <= wildCount && values.every(v => seq.includes(v))) {
124-
return { ok: true, key: start };
125-
}
126-
}
127-
return { ok: false };
98+
return canBuildUnitSequence(cards, wildCount, 3, tripleCount);
12899
}
129100

130101
function canBuildStraight(cards, wildCount, len) {
131-
const normals = cards.filter(c => !c.__wild);
132-
const values = normals.map(c => c.value).filter(v => v < 15);
133-
if (values.length !== new Set(values).size) return { ok: false };
134-
if (normals.some(c => c.value >= 15)) return { ok: false };
135-
for (let start = 3; start <= 14 - len + 1; start++) {
136-
const seq = [];
137-
for (let i = 0; i < len; i++) seq.push(start + i);
138-
const miss = seq.filter(v => !values.includes(v)).length;
139-
if (miss <= wildCount && values.every(v => seq.includes(v))) {
140-
return { ok: true, key: start };
141-
}
142-
}
143-
return { ok: false };
102+
return canBuildUnitSequence(cards, wildCount, 1, len);
144103
}
145104

146105
function analyze(cards, levelRank) {
@@ -200,13 +159,13 @@ function analyze(cards, levelRank) {
200159
}
201160
}
202161

203-
if (len >= 6 && len % 2 === 0) {
204-
const pairs = canBuildPairsSequence(cloned, wildCount, len / 2);
162+
if (len === 6) {
163+
const pairs = canBuildPairsSequence(cloned, wildCount, 3);
205164
if (pairs.ok) return { status: true, len, key: pairs.key, type: 'PAIRS_SEQ' };
206165
}
207166

208-
if (len >= 6 && len % 3 === 0) {
209-
const triples = canBuildTriplesSequence(cloned, wildCount, len / 3);
167+
if (len === 6) {
168+
const triples = canBuildTriplesSequence(cloned, wildCount, 2);
210169
if (triples.ok) return { status: true, len, key: triples.key, type: 'TRIPLES_SEQ' };
211170
}
212171

0 commit comments

Comments
 (0)