hansaka01 commited on
Commit
c6fb155
Β·
verified Β·
1 Parent(s): fc49a2d

Upload 52 files

Browse files
Files changed (6) hide show
  1. api.js +74 -0
  2. bot.js +18 -1
  3. games/WebCardGame.js +184 -0
  4. handlers/webCards.js +96 -0
  5. webapp/cards.html +225 -0
  6. 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 &lt;bet&gt; β€” Dice duel\n" +
172
  "🎯 /carrom &lt;bet&gt; β€” Carrom Pool\n" +
173
  "πŸ”“ /hack &lt;bet&gt; &lt;len&gt; β€” PIN hack\n" +
174
- "πŸƒ /cards &lt;bet&gt; β€” Card flip game\n\n" +
 
175
  "<b>🎰 Solo Casino</b>\n" +
176
  "/bj &lt;bet&gt; β€” 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 &lt;bet&gt; β€” Dice duel\n" +
183
  "🎯 /carrom &lt;bet&gt; β€” Carrom Pool\n" +
184
  "πŸ”“ /hack &lt;bet&gt; &lt;len&gt; β€” PIN hack\n" +
185
+ "πŸƒ /cards &lt;bet&gt; β€” Card flip (text)\n" +
186
+ "πŸƒ /wcards &lt;bet&gt; β€” Card game (Mini App)\n\n" +
187
  "<b>🎰 Solo Casino</b>\n" +
188
  "/bj &lt;bet&gt; β€” 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
  }