Spaces:
Running
Running
Upload 52 files
Browse files- api.js +74 -0
- bot.js +18 -1
- games/WebCardGame.js +184 -0
- handlers/webCards.js +96 -0
- webapp/cards.html +225 -0
- webapp/index.html +3 -0
api.js
CHANGED
|
@@ -299,6 +299,80 @@ app.post('/carrom/shoot', auth, async (req, res) => {
|
|
| 299 |
}
|
| 300 |
});
|
| 301 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
// ββ Compat: GET /daily/status ββ
|
| 303 |
app.get('/daily/status', auth, async (req, res) => {
|
| 304 |
const userId = req.tgUser.id.toString();
|
|
|
|
| 299 |
}
|
| 300 |
});
|
| 301 |
|
| 302 |
+
// ββ Card Web Game API ββ
|
| 303 |
+
app.get('/wcards/state', auth, async (req, res) => {
|
| 304 |
+
try {
|
| 305 |
+
const { gameId } = req.query;
|
| 306 |
+
const sessions = require('./games/sessions');
|
| 307 |
+
const game = sessions.get(gameId);
|
| 308 |
+
if (!game || !game.getState) return res.json({ ok: false, error: 'Game not found' });
|
| 309 |
+
res.json({ ok: true, state: game.getState(req.tgUser.id.toString()) });
|
| 310 |
+
} catch (err) { console.error('[API] wcards/state:', err); res.status(500).json({ ok: false, error: 'Error' }); }
|
| 311 |
+
});
|
| 312 |
+
|
| 313 |
+
app.post('/wcards/start', auth, async (req, res) => {
|
| 314 |
+
try {
|
| 315 |
+
const { gameId } = req.body;
|
| 316 |
+
const sessions = require('./games/sessions');
|
| 317 |
+
const game = sessions.get(gameId);
|
| 318 |
+
if (!game) return res.json({ ok: false, error: 'Game not found' });
|
| 319 |
+
if (game.hostId !== req.tgUser.id.toString()) return res.json({ ok: false, error: 'Only host' });
|
| 320 |
+
if (!game.start()) return res.json({ ok: false, error: 'Need 2+ players' });
|
| 321 |
+
if (game._timer) { clearTimeout(game._timer); game._timer = null; }
|
| 322 |
+
if (game._autoStart) { clearTimeout(game._autoStart); game._autoStart = null; }
|
| 323 |
+
// Charge
|
| 324 |
+
for (const p of game.players) {
|
| 325 |
+
let u = await User.findOne({ userId: p.userId }) || await User.create({ userId: p.userId });
|
| 326 |
+
u.wallet -= game.bet; await u.save();
|
| 327 |
+
}
|
| 328 |
+
res.json({ ok: true, state: game.getState(req.tgUser.id.toString()) });
|
| 329 |
+
} catch (err) { console.error('[API] wcards/start:', err); res.status(500).json({ ok: false, error: 'Error' }); }
|
| 330 |
+
});
|
| 331 |
+
|
| 332 |
+
app.post('/wcards/play', auth, async (req, res) => {
|
| 333 |
+
try {
|
| 334 |
+
const { gameId, cardIndex } = req.body;
|
| 335 |
+
const sessions = require('./games/sessions');
|
| 336 |
+
const leveling = require('./utils/leveling');
|
| 337 |
+
const game = sessions.get(gameId);
|
| 338 |
+
if (!game) return res.json({ ok: false, error: 'Game not found' });
|
| 339 |
+
const userId = req.tgUser.id.toString();
|
| 340 |
+
const result = game.playCard(userId, cardIndex);
|
| 341 |
+
if (result.error) return res.json({ ok: false, error: result.error });
|
| 342 |
+
|
| 343 |
+
// If game ended, pay out
|
| 344 |
+
if (game.status === 'ended') {
|
| 345 |
+
const winners = game.getWinners();
|
| 346 |
+
const pot = game.bet * game.players.length;
|
| 347 |
+
const share = Math.floor(pot / winners.length);
|
| 348 |
+
for (const w of winners) {
|
| 349 |
+
let u = await User.findOne({ userId: w.userId }) || await User.create({ userId: w.userId });
|
| 350 |
+
u.wallet += share;
|
| 351 |
+
await leveling.addXP(w.userId, 150);
|
| 352 |
+
await u.save();
|
| 353 |
+
}
|
| 354 |
+
const wNames = winners.map(w => w.name).join(' & ');
|
| 355 |
+
if (global._tgClient && game.chatId) {
|
| 356 |
+
try { await global._tgClient.sendMessage(game.chatId, { message: `π <b>CARD GAME β OVER!</b>\n\nπ ${wNames} wins <b>$${pot}</b>!` }); } catch (e) {}
|
| 357 |
+
}
|
| 358 |
+
setTimeout(() => sessions.delete(gameId), 5000);
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
res.json({ ok: true, state: game.getState(userId) });
|
| 362 |
+
} catch (err) { console.error('[API] wcards/play:', err); res.status(500).json({ ok: false, error: 'Error' }); }
|
| 363 |
+
});
|
| 364 |
+
|
| 365 |
+
app.post('/wcards/next', auth, async (req, res) => {
|
| 366 |
+
try {
|
| 367 |
+
const { gameId } = req.body;
|
| 368 |
+
const sessions = require('./games/sessions');
|
| 369 |
+
const game = sessions.get(gameId);
|
| 370 |
+
if (!game) return res.json({ ok: false, error: 'Game not found' });
|
| 371 |
+
game.nextRound();
|
| 372 |
+
res.json({ ok: true, state: game.getState(req.tgUser.id.toString()) });
|
| 373 |
+
} catch (err) { res.status(500).json({ ok: false, error: 'Error' }); }
|
| 374 |
+
});
|
| 375 |
+
|
| 376 |
// ββ Compat: GET /daily/status ββ
|
| 377 |
app.get('/daily/status', auth, async (req, res) => {
|
| 378 |
const userId = req.tgUser.id.toString();
|
bot.js
CHANGED
|
@@ -28,6 +28,7 @@ const strategy = require('./handlers/strategyGames');
|
|
| 28 |
const shopHandler = require('./handlers/shop');
|
| 29 |
const admin = require('./handlers/admin');
|
| 30 |
const carrom = require('./handlers/carrom');
|
|
|
|
| 31 |
|
| 32 |
const apiId = parseInt(process.env.API_ID || 0);
|
| 33 |
const apiHash = process.env.API_HASH || "";
|
|
@@ -126,6 +127,16 @@ const MP_FALLBACK = "β οΈ This command only works in groups! Add me to a group
|
|
| 126 |
const startParam = parts[1] || '';
|
| 127 |
|
| 128 |
// Handle startapp deep links (carrom, daily, mission, etc.)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
if (startParam.startsWith('carrom_')) {
|
| 130 |
// Send mini app button to open carrom game
|
| 131 |
const webUrl = getWebAppUrl() + '/carrom?tgWebAppStartParam=' + startParam;
|
|
@@ -171,7 +182,8 @@ const MP_FALLBACK = "β οΈ This command only works in groups! Add me to a group
|
|
| 171 |
"π² /dice <bet> β Dice duel\n" +
|
| 172 |
"π― /carrom <bet> β Carrom Pool\n" +
|
| 173 |
"π /hack <bet> <len> β PIN hack\n" +
|
| 174 |
-
"π /cards <bet> β Card flip
|
|
|
|
| 175 |
"<b>π° Solo Casino</b>\n" +
|
| 176 |
"/bj <bet> β Blackjack\n" +
|
| 177 |
"/slots β Slots | /mines β Mines\n" +
|
|
@@ -236,6 +248,7 @@ const MP_FALLBACK = "β οΈ This command only works in groups! Add me to a group
|
|
| 236 |
if (cmdStart("/xox", text)) { if (requireGroup(event) && await isAlive(event)) await boardGames.startXOX(client, event); }
|
| 237 |
if (cmdStart("/c4", text)) { if (requireGroup(event) && await isAlive(event)) await boardGames.startC4(client, event); }
|
| 238 |
if (cmdStart("/dice", text)) { if (requireGroup(event) && await isAlive(event)) await games.startDice(client, event); }
|
|
|
|
| 239 |
if (cmdStart("/carrom", text)) { if (requireGroup(event) && await isAlive(event)) await carrom.startCarrom(client, event); }
|
| 240 |
if (cmdStart("/cards", text)) { if (requireGroup(event) && await isAlive(event)) await cardGame.initCardGame(client, event); }
|
| 241 |
if (cmdStart("/hack", text)) { if (requireGroup(event) && await isAlive(event)) await multiHack.initHack(client, event); }
|
|
@@ -260,6 +273,10 @@ const MP_FALLBACK = "β οΈ This command only works in groups! Add me to a group
|
|
| 260 |
let handled = false;
|
| 261 |
const cbData = update.data.toString();
|
| 262 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
if (cbData.startsWith("crmjn|") || cbData.startsWith("crmst|")) {
|
| 264 |
try { await carrom.handleCarromCallback(client, update); } catch (e) { console.error('[CB] carrom:', e.message); }
|
| 265 |
handled = true;
|
|
|
|
| 28 |
const shopHandler = require('./handlers/shop');
|
| 29 |
const admin = require('./handlers/admin');
|
| 30 |
const carrom = require('./handlers/carrom');
|
| 31 |
+
const webCards = require('./handlers/webCards');
|
| 32 |
|
| 33 |
const apiId = parseInt(process.env.API_ID || 0);
|
| 34 |
const apiHash = process.env.API_HASH || "";
|
|
|
|
| 127 |
const startParam = parts[1] || '';
|
| 128 |
|
| 129 |
// Handle startapp deep links (carrom, daily, mission, etc.)
|
| 130 |
+
if (startParam.startsWith('wcards_')) {
|
| 131 |
+
const webUrl = getWebAppUrl() + '/cards.html?tgWebAppStartParam=' + startParam;
|
| 132 |
+
await client.sendMessage(event.chatId, {
|
| 133 |
+
message: "π <b>Card Game</b>\n\nClick below to open!",
|
| 134 |
+
buttons: new Api.ReplyInlineMarkup({ rows: [new Api.KeyboardButtonRow({ buttons: [
|
| 135 |
+
new Api.KeyboardButtonWebView({ text: "π Open Cards", url: webUrl })
|
| 136 |
+
]})] })
|
| 137 |
+
});
|
| 138 |
+
return;
|
| 139 |
+
}
|
| 140 |
if (startParam.startsWith('carrom_')) {
|
| 141 |
// Send mini app button to open carrom game
|
| 142 |
const webUrl = getWebAppUrl() + '/carrom?tgWebAppStartParam=' + startParam;
|
|
|
|
| 182 |
"π² /dice <bet> β Dice duel\n" +
|
| 183 |
"π― /carrom <bet> β Carrom Pool\n" +
|
| 184 |
"π /hack <bet> <len> β PIN hack\n" +
|
| 185 |
+
"π /cards <bet> β Card flip (text)\n" +
|
| 186 |
+
"π /wcards <bet> β Card game (Mini App)\n\n" +
|
| 187 |
"<b>π° Solo Casino</b>\n" +
|
| 188 |
"/bj <bet> β Blackjack\n" +
|
| 189 |
"/slots β Slots | /mines β Mines\n" +
|
|
|
|
| 248 |
if (cmdStart("/xox", text)) { if (requireGroup(event) && await isAlive(event)) await boardGames.startXOX(client, event); }
|
| 249 |
if (cmdStart("/c4", text)) { if (requireGroup(event) && await isAlive(event)) await boardGames.startC4(client, event); }
|
| 250 |
if (cmdStart("/dice", text)) { if (requireGroup(event) && await isAlive(event)) await games.startDice(client, event); }
|
| 251 |
+
if (cmdStart("/wcards", text)) { if (requireGroup(event) && await isAlive(event)) await webCards.startWebCards(client, event); }
|
| 252 |
if (cmdStart("/carrom", text)) { if (requireGroup(event) && await isAlive(event)) await carrom.startCarrom(client, event); }
|
| 253 |
if (cmdStart("/cards", text)) { if (requireGroup(event) && await isAlive(event)) await cardGame.initCardGame(client, event); }
|
| 254 |
if (cmdStart("/hack", text)) { if (requireGroup(event) && await isAlive(event)) await multiHack.initHack(client, event); }
|
|
|
|
| 273 |
let handled = false;
|
| 274 |
const cbData = update.data.toString();
|
| 275 |
|
| 276 |
+
if (cbData.startsWith("wcjn|")) {
|
| 277 |
+
try { await webCards.handleWebCardsCallback(client, update); } catch (e) { console.error('[CB] wcards:', e.message); }
|
| 278 |
+
handled = true;
|
| 279 |
+
}
|
| 280 |
if (cbData.startsWith("crmjn|") || cbData.startsWith("crmst|")) {
|
| 281 |
try { await carrom.handleCarromCallback(client, update); } catch (e) { console.error('[CB] carrom:', e.message); }
|
| 282 |
handled = true;
|
games/WebCardGame.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Web Card Game β Server-side state for Mini App
|
| 3 |
+
* Classic card flip: each player has 4 cards, play 1 per round, highest wins
|
| 4 |
+
* 4 rounds total, most round wins = game winner
|
| 5 |
+
*/
|
| 6 |
+
class WebCardGame {
|
| 7 |
+
constructor(bet) {
|
| 8 |
+
this.bet = bet;
|
| 9 |
+
this.players = []; // [{userId, name}]
|
| 10 |
+
this.status = 'lobby'; // lobby, playing, reveal, ended
|
| 11 |
+
this.hands = {}; // {userId: [{label, val, suit, played}]}
|
| 12 |
+
this.round = 0;
|
| 13 |
+
this.roundPlays = {}; // {userId: {label, val, suit}}
|
| 14 |
+
this.roundResults = []; // [{winner, plays}]
|
| 15 |
+
this.scores = {}; // {userId: roundsWon}
|
| 16 |
+
this.hostId = null;
|
| 17 |
+
this.chatId = null;
|
| 18 |
+
this._timer = null;
|
| 19 |
+
this._autoStart = null;
|
| 20 |
+
this._revealTimer = null;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
addPlayer(userId, name) {
|
| 24 |
+
if (this.players.length >= 6 || this.players.some(p => p.userId === userId)) return false;
|
| 25 |
+
this.players.push({ userId, name });
|
| 26 |
+
this.scores[userId] = 0;
|
| 27 |
+
if (!this.hostId) this.hostId = userId;
|
| 28 |
+
return true;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
start() {
|
| 32 |
+
if (this.players.length < 2) return false;
|
| 33 |
+
this.status = 'playing';
|
| 34 |
+
this.round = 1;
|
| 35 |
+
this.dealCards();
|
| 36 |
+
return true;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
dealCards() {
|
| 40 |
+
const suits = ['β ', 'β₯', 'β¦', 'β£'];
|
| 41 |
+
const labels = ['2','3','4','5','6','7','8','9','10','J','Q','K','A'];
|
| 42 |
+
const valMap = {'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9,'10':10,'J':11,'Q':12,'K':13,'A':14};
|
| 43 |
+
|
| 44 |
+
// Build a deck
|
| 45 |
+
const deck = [];
|
| 46 |
+
for (const s of suits)
|
| 47 |
+
for (const l of labels)
|
| 48 |
+
deck.push({ label: l, val: valMap[l], suit: s, played: false });
|
| 49 |
+
|
| 50 |
+
// Shuffle
|
| 51 |
+
for (let i = deck.length - 1; i > 0; i--) {
|
| 52 |
+
const j = Math.floor(Math.random() * (i + 1));
|
| 53 |
+
[deck[i], deck[j]] = [deck[j], deck[i]];
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// Deal 4 cards to each player
|
| 57 |
+
this.hands = {};
|
| 58 |
+
let idx = 0;
|
| 59 |
+
for (const p of this.players) {
|
| 60 |
+
this.hands[p.userId] = [];
|
| 61 |
+
for (let i = 0; i < 4; i++) {
|
| 62 |
+
this.hands[p.userId].push(deck[idx++]);
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
this.roundPlays = {};
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
playCard(userId, cardIndex) {
|
| 69 |
+
if (this.status !== 'playing') return { error: 'Game not active' };
|
| 70 |
+
if (this.roundPlays[userId]) return { error: 'Already played this round' };
|
| 71 |
+
const hand = this.hands[userId];
|
| 72 |
+
if (!hand) return { error: 'Not in game' };
|
| 73 |
+
if (cardIndex < 0 || cardIndex >= hand.length) return { error: 'Invalid card' };
|
| 74 |
+
if (hand[cardIndex].played) return { error: 'Card already used' };
|
| 75 |
+
|
| 76 |
+
hand[cardIndex].played = true;
|
| 77 |
+
this.roundPlays[userId] = {
|
| 78 |
+
label: hand[cardIndex].label,
|
| 79 |
+
val: hand[cardIndex].val,
|
| 80 |
+
suit: hand[cardIndex].suit,
|
| 81 |
+
cardIndex
|
| 82 |
+
};
|
| 83 |
+
|
| 84 |
+
// Check if all players have played
|
| 85 |
+
if (Object.keys(this.roundPlays).length >= this.players.length) {
|
| 86 |
+
this.resolveRound();
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
return { ok: true };
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
resolveRound() {
|
| 93 |
+
this.status = 'reveal';
|
| 94 |
+
|
| 95 |
+
// Find highest card
|
| 96 |
+
let maxVal = -1, winners = [];
|
| 97 |
+
for (const [uid, play] of Object.entries(this.roundPlays)) {
|
| 98 |
+
if (play.val > maxVal) { maxVal = play.val; winners = [uid]; }
|
| 99 |
+
else if (play.val === maxVal) { winners.push(uid); }
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
for (const w of winners) this.scores[w] = (this.scores[w] || 0) + 1;
|
| 103 |
+
|
| 104 |
+
this.roundResults.push({
|
| 105 |
+
round: this.round,
|
| 106 |
+
plays: { ...this.roundPlays },
|
| 107 |
+
winners
|
| 108 |
+
});
|
| 109 |
+
|
| 110 |
+
// Auto-advance after reveal delay (handled by client polling)
|
| 111 |
+
this.round++;
|
| 112 |
+
if (this.round > 4) {
|
| 113 |
+
this.status = 'ended';
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
nextRound() {
|
| 118 |
+
if (this.status === 'reveal') {
|
| 119 |
+
this.status = 'playing';
|
| 120 |
+
this.roundPlays = {};
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
getWinners() {
|
| 125 |
+
let maxScore = -1, winners = [];
|
| 126 |
+
for (const p of this.players) {
|
| 127 |
+
const s = this.scores[p.userId] || 0;
|
| 128 |
+
if (s > maxScore) { maxScore = s; winners = [p]; }
|
| 129 |
+
else if (s === maxScore) { winners.push(p); }
|
| 130 |
+
}
|
| 131 |
+
return winners;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
getState(forUserId) {
|
| 135 |
+
const myHand = this.hands[forUserId]?.map((c, i) => ({
|
| 136 |
+
label: c.label, suit: c.suit, val: c.val, played: c.played, index: i
|
| 137 |
+
})) || [];
|
| 138 |
+
|
| 139 |
+
// Other players' played cards: show face-down until reveal
|
| 140 |
+
const tablePlays = {};
|
| 141 |
+
for (const [uid, play] of Object.entries(this.roundPlays)) {
|
| 142 |
+
if (this.status === 'reveal' || this.status === 'ended') {
|
| 143 |
+
tablePlays[uid] = { label: play.label, suit: play.suit, val: play.val, faceUp: true };
|
| 144 |
+
} else {
|
| 145 |
+
tablePlays[uid] = { faceUp: false }; // face down
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
const lastResult = this.roundResults.length > 0 ? this.roundResults[this.roundResults.length - 1] : null;
|
| 150 |
+
let lastResultFormatted = null;
|
| 151 |
+
if (lastResult) {
|
| 152 |
+
lastResultFormatted = {
|
| 153 |
+
round: lastResult.round,
|
| 154 |
+
plays: {},
|
| 155 |
+
winnerNames: lastResult.winners.map(uid => this.players.find(p => p.userId === uid)?.name || '?')
|
| 156 |
+
};
|
| 157 |
+
for (const [uid, play] of Object.entries(lastResult.plays)) {
|
| 158 |
+
const pName = this.players.find(p => p.userId === uid)?.name || '?';
|
| 159 |
+
lastResultFormatted.plays[pName] = { label: play.label, suit: play.suit };
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
return {
|
| 164 |
+
status: this.status,
|
| 165 |
+
players: this.players.map(p => ({
|
| 166 |
+
name: p.name,
|
| 167 |
+
isMe: p.userId === forUserId,
|
| 168 |
+
score: this.scores[p.userId] || 0,
|
| 169 |
+
hasPlayed: !!this.roundPlays[p.userId]
|
| 170 |
+
})),
|
| 171 |
+
round: this.round,
|
| 172 |
+
totalRounds: 4,
|
| 173 |
+
myHand,
|
| 174 |
+
iPlayed: !!this.roundPlays[forUserId],
|
| 175 |
+
tablePlays,
|
| 176 |
+
lastResult: lastResultFormatted,
|
| 177 |
+
isHost: this.hostId === forUserId,
|
| 178 |
+
winner: this.status === 'ended' ? this.getWinners().map(p => p.name) : null,
|
| 179 |
+
playerCount: this.players.length,
|
| 180 |
+
};
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
module.exports = WebCardGame;
|
handlers/webCards.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { Api } = require("telegram");
|
| 2 |
+
const crypto = require('crypto');
|
| 3 |
+
const User = require('../models/User');
|
| 4 |
+
const WebCardGame = require('../games/WebCardGame');
|
| 5 |
+
const sessions = require('../games/sessions');
|
| 6 |
+
const { getName } = require('../utils/getName');
|
| 7 |
+
const { getBotUsername } = require('../utils/miniAppButton');
|
| 8 |
+
|
| 9 |
+
function playUrl(gameId) {
|
| 10 |
+
return `https://t.me/${getBotUsername()}?startapp=wcards_${gameId.replace(/:/g, '_0_')}`;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
const startWebCards = async (client, event) => {
|
| 14 |
+
const bet = Math.max(50, parseInt(event.message.message.split(" ")[1]) || 100);
|
| 15 |
+
const userId = event.message.senderId.toString();
|
| 16 |
+
let user = await User.findOne({ userId }) || await User.create({ userId });
|
| 17 |
+
if (user.wallet < bet) return event.message.respond({ message: "Insufficient funds!" });
|
| 18 |
+
|
| 19 |
+
const gameId = `wc:${event.chatId}:${crypto.randomBytes(4).toString('hex')}`;
|
| 20 |
+
const name = await getName(client, userId);
|
| 21 |
+
const game = new WebCardGame(bet);
|
| 22 |
+
game.addPlayer(userId, name);
|
| 23 |
+
game.chatId = event.chatId.toString();
|
| 24 |
+
sessions.set(gameId, game);
|
| 25 |
+
|
| 26 |
+
await client.sendMessage(event.chatId, {
|
| 27 |
+
message: `π <b>CARD GAME</b>\nBet: $${bet} each\n\nπ€ ${name} (host)\nβ³ Waiting for players (2-6)...\n\n<i>Open the Mini App to see your cards!</i>`,
|
| 28 |
+
buttons: new Api.ReplyInlineMarkup({ rows: [
|
| 29 |
+
new Api.KeyboardButtonRow({ buttons: [
|
| 30 |
+
new Api.KeyboardButtonCallback({ text: `βοΈ Join ($${bet})`, data: Buffer.from(`wcjn|${gameId}`) })
|
| 31 |
+
] }),
|
| 32 |
+
new Api.KeyboardButtonRow({ buttons: [
|
| 33 |
+
new Api.KeyboardButtonUrl({ text: "π Open Game", url: playUrl(gameId) })
|
| 34 |
+
] })
|
| 35 |
+
] })
|
| 36 |
+
});
|
| 37 |
+
|
| 38 |
+
game._timer = setTimeout(async () => {
|
| 39 |
+
const g = sessions.get(gameId);
|
| 40 |
+
if (!g || g.status !== 'lobby') return;
|
| 41 |
+
if (g.players.length >= 2) { g.start(); /* auto-start */ }
|
| 42 |
+
else { sessions.delete(gameId); try { await client.sendMessage(event.chatId, { message: `π Lobby expired.` }); } catch (e) {} }
|
| 43 |
+
}, 120000);
|
| 44 |
+
};
|
| 45 |
+
|
| 46 |
+
const handleWebCardsCallback = async (client, update) => {
|
| 47 |
+
const data = update.data.toString();
|
| 48 |
+
const userId = update.userId.toString();
|
| 49 |
+
|
| 50 |
+
if (data.startsWith("wcjn|")) {
|
| 51 |
+
const gameId = data.split("|")[1];
|
| 52 |
+
const game = sessions.get(gameId);
|
| 53 |
+
if (!game || game.status !== 'lobby') { await alert(client, update, "Expired!"); return; }
|
| 54 |
+
if (game.players.some(p => p.userId === userId)) { await alert(client, update, "Already in!"); return; }
|
| 55 |
+
if (game.players.length >= 6) { await alert(client, update, "Full!"); return; }
|
| 56 |
+
let user = await User.findOne({ userId }) || await User.create({ userId });
|
| 57 |
+
if (user.wallet < game.bet) { await alert(client, update, "Insufficient funds!"); return; }
|
| 58 |
+
game.addPlayer(userId, await getName(client, userId));
|
| 59 |
+
|
| 60 |
+
if (game.players.length >= 2 && !game._autoStart) {
|
| 61 |
+
game._autoStart = setTimeout(async () => {
|
| 62 |
+
const g = sessions.get(gameId);
|
| 63 |
+
if (g && g.status === 'lobby' && g.players.length >= 2) g.start();
|
| 64 |
+
}, 60000);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
const pl = game.players.map((p, i) => `π€ ${p.name}${i === 0 ? ' (host)' : ''}`).join('\n');
|
| 68 |
+
try {
|
| 69 |
+
await client.editMessage(update.peer, {
|
| 70 |
+
message: update.msgId,
|
| 71 |
+
text: `π <b>CARD GAME</b>\nBet: $${game.bet} each\n\n${pl}\n\nβ
${game.players.length} players β Host can start!`,
|
| 72 |
+
parseMode: 'html',
|
| 73 |
+
buttons: new Api.ReplyInlineMarkup({ rows: [
|
| 74 |
+
new Api.KeyboardButtonRow({ buttons: [
|
| 75 |
+
new Api.KeyboardButtonCallback({ text: `βοΈ Join ($${game.bet})`, data: Buffer.from(`wcjn|${gameId}`) })
|
| 76 |
+
] }),
|
| 77 |
+
new Api.KeyboardButtonRow({ buttons: [
|
| 78 |
+
new Api.KeyboardButtonUrl({ text: "π Open Game", url: playUrl(gameId) })
|
| 79 |
+
] })
|
| 80 |
+
] })
|
| 81 |
+
});
|
| 82 |
+
} catch (e) {}
|
| 83 |
+
await alert(client, update, "Joined!");
|
| 84 |
+
return;
|
| 85 |
+
}
|
| 86 |
+
await ack(client, update);
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
async function alert(client, update, text) {
|
| 90 |
+
try { await client.invoke(new Api.messages.SetBotCallbackAnswer({ queryId: update.queryId, message: text, alert: true, cacheTime: 1 })); } catch (e) {}
|
| 91 |
+
}
|
| 92 |
+
async function ack(client, update) {
|
| 93 |
+
try { await client.invoke(new Api.messages.SetBotCallbackAnswer({ queryId: update.queryId, cacheTime: 1 })); } catch (e) {}
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
module.exports = { startWebCards, handleWebCardsCallback };
|
webapp/cards.html
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
| 6 |
+
<title>Card Game</title>
|
| 7 |
+
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
| 8 |
+
<style>
|
| 9 |
+
*{margin:0;padding:0;box-sizing:border-box;}
|
| 10 |
+
body{background:#0a1628;color:#fff;font-family:-apple-system,sans-serif;min-height:100vh;display:flex;flex-direction:column;overflow-x:hidden;}
|
| 11 |
+
#blocked{display:none;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;text-align:center;padding:40px;} #blocked a{color:#3b82f6;}
|
| 12 |
+
#app{display:none;flex:1;flex-direction:column;}
|
| 13 |
+
#hdr{padding:10px;text-align:center;font-size:13px;}
|
| 14 |
+
#hdr b{color:#f59e0b;}
|
| 15 |
+
/* Players bar */
|
| 16 |
+
#players{display:flex;justify-content:center;gap:8px;padding:4px 10px;flex-wrap:wrap;}
|
| 17 |
+
.pbadge{padding:3px 10px;border-radius:8px;font-size:11px;font-weight:600;border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.03);}
|
| 18 |
+
.pbadge.turn{border-color:#f59e0b;background:rgba(245,158,11,.1);}
|
| 19 |
+
.pbadge.played{border-color:#22c55e;background:rgba(34,197,94,.1);}
|
| 20 |
+
/* Table β center area */
|
| 21 |
+
#table{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:10px;min-height:200px;position:relative;}
|
| 22 |
+
#round-info{font-size:14px;font-weight:700;margin-bottom:12px;}
|
| 23 |
+
#table-cards{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;min-height:100px;align-items:center;}
|
| 24 |
+
.tcard{width:65px;height:95px;border-radius:8px;display:flex;flex-direction:column;align-items:center;justify-content:center;font-weight:700;position:relative;transition:transform .3s,box-shadow .3s;}
|
| 25 |
+
.tcard.facedown{background:linear-gradient(135deg,#1e40af,#1e3a8a);border:2px solid #3b82f6;box-shadow:0 4px 12px rgba(59,130,246,.2);}
|
| 26 |
+
.tcard.facedown::after{content:'π ';font-size:32px;}
|
| 27 |
+
.tcard.faceup{background:#fff;color:#1a1a1a;border:2px solid #ddd;box-shadow:0 4px 15px rgba(0,0,0,.3);animation:flipIn .4s ease;}
|
| 28 |
+
.tcard.faceup .tsuit{font-size:24px;}
|
| 29 |
+
.tcard.faceup .tlabel{font-size:18px;font-weight:800;}
|
| 30 |
+
.tcard.faceup.red{color:#dc2626;}
|
| 31 |
+
.tcard.faceup.winner{border-color:#f59e0b;box-shadow:0 0 20px rgba(245,158,11,.4);}
|
| 32 |
+
.tcard-name{font-size:9px;color:#888;text-align:center;margin-top:2px;max-width:65px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
| 33 |
+
@keyframes flipIn{0%{transform:rotateY(90deg) scale(.8);}100%{transform:rotateY(0) scale(1);}}
|
| 34 |
+
/* Result banner */
|
| 35 |
+
#result{display:none;text-align:center;padding:10px;font-size:14px;}
|
| 36 |
+
#result b{color:#f59e0b;}
|
| 37 |
+
/* My hand β bottom */
|
| 38 |
+
#hand-area{padding:10px 8px 20px;background:linear-gradient(to top,rgba(0,0,0,.3),transparent);}
|
| 39 |
+
#hand-label{text-align:center;font-size:11px;color:#666;margin-bottom:6px;}
|
| 40 |
+
#hand{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;}
|
| 41 |
+
.hcard{width:72px;height:105px;border-radius:10px;background:#fff;color:#1a1a1a;display:flex;flex-direction:column;align-items:center;justify-content:center;cursor:pointer;transition:transform .2s,box-shadow .2s,opacity .2s;border:2px solid #e5e7eb;position:relative;user-select:none;-webkit-user-select:none;}
|
| 42 |
+
.hcard:active{transform:translateY(-8px) scale(1.05);}
|
| 43 |
+
.hcard.red{color:#dc2626;}
|
| 44 |
+
.hcard .hsuit{font-size:28px;}
|
| 45 |
+
.hcard .hlabel{font-size:20px;font-weight:800;}
|
| 46 |
+
.hcard.used{opacity:.25;cursor:default;transform:none !important;}
|
| 47 |
+
.hcard.selected{transform:translateY(-12px);border-color:#f59e0b;box-shadow:0 8px 25px rgba(245,158,11,.3);}
|
| 48 |
+
/* Buttons */
|
| 49 |
+
#actions{display:flex;justify-content:center;gap:8px;padding:6px;}
|
| 50 |
+
.abtn{padding:10px 24px;border:none;border-radius:10px;font-size:13px;font-weight:700;cursor:pointer;color:#fff;transition:.2s;}
|
| 51 |
+
.abtn.green{background:#22c55e;} .abtn.blue{background:#3b82f6;}
|
| 52 |
+
.abtn:disabled{opacity:.3;cursor:not-allowed;}
|
| 53 |
+
/* Lobby */
|
| 54 |
+
#lobby{display:none;flex:1;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:30px;}
|
| 55 |
+
#lobby h2{margin:10px 0 6px;}
|
| 56 |
+
.lplayers{margin:16px 0;font-size:14px;line-height:1.8;}
|
| 57 |
+
.lstbtn{padding:12px 36px;border:none;border-radius:12px;font-size:15px;font-weight:700;cursor:pointer;color:#fff;background:linear-gradient(135deg,#22c55e,#16a34a);margin:6px;}
|
| 58 |
+
.lstbtn:disabled{opacity:.3;cursor:not-allowed;}
|
| 59 |
+
</style>
|
| 60 |
+
</head>
|
| 61 |
+
<body>
|
| 62 |
+
<div id="blocked"><div style="font-size:48px;margin-bottom:16px;">π</div><h2>Access Blocked</h2><p>Open from <a href="https://t.me/Alexagamebot">@Alexagamebot</a> via <b>/wcards</b></p></div>
|
| 63 |
+
<div id="app" style="display:none;">
|
| 64 |
+
<div id="hdr">π <b>Card Game</b></div>
|
| 65 |
+
<div id="players"></div>
|
| 66 |
+
<div id="lobby"><div style="font-size:42px;">π</div><h2>Card Game</h2><div id="linfo"></div><div class="lplayers" id="lplayers"></div><button class="lstbtn" id="lstbtn" onclick="hostStart()" disabled>π Start</button><div style="font-size:11px;color:#555;margin-top:8px;" id="lwait"></div></div>
|
| 67 |
+
<div id="table"><div id="round-info"></div><div id="table-cards"></div></div>
|
| 68 |
+
<div id="result"></div>
|
| 69 |
+
<div id="actions"></div>
|
| 70 |
+
<div id="hand-area"><div id="hand-label">Your Cards</div><div id="hand"></div></div>
|
| 71 |
+
</div>
|
| 72 |
+
<script>
|
| 73 |
+
const tg=window.Telegram?.WebApp;if(tg){tg.expand();tg.ready();}
|
| 74 |
+
if(!tg?.initData||!tg.initDataUnsafe?.user?.id){document.getElementById('blocked').style.display='flex';throw 0;}
|
| 75 |
+
document.getElementById('app').style.display='flex';
|
| 76 |
+
const API=location.origin;
|
| 77 |
+
const sp=new URLSearchParams(location.search).get('tgWebAppStartParam')||tg.initDataUnsafe?.start_param||'';
|
| 78 |
+
const GID=sp.replace('wcards_','').replace(/_0_/g,':');
|
| 79 |
+
async function rpc(m,p,b){try{const o={method:m,headers:{'X-Init-Data':tg.initData,'Content-Type':'application/json'}};if(b)o.body=JSON.stringify(b);const r=await fetch(API+p,o);return JSON.parse(await r.text());}catch{return{ok:false,error:'Network error'};}}
|
| 80 |
+
|
| 81 |
+
let S=null,selectedCard=null,pollId=null;
|
| 82 |
+
|
| 83 |
+
function isRed(suit){return suit==='β₯'||suit==='β¦';}
|
| 84 |
+
|
| 85 |
+
function renderLobby(){
|
| 86 |
+
document.getElementById('lobby').style.display='flex';
|
| 87 |
+
document.getElementById('table').style.display='none';
|
| 88 |
+
document.getElementById('hand-area').style.display='none';
|
| 89 |
+
document.getElementById('actions').innerHTML='';
|
| 90 |
+
document.getElementById('result').style.display='none';
|
| 91 |
+
let pl='';S.players.forEach((p,i)=>pl+=`<div>${p.isMe?'β':'π€'} ${p.name}${i===0?' (Host)':''} β ${p.score} pts</div>`);
|
| 92 |
+
document.getElementById('lplayers').innerHTML=pl;
|
| 93 |
+
document.getElementById('linfo').textContent=`$${S.players.length>0?S.players[0].score:0} bet β’ ${S.playerCount} players`;
|
| 94 |
+
const btn=document.getElementById('lstbtn');
|
| 95 |
+
if(S.isHost){btn.style.display='';btn.disabled=S.playerCount<2;btn.textContent=S.playerCount<2?'Need 2+ players':'π Start Game';}
|
| 96 |
+
else{btn.style.display='';btn.disabled=true;btn.textContent='Waiting for host...';}
|
| 97 |
+
document.getElementById('lwait').textContent=S.isHost?'You are host':'Host will start the game';
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
function renderGame(){
|
| 101 |
+
document.getElementById('lobby').style.display='none';
|
| 102 |
+
document.getElementById('table').style.display='flex';
|
| 103 |
+
document.getElementById('hand-area').style.display='block';
|
| 104 |
+
|
| 105 |
+
// Players bar
|
| 106 |
+
let pb='';
|
| 107 |
+
S.players.forEach(p=>{
|
| 108 |
+
let cls='pbadge';
|
| 109 |
+
if(p.hasPlayed)cls+=' played';
|
| 110 |
+
pb+=`<span class="${cls}">${p.isMe?'β':''} ${p.name}: ${p.score}</span>`;
|
| 111 |
+
});
|
| 112 |
+
document.getElementById('players').innerHTML=pb;
|
| 113 |
+
|
| 114 |
+
// Round info
|
| 115 |
+
if(S.status==='playing')document.getElementById('round-info').textContent=`Round ${Math.min(S.round,S.totalRounds)} / ${S.totalRounds}`;
|
| 116 |
+
else if(S.status==='reveal')document.getElementById('round-info').textContent=`Round ${S.round-1} β Reveal!`;
|
| 117 |
+
else document.getElementById('round-info').textContent='';
|
| 118 |
+
|
| 119 |
+
// Table cards
|
| 120 |
+
let tc='';
|
| 121 |
+
S.players.forEach(p=>{
|
| 122 |
+
const play=S.tablePlays[Object.keys(S.tablePlays).find(k=>S.players.find(pp=>pp.name===p.name))]||null;
|
| 123 |
+
// Find play by checking if this player played
|
| 124 |
+
const uid=Object.keys(S.tablePlays).find(k=>{
|
| 125 |
+
// We don't have uids in tablePlays from client perspective, match by order
|
| 126 |
+
return false;
|
| 127 |
+
});
|
| 128 |
+
});
|
| 129 |
+
// Simpler: just show all table plays
|
| 130 |
+
if(Object.keys(S.tablePlays).length>0){
|
| 131 |
+
for(const [uid,play] of Object.entries(S.tablePlays)){
|
| 132 |
+
if(play.faceUp){
|
| 133 |
+
const r=isRed(play.suit)?'red':'';
|
| 134 |
+
const isWin=S.lastResult?.winnerNames?.some(n=>true)||false; // simplified
|
| 135 |
+
tc+=`<div><div class="tcard faceup ${r}"><span class="tsuit">${play.suit}</span><span class="tlabel">${play.label}</span></div></div>`;
|
| 136 |
+
} else {
|
| 137 |
+
tc+=`<div><div class="tcard facedown"></div></div>`;
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
} else {
|
| 141 |
+
tc='<div style="color:#444;font-size:13px;">Play a card from your hand</div>';
|
| 142 |
+
}
|
| 143 |
+
document.getElementById('table-cards').innerHTML=tc;
|
| 144 |
+
|
| 145 |
+
// Result
|
| 146 |
+
const res=document.getElementById('result');
|
| 147 |
+
if(S.status==='reveal'&&S.lastResult){
|
| 148 |
+
res.style.display='block';
|
| 149 |
+
const plays=Object.entries(S.lastResult.plays).map(([n,c])=>`${n}: ${c.suit}${c.label}`).join(' | ');
|
| 150 |
+
res.innerHTML=`<b>Round ${S.lastResult.round}:</b> ${plays}<br>π <b>${S.lastResult.winnerNames.join(', ')}</b> wins!`;
|
| 151 |
+
} else if(S.status==='ended'&&S.winner){
|
| 152 |
+
res.style.display='block';
|
| 153 |
+
res.innerHTML=`π <b>GAME OVER!</b><br>${S.winner.join(' & ')} wins!`;
|
| 154 |
+
} else { res.style.display='none'; }
|
| 155 |
+
|
| 156 |
+
// Actions
|
| 157 |
+
let acts='';
|
| 158 |
+
if(S.status==='playing'&&!S.iPlayed&&selectedCard!==null){
|
| 159 |
+
acts=`<button class="abtn green" onclick="playCard()">βΆοΈ Play Card</button>`;
|
| 160 |
+
}
|
| 161 |
+
if(S.status==='reveal'){
|
| 162 |
+
acts=`<button class="abtn blue" onclick="nextRound()">Next Round β</button>`;
|
| 163 |
+
}
|
| 164 |
+
document.getElementById('actions').innerHTML=acts;
|
| 165 |
+
|
| 166 |
+
// Hand
|
| 167 |
+
let h='';
|
| 168 |
+
if(S.myHand){
|
| 169 |
+
S.myHand.forEach((c,i)=>{
|
| 170 |
+
const r=isRed(c.suit)?'red':'';
|
| 171 |
+
const used=c.played?'used':'';
|
| 172 |
+
const sel=selectedCard===i?'selected':'';
|
| 173 |
+
h+=`<div class="hcard ${r} ${used} ${sel}" onclick="selectCard(${i},${c.played})" data-i="${i}"><span class="hsuit">${c.suit}</span><span class="hlabel">${c.label}</span></div>`;
|
| 174 |
+
});
|
| 175 |
+
}
|
| 176 |
+
document.getElementById('hand').innerHTML=h;
|
| 177 |
+
document.getElementById('hand-label').textContent=S.iPlayed?'β
Card played β waiting for others':'Tap a card to select, then Play';
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
function selectCard(i,used){
|
| 181 |
+
if(used||S.iPlayed||S.status!=='playing')return;
|
| 182 |
+
selectedCard=selectedCard===i?null:i;
|
| 183 |
+
renderGame();
|
| 184 |
+
if(tg.HapticFeedback)tg.HapticFeedback.selectionChanged();
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
async function playCard(){
|
| 188 |
+
if(selectedCard===null)return;
|
| 189 |
+
const r=await rpc('POST','/wcards/play',{gameId:GID,cardIndex:selectedCard});
|
| 190 |
+
if(r.ok){S=r.state;selectedCard=null;renderGame();if(tg.HapticFeedback)tg.HapticFeedback.notificationOccurred('success');}
|
| 191 |
+
else{alert(r.error);}
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
async function nextRound(){
|
| 195 |
+
const r=await rpc('POST','/wcards/next',{gameId:GID});
|
| 196 |
+
if(r.ok){S=r.state;selectedCard=null;renderGame();}
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
async function hostStart(){
|
| 200 |
+
document.getElementById('lstbtn').disabled=true;
|
| 201 |
+
const r=await rpc('POST','/wcards/start',{gameId:GID});
|
| 202 |
+
if(r.ok){S=r.state;render();}
|
| 203 |
+
else{document.getElementById('lwait').textContent=r.error;document.getElementById('lstbtn').disabled=false;}
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
function render(){
|
| 207 |
+
if(!S)return;
|
| 208 |
+
if(S.status==='lobby')renderLobby();
|
| 209 |
+
else renderGame();
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
async function poll(){
|
| 213 |
+
const r=await rpc('GET',`/wcards/state?gameId=${encodeURIComponent(GID)}`);
|
| 214 |
+
if(r.ok){S=r.state;render();if(S.status==='ended'){clearInterval(pollId);pollId=null;}}
|
| 215 |
+
else document.getElementById('hdr').innerHTML='π '+r.error;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
async function init(){
|
| 219 |
+
if(!GID||GID===sp){document.getElementById('hdr').innerHTML='π No game. Use /wcards in group.';return;}
|
| 220 |
+
await poll();pollId=setInterval(poll,1000);
|
| 221 |
+
}
|
| 222 |
+
init();
|
| 223 |
+
</script>
|
| 224 |
+
</body>
|
| 225 |
+
</html>
|
webapp/index.html
CHANGED
|
@@ -14,6 +14,9 @@
|
|
| 14 |
// Fallback: check TG WebApp start_param
|
| 15 |
if (!sp) try { sp = window.Telegram.WebApp.initDataUnsafe.start_param || ''; } catch(e){}
|
| 16 |
// Route to game pages
|
|
|
|
|
|
|
|
|
|
| 17 |
if (sp.startsWith('carrom_')) {
|
| 18 |
window.location.replace('carrom.html?tgWebAppStartParam=' + sp);
|
| 19 |
}
|
|
|
|
| 14 |
// Fallback: check TG WebApp start_param
|
| 15 |
if (!sp) try { sp = window.Telegram.WebApp.initDataUnsafe.start_param || ''; } catch(e){}
|
| 16 |
// Route to game pages
|
| 17 |
+
if (sp.startsWith('wcards_')) {
|
| 18 |
+
window.location.replace('cards.html?tgWebAppStartParam=' + sp);
|
| 19 |
+
}
|
| 20 |
if (sp.startsWith('carrom_')) {
|
| 21 |
window.location.replace('carrom.html?tgWebAppStartParam=' + sp);
|
| 22 |
}
|