trucoshi 12.0.2 → 12.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/events.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { SocketError } from "./server";
2
- import { EMatchState, IAdminCreateChestRewardCodeInput, IAdminCreateChestRewardCodeResult, IAdminDashboard, IAdminSetNoticeBannerInput, IAdminSetNoticeBannerResult, IAccountDetails, CardSkinId, ICard, IChatMessage, IEquippedDeck, ILobbyOptions, IInventoryCardGroup, IMatchDetails, IPlayerRanking, IPublicChatRoom, IPublicMatch, IPublicMatchInfo, IPublicMatchStats, IJoinQueueOptions, IQueueMatchFound, IQueueStatus, IRewardCodeRedeemResult, IPublicNoticeBanner, ITreasureOpenResult, ITreasureStatus, ITrucoshiStats, IUserData, IWaitingPlayData, IWaitingSayData } from "./types";
2
+ import { EMatchState, IAdminCreateChestRewardCodeInput, IAdminCreateChestRewardCodeResult, IAdminDashboard, IAdminSetNoticeBannerInput, IAdminSetNoticeBannerResult, IAccountDetails, CardSkinId, ICard, IChatMessage, IEquippedDeck, ILobbyOptions, IInventoryCardGroup, IMatchDetails, IPlayerRanking, IPublicChatRoom, IPublicMatch, IPublicMatchInfo, IPublicMatchStats, IJoinQueueOptions, IQueueMatchFound, IQueueMatchCancelled, IQueueMatchStarting, IQueueReadyUpdate, IQueueStatus, IRewardCodeRedeemResult, IPublicNoticeBanner, ITreasureOpenResult, ITreasureStatus, ITrucoshiStats, IUserData, IWaitingPlayData, IWaitingSayData } from "./types";
3
3
  export type IEventCallback<T = {}> = (args: {
4
4
  success: boolean;
5
5
  error?: SocketError;
@@ -23,6 +23,9 @@ export declare enum EServerEvent {
23
23
  UPDATE_STATS = "UPDATE_STATS",
24
24
  QUEUE_UPDATE = "QUEUE_UPDATE",
25
25
  QUEUE_MATCH_FOUND = "QUEUE_MATCH_FOUND",
26
+ QUEUE_READY_UPDATE = "QUEUE_READY_UPDATE",
27
+ QUEUE_MATCH_STARTING = "QUEUE_MATCH_STARTING",
28
+ QUEUE_MATCH_CANCELLED = "QUEUE_MATCH_CANCELLED",
26
29
  UPDATE_NOTICE_BANNER = "UPDATE_NOTICE_BANNER"
27
30
  }
28
31
  export interface ServerToClientEvents {
@@ -31,6 +34,9 @@ export interface ServerToClientEvents {
31
34
  [EServerEvent.UPDATE_STATS]: (room: ITrucoshiStats) => void;
32
35
  [EServerEvent.QUEUE_UPDATE]: (status: IQueueStatus) => void;
33
36
  [EServerEvent.QUEUE_MATCH_FOUND]: (match: IQueueMatchFound) => void;
37
+ [EServerEvent.QUEUE_READY_UPDATE]: (update: IQueueReadyUpdate) => void;
38
+ [EServerEvent.QUEUE_MATCH_STARTING]: (starting: IQueueMatchStarting) => void;
39
+ [EServerEvent.QUEUE_MATCH_CANCELLED]: (cancelled: IQueueMatchCancelled) => void;
34
40
  [EServerEvent.UPDATE_NOTICE_BANNER]: (noticeBanner: IPublicNoticeBanner | null) => void;
35
41
  [EServerEvent.NEW_MESSAGE]: (roomId: string, message?: IChatMessage) => void;
36
42
  [EServerEvent.UPDATE_ACTIVE_MATCHES]: (activeMatches: IPublicMatchInfo[]) => void;
@@ -50,6 +56,7 @@ export declare enum EClientEvent {
50
56
  LOGOUT = "LOGOUT",
51
57
  LEAVE_MATCH = "LEAVE_MATCH",
52
58
  CREATE_MATCH = "CREATE_MATCH",
59
+ CREATE_TUTORIAL_MATCH = "CREATE_TUTORIAL_MATCH",
53
60
  FETCH_ACCOUNT_DETAILS = "FETCH_ACCOUNT_DETAILS",
54
61
  FETCH_MATCH_DETAILS = "FETCH_MATCH_DETAILS",
55
62
  SET_MATCH_OPTIONS = "SET_MATCH_OPTIONS",
@@ -62,6 +69,8 @@ export declare enum EClientEvent {
62
69
  PLAY_AGAIN = "PLAY_AGAIN",
63
70
  JOIN_QUEUE = "JOIN_QUEUE",
64
71
  LEAVE_QUEUE = "LEAVE_QUEUE",
72
+ CONFIRM_QUEUE_MATCH = "CONFIRM_QUEUE_MATCH",
73
+ DECLINE_QUEUE_MATCH = "DECLINE_QUEUE_MATCH",
65
74
  FETCH_INVENTORY = "FETCH_INVENTORY",
66
75
  SET_DECK_CARD_SKIN = "SET_DECK_CARD_SKIN",
67
76
  FETCH_TREASURE_STATUS = "FETCH_TREASURE_STATUS",
@@ -95,6 +104,10 @@ export interface ClientToServerEvents {
95
104
  match?: IPublicMatch;
96
105
  activeMatches?: IPublicMatchInfo[];
97
106
  }>) => void;
107
+ [EClientEvent.CREATE_TUTORIAL_MATCH]: (tutorialId: string | null | undefined, callback: IEventCallback<{
108
+ match?: IPublicMatch;
109
+ activeMatches?: IPublicMatchInfo[];
110
+ }>) => void;
98
111
  [EClientEvent.SET_MATCH_OPTIONS]: (matchSessionId: string, options: Partial<ILobbyOptions>, callback: IEventCallback<{
99
112
  match?: IPublicMatch;
100
113
  activeMatches?: IPublicMatchInfo[];
@@ -125,6 +138,10 @@ export interface ClientToServerEvents {
125
138
  status?: IQueueStatus;
126
139
  }>) => void;
127
140
  [EClientEvent.LEAVE_QUEUE]: (callback?: IEventCallback<{}>) => void;
141
+ [EClientEvent.CONFIRM_QUEUE_MATCH]: (proposalId: string, callback?: IEventCallback<{
142
+ update?: IQueueReadyUpdate;
143
+ }>) => void;
144
+ [EClientEvent.DECLINE_QUEUE_MATCH]: (proposalId: string, callback?: IEventCallback<{}>) => void;
128
145
  [EClientEvent.FETCH_INVENTORY]: (callback: IEventCallback<{
129
146
  inventory: IInventoryCardGroup[];
130
147
  equippedDeck: IEquippedDeck;
package/dist/events.js CHANGED
@@ -18,6 +18,9 @@ export var EServerEvent;
18
18
  EServerEvent["UPDATE_STATS"] = "UPDATE_STATS";
19
19
  EServerEvent["QUEUE_UPDATE"] = "QUEUE_UPDATE";
20
20
  EServerEvent["QUEUE_MATCH_FOUND"] = "QUEUE_MATCH_FOUND";
21
+ EServerEvent["QUEUE_READY_UPDATE"] = "QUEUE_READY_UPDATE";
22
+ EServerEvent["QUEUE_MATCH_STARTING"] = "QUEUE_MATCH_STARTING";
23
+ EServerEvent["QUEUE_MATCH_CANCELLED"] = "QUEUE_MATCH_CANCELLED";
21
24
  EServerEvent["UPDATE_NOTICE_BANNER"] = "UPDATE_NOTICE_BANNER";
22
25
  })(EServerEvent || (EServerEvent = {}));
23
26
  export var EClientEvent;
@@ -25,6 +28,7 @@ export var EClientEvent;
25
28
  EClientEvent["LOGOUT"] = "LOGOUT";
26
29
  EClientEvent["LEAVE_MATCH"] = "LEAVE_MATCH";
27
30
  EClientEvent["CREATE_MATCH"] = "CREATE_MATCH";
31
+ EClientEvent["CREATE_TUTORIAL_MATCH"] = "CREATE_TUTORIAL_MATCH";
28
32
  EClientEvent["FETCH_ACCOUNT_DETAILS"] = "FETCH_ACCOUNT_DETAILS";
29
33
  EClientEvent["FETCH_MATCH_DETAILS"] = "FETCH_MATCH_DETAILS";
30
34
  EClientEvent["SET_MATCH_OPTIONS"] = "SET_MATCH_OPTIONS";
@@ -37,6 +41,8 @@ export var EClientEvent;
37
41
  EClientEvent["PLAY_AGAIN"] = "PLAY_AGAIN";
38
42
  EClientEvent["JOIN_QUEUE"] = "JOIN_QUEUE";
39
43
  EClientEvent["LEAVE_QUEUE"] = "LEAVE_QUEUE";
44
+ EClientEvent["CONFIRM_QUEUE_MATCH"] = "CONFIRM_QUEUE_MATCH";
45
+ EClientEvent["DECLINE_QUEUE_MATCH"] = "DECLINE_QUEUE_MATCH";
40
46
  EClientEvent["FETCH_INVENTORY"] = "FETCH_INVENTORY";
41
47
  EClientEvent["SET_DECK_CARD_SKIN"] = "SET_DECK_CARD_SKIN";
42
48
  EClientEvent["FETCH_TREASURE_STATUS"] = "FETCH_TREASURE_STATUS";
@@ -6,6 +6,6 @@ export declare function dealCards<TPlayer extends {
6
6
  key: string;
7
7
  idx: number;
8
8
  setHand(h: Array<ICard>): void;
9
- } = IPlayer>(table: ITable<TPlayer>, deck: IDeck): void;
9
+ } = IPlayer>(table: ITable<TPlayer>, deck: IDeck, fixedHandsByPlayerIdx?: Record<string, ICard[]>): void;
10
10
  export declare function shuffleArray<T = unknown>(array: Array<T>, getRandom?: (max: number) => number): Array<T>;
11
11
  export declare function PlayedCard(player: IPlayer | IPublicPlayer, card: ICard, burn?: boolean): IPlayedCard;
@@ -38,11 +38,54 @@ export function Deck() {
38
38
  return deck;
39
39
  }
40
40
  export const getAllCards = () => Object.keys(CARDS);
41
- export function dealCards(table, deck) {
41
+ const normalizeFixedHands = (table, fixedHandsByPlayerIdx) => {
42
+ if (!fixedHandsByPlayerIdx) {
43
+ return null;
44
+ }
45
+ const fixedHands = new Map();
46
+ const usedCards = new Set();
47
+ const playersByIdx = new Set(table.players.map((player) => player.idx));
48
+ for (const [playerIdxText, cards] of Object.entries(fixedHandsByPlayerIdx)) {
49
+ const playerIdx = Number(playerIdxText);
50
+ if (!Number.isInteger(playerIdx) || !playersByIdx.has(playerIdx)) {
51
+ throw new Error(`Invalid fixed hand player index: ${playerIdxText}`);
52
+ }
53
+ if (!Array.isArray(cards) || cards.length !== 3) {
54
+ throw new Error(`Fixed hand for player ${playerIdx} must contain exactly 3 cards`);
55
+ }
56
+ for (const card of cards) {
57
+ if (!(card in CARDS)) {
58
+ throw new Error(`Invalid fixed hand card: ${card}`);
59
+ }
60
+ if (usedCards.has(card)) {
61
+ throw new Error(`Duplicate fixed hand card: ${card}`);
62
+ }
63
+ usedCards.add(card);
64
+ }
65
+ fixedHands.set(playerIdx, cards);
66
+ }
67
+ return fixedHands;
68
+ };
69
+ export function dealCards(table, deck, fixedHandsByPlayerIdx) {
42
70
  const cheat_lots_of_flowers = process.env.APP_CHEAT_LOTS_OF_FLOWERS_FOR_TESTING === "1";
43
71
  const cheat_cards = process.env.APP_CHEAT_CARDS || "";
44
72
  const playerHands = [];
45
73
  const players = table.getPlayersForehandFirst();
74
+ const fixedHands = normalizeFixedHands(table, fixedHandsByPlayerIdx);
75
+ if (fixedHands) {
76
+ for (const player of table.players) {
77
+ const fixedHand = fixedHands.get(player.idx);
78
+ playerHands[player.idx] = fixedHand
79
+ ? fixedHand.map((card) => deck.pick(card) || (() => {
80
+ throw new Error(`Fixed hand card is not available in deck: ${card}`);
81
+ })())
82
+ : deck.takeThree();
83
+ }
84
+ for (const player of table.players) {
85
+ player.setHand(playerHands[player.idx]);
86
+ }
87
+ return;
88
+ }
46
89
  for (let i = 0; i < 3; i++) {
47
90
  for (const player of players) {
48
91
  playerHands[player.idx] = [...(playerHands[player.idx] || []), deck.takeCard()];
@@ -70,8 +113,8 @@ export function dealCards(table, deck) {
70
113
  playerHands[player.idx] = deck.takeThree();
71
114
  }
72
115
  }
73
- for (const [key, player] of table.players.entries()) {
74
- player.setHand(playerHands[key]);
116
+ for (const player of table.players) {
117
+ player.setHand(playerHands[player.idx]);
75
118
  }
76
119
  }
77
120
  const defaultGetRandom = (max) => Math.floor(Math.random() * max);
@@ -0,0 +1,408 @@
1
+ {
2
+ "id": "basic-truco-v1",
3
+ "title": "Aprende a jugar",
4
+ "botProfile": "ProfeTruco",
5
+ "botName": "Profe Truco",
6
+ "options": {
7
+ "maxPlayers": 2,
8
+ "matchPoint": 9,
9
+ "flor": true,
10
+ "satsPerPlayer": 0
11
+ },
12
+ "hands": [
13
+ {
14
+ "goal": "Entender el objetivo y la fuerza de las cartas.",
15
+ "cardsByPlayerIdx": {
16
+ "0": ["4c", "7e", "1e"],
17
+ "1": ["6b", "5o", "3c"]
18
+ },
19
+ "messages": [
20
+ {
21
+ "trigger": "hand_start",
22
+ "text": "Bienvenido. Ganas el partido llegando a 9 puntos antes que el Profe."
23
+ },
24
+ {
25
+ "trigger": "before_human_turn",
26
+ "roundIdx": 1,
27
+ "text": "La ronda se gana con la carta mas fuerte. El ancho de espada es la mejor."
28
+ },
29
+ {
30
+ "trigger": "after_human_action",
31
+ "actionValue": "1e",
32
+ "text": "Esa manda. Despues vienen ancho de basto, 7 de espada y 7 de oro."
33
+ },
34
+ {
35
+ "trigger": "before_human_turn",
36
+ "roundIdx": 2,
37
+ "text": "Ahora cerra la mano usando otra carta fuerte cuando haga falta."
38
+ },
39
+ {
40
+ "trigger": "before_human_turn",
41
+ "roundIdx": 3,
42
+ "state": "WAITING_PLAY",
43
+ "text": "Si hace falta otra ronda, elegi la carta que mejor cierre la mano."
44
+ },
45
+ {
46
+ "trigger": "hand_end",
47
+ "text": "Perfecto. En truco se gana al mejor de tres rondas."
48
+ }
49
+ ],
50
+ "botActions": [
51
+ {
52
+ "trigger": "before_bot_action",
53
+ "roundIdx": 1,
54
+ "action": { "type": "card", "value": "3c" }
55
+ },
56
+ {
57
+ "trigger": "before_bot_action",
58
+ "roundIdx": 2,
59
+ "action": { "type": "card", "value": "6b" }
60
+ },
61
+ {
62
+ "trigger": "before_bot_action",
63
+ "roundIdx": 3,
64
+ "action": { "type": "card", "value": "5o" }
65
+ }
66
+ ]
67
+ },
68
+ {
69
+ "goal": "Aprender a jugar bajo antes de gastar cartas fuertes.",
70
+ "cardsByPlayerIdx": {
71
+ "0": ["4b", "7o", "1b"],
72
+ "1": ["5c", "3o", "2e"]
73
+ },
74
+ "messages": [
75
+ {
76
+ "trigger": "hand_start",
77
+ "text": "No siempre conviene tirar lo mejor de entrada. Guardar poder tambien gana manos."
78
+ },
79
+ {
80
+ "trigger": "before_bot_action",
81
+ "roundIdx": 1,
82
+ "text": "Ahora arranca el Profe. Mira que carta tira antes de gastar una fuerte."
83
+ },
84
+ {
85
+ "trigger": "before_human_turn",
86
+ "roundIdx": 1,
87
+ "text": "Podes probar con el 4 de basto y guardar dos cartas bravas."
88
+ },
89
+ {
90
+ "trigger": "after_bot_action",
91
+ "roundIdx": 2,
92
+ "actionValue": "3o",
93
+ "text": "Si perdes una ronda, tranqui. Usa una fuerte para recuperar control."
94
+ },
95
+ {
96
+ "trigger": "before_human_turn",
97
+ "roundIdx": 2,
98
+ "text": "El ancho de basto es enorme. Usalo si necesitas ganar esta ronda."
99
+ },
100
+ {
101
+ "trigger": "before_human_turn",
102
+ "roundIdx": 3,
103
+ "text": "Te queda el 7 de oro. Muy buena carta para definir."
104
+ },
105
+ {
106
+ "trigger": "hand_end",
107
+ "text": "Linda remontada. A veces perder barato la primera te deja mejor parado."
108
+ }
109
+ ],
110
+ "botActions": [
111
+ {
112
+ "trigger": "before_bot_action",
113
+ "roundIdx": 1,
114
+ "action": { "type": "card", "value": "5c" }
115
+ },
116
+ {
117
+ "trigger": "before_bot_action",
118
+ "roundIdx": 2,
119
+ "action": { "type": "card", "value": "3o" }
120
+ },
121
+ {
122
+ "trigger": "before_bot_action",
123
+ "roundIdx": 3,
124
+ "action": { "type": "card", "value": "2e" }
125
+ }
126
+ ]
127
+ },
128
+ {
129
+ "goal": "Cantar Envido antes de jugar y comparar tantos.",
130
+ "cardsByPlayerIdx": {
131
+ "0": ["7c", "6c", "1o"],
132
+ "1": ["5b", "4b", "re"]
133
+ },
134
+ "messages": [
135
+ {
136
+ "trigger": "hand_start",
137
+ "text": "Tenes 33 de envido: 7 y 6 de copa suman 20 + 13. Cantalo antes de jugar."
138
+ },
139
+ {
140
+ "trigger": "before_human_turn",
141
+ "roundIdx": 1,
142
+ "text": "Toca ENVIDO ahora. El envido se resuelve antes de seguir con las cartas."
143
+ },
144
+ {
145
+ "trigger": "before_bot_action",
146
+ "state": "WAITING_ENVIDO_ANSWER",
147
+ "text": "El Profe puede aceptar con QUIERO o achicarse con NO QUIERO."
148
+ },
149
+ {
150
+ "trigger": "after_bot_action",
151
+ "actionValue": "QUIERO",
152
+ "text": "El Profe quiso. Ahora declara tus 33 para comparar tantos."
153
+ },
154
+ {
155
+ "trigger": "before_human_turn",
156
+ "state": "WAITING_ENVIDO_POINTS_ANSWER",
157
+ "text": "Deci 33. Ese tanto obliga al Profe a superar tu envido."
158
+ },
159
+ {
160
+ "trigger": "after_bot_action",
161
+ "actionValue": 29,
162
+ "text": "El Profe dijo 29. Tus 33 ganan el envido."
163
+ },
164
+ {
165
+ "trigger": "before_human_turn",
166
+ "roundIdx": 1,
167
+ "state": "WAITING_PLAY",
168
+ "text": "Ahora segui la mano. El envido suma aparte del juego de cartas."
169
+ },
170
+ {
171
+ "trigger": "before_human_turn",
172
+ "roundIdx": 2,
173
+ "state": "WAITING_PLAY",
174
+ "text": "Ahora segui jugando tus cartas. Busca ganar la ronda sin regalar de mas."
175
+ },
176
+ {
177
+ "trigger": "before_human_turn",
178
+ "roundIdx": 3,
179
+ "state": "WAITING_PLAY",
180
+ "text": "Ya conoces la idea: elegi la mejor carta para esta ronda."
181
+ },
182
+ {
183
+ "trigger": "hand_end",
184
+ "text": "Clave: si tenes buen tanto, canta antes de mostrar cartas."
185
+ }
186
+ ],
187
+ "botActions": [
188
+ {
189
+ "trigger": "before_bot_action",
190
+ "state": "WAITING_ENVIDO_ANSWER",
191
+ "action": { "type": "command", "value": "QUIERO" }
192
+ },
193
+ {
194
+ "trigger": "before_bot_action",
195
+ "state": "WAITING_ENVIDO_POINTS_ANSWER",
196
+ "action": { "type": "command", "value": 29 }
197
+ },
198
+ {
199
+ "trigger": "before_bot_action",
200
+ "roundIdx": 1,
201
+ "action": { "type": "card", "value": "re" }
202
+ },
203
+ {
204
+ "trigger": "before_bot_action",
205
+ "roundIdx": 2,
206
+ "action": { "type": "card", "value": "5b" }
207
+ },
208
+ {
209
+ "trigger": "before_bot_action",
210
+ "roundIdx": 3,
211
+ "action": { "type": "card", "value": "4b" }
212
+ }
213
+ ]
214
+ },
215
+ {
216
+ "goal": "Responder Truco y entender cuando aceptar o rechazar.",
217
+ "cardsByPlayerIdx": {
218
+ "0": ["7b", "3e", "1c"],
219
+ "1": ["4o", "2b", "1e"]
220
+ },
221
+ "messages": [
222
+ {
223
+ "trigger": "hand_start",
224
+ "text": "El Truco sube la apuesta de la mano. Aceptar puede pagar, pero duele perder."
225
+ },
226
+ {
227
+ "trigger": "before_bot_action",
228
+ "roundIdx": 1,
229
+ "text": "Ahora empieza el Profe. No aceptes presion sin mirar tu mano."
230
+ },
231
+ {
232
+ "trigger": "before_human_turn",
233
+ "roundIdx": 1,
234
+ "text": "Responde con el 7 de basto. No es fuerte, pero guardas dos cartas decentes."
235
+ },
236
+ {
237
+ "trigger": "before_bot_action",
238
+ "roundIdx": 2,
239
+ "text": "Ojo, el Profe te va a apretar con TRUCO."
240
+ },
241
+ {
242
+ "trigger": "after_bot_action",
243
+ "actionValue": "TRUCO",
244
+ "text": "Con QUIERO jugas por 2 puntos. Con NO QUIERO entregas solo 1."
245
+ },
246
+ {
247
+ "trigger": "before_human_turn",
248
+ "state": "WAITING_FOR_TRUCO_ANSWER",
249
+ "text": "Aca acepta con QUIERO: tenes cartas para pelear y aprender el riesgo."
250
+ },
251
+ {
252
+ "trigger": "before_human_turn",
253
+ "roundIdx": 2,
254
+ "state": "WAITING_PLAY",
255
+ "text": "Ahora juga tu mejor carta disponible para pelear el Truco."
256
+ },
257
+ {
258
+ "trigger": "before_human_turn",
259
+ "roundIdx": 3,
260
+ "state": "WAITING_PLAY",
261
+ "text": "Si queda una ronda, mira que carta te queda y busca cerrarla."
262
+ },
263
+ {
264
+ "trigger": "hand_end",
265
+ "text": "A veces se pierde un Truco querido. La clave es medir riesgo y mano."
266
+ }
267
+ ],
268
+ "botActions": [
269
+ {
270
+ "trigger": "before_bot_action",
271
+ "roundIdx": 1,
272
+ "action": { "type": "card", "value": "4o" }
273
+ },
274
+ {
275
+ "trigger": "before_bot_action",
276
+ "roundIdx": 2,
277
+ "action": { "type": "command", "value": "TRUCO" }
278
+ },
279
+ {
280
+ "trigger": "before_bot_action",
281
+ "roundIdx": 2,
282
+ "action": { "type": "card", "value": "1e" }
283
+ },
284
+ {
285
+ "trigger": "before_bot_action",
286
+ "roundIdx": 3,
287
+ "action": { "type": "card", "value": "2b" }
288
+ }
289
+ ]
290
+ },
291
+ {
292
+ "goal": "Reconocer Flor y declararla con tres cartas del mismo palo.",
293
+ "cardsByPlayerIdx": {
294
+ "0": ["7o", "6o", "1o"],
295
+ "1": ["4e", "5b", "rc"]
296
+ },
297
+ "messages": [
298
+ {
299
+ "trigger": "hand_start",
300
+ "text": "Tres cartas del mismo palo son Flor. Tenes flor de oro: cantala antes de jugar."
301
+ },
302
+ {
303
+ "trigger": "before_human_turn",
304
+ "roundIdx": 1,
305
+ "text": "Toca FLOR. Con flor activa, vale mas que pensar en envido."
306
+ },
307
+ {
308
+ "trigger": "after_human_action",
309
+ "actionValue": "FLOR",
310
+ "text": "Flor cantada. Ahora segui jugando la mano de cartas."
311
+ },
312
+ {
313
+ "trigger": "before_human_turn",
314
+ "roundIdx": 1,
315
+ "state": "WAITING_PLAY",
316
+ "text": "Tu 7 de oro es muy fuerte. Usalo para marcar la cancha."
317
+ },
318
+ {
319
+ "trigger": "before_human_turn",
320
+ "roundIdx": 2,
321
+ "state": "WAITING_PLAY",
322
+ "text": "Ahora segui jugando tus cartas. La Flor ya sumo, falta la mano."
323
+ },
324
+ {
325
+ "trigger": "before_human_turn",
326
+ "roundIdx": 3,
327
+ "state": "WAITING_PLAY",
328
+ "text": "Ya conoces la idea: elegi la mejor carta para esta ronda."
329
+ },
330
+ {
331
+ "trigger": "hand_end",
332
+ "text": "Bien ahi. Si ves tres del mismo palo, primero mira la Flor."
333
+ }
334
+ ],
335
+ "botActions": [
336
+ {
337
+ "trigger": "before_bot_action",
338
+ "roundIdx": 1,
339
+ "action": { "type": "card", "value": "4e" }
340
+ },
341
+ {
342
+ "trigger": "before_bot_action",
343
+ "roundIdx": 2,
344
+ "action": { "type": "card", "value": "5b" }
345
+ },
346
+ {
347
+ "trigger": "before_bot_action",
348
+ "roundIdx": 3,
349
+ "action": { "type": "card", "value": "rc" }
350
+ }
351
+ ]
352
+ },
353
+ {
354
+ "goal": "Aprender cuando irse al mazo para perder menos.",
355
+ "cardsByPlayerIdx": {
356
+ "0": ["4c", "5o", "6b"],
357
+ "1": ["1e", "7e", "3b"]
358
+ },
359
+ "messages": [
360
+ {
361
+ "trigger": "hand_start",
362
+ "text": "Mano flojita: sin envido fuerte y sin cartas altas. No todo se pelea."
363
+ },
364
+ {
365
+ "trigger": "before_bot_action",
366
+ "roundIdx": 1,
367
+ "text": "El Profe huele debilidad y va a cantar TRUCO."
368
+ },
369
+ {
370
+ "trigger": "after_bot_action",
371
+ "actionValue": "TRUCO",
372
+ "text": "Con esta mano, irte al MAZO es correcto: perdes poco y pasas a la proxima."
373
+ },
374
+ {
375
+ "trigger": "before_human_turn",
376
+ "state": "WAITING_FOR_TRUCO_ANSWER",
377
+ "text": "Toca NO QUIERO. En truco, abandonar a tiempo tambien es jugar bien."
378
+ },
379
+ {
380
+ "trigger": "hand_end",
381
+ "text": "Excelente. Ahora intenta ganarme el resto del partido sin ayuda."
382
+ }
383
+ ],
384
+ "botActions": [
385
+ {
386
+ "trigger": "before_bot_action",
387
+ "roundIdx": 1,
388
+ "action": { "type": "command", "value": "TRUCO" }
389
+ },
390
+ {
391
+ "trigger": "before_bot_action",
392
+ "roundIdx": 1,
393
+ "action": { "type": "card", "value": "1e" }
394
+ },
395
+ {
396
+ "trigger": "before_bot_action",
397
+ "roundIdx": 2,
398
+ "action": { "type": "card", "value": "7e" }
399
+ },
400
+ {
401
+ "trigger": "before_bot_action",
402
+ "roundIdx": 3,
403
+ "action": { "type": "card", "value": "3b" }
404
+ }
405
+ ]
406
+ }
407
+ ]
408
+ }
@@ -0,0 +1,5 @@
1
+ import { ITutorialScenario, TutorialScenarioId } from "../types";
2
+ export declare const DEFAULT_TUTORIAL_SCENARIO_ID: TutorialScenarioId;
3
+ export declare const validateTutorialScenario: (scenario: ITutorialScenario) => ITutorialScenario;
4
+ export declare const TUTORIAL_SCENARIOS: Record<TutorialScenarioId, ITutorialScenario>;
5
+ export declare const getTutorialScenario: (id?: TutorialScenarioId | null | undefined) => ITutorialScenario;
@@ -0,0 +1,115 @@
1
+ import { CARDS, EAnswerCommand, EEnvidoAnswerCommand, EEnvidoCommand, EFlorCommand, EHandState, ESayCommand, ETrucoCommand, } from "../types";
2
+ import basicTrucoV1 from "./basic-truco-v1.json";
3
+ export const DEFAULT_TUTORIAL_SCENARIO_ID = "basic-truco-v1";
4
+ const MESSAGE_TRIGGERS = [
5
+ "hand_start",
6
+ "before_human_turn",
7
+ "after_human_action",
8
+ "before_bot_action",
9
+ "after_bot_action",
10
+ "hand_end",
11
+ ];
12
+ const COMMANDS = new Set([
13
+ ...Object.values(ESayCommand),
14
+ ...Object.values(ETrucoCommand),
15
+ ...Object.values(EAnswerCommand),
16
+ ...Object.values(EEnvidoCommand),
17
+ ...Object.values(EEnvidoAnswerCommand),
18
+ ...Object.values(EFlorCommand),
19
+ ]);
20
+ const HAND_STATES = new Set(Object.values(EHandState));
21
+ const isCard = (value) => typeof value === "string" && value in CARDS;
22
+ const isCommand = (value) => typeof value === "string" && COMMANDS.has(value);
23
+ const assertValidActionValue = (value, context) => {
24
+ if (typeof value === "number") {
25
+ return;
26
+ }
27
+ if (isCard(value) || isCommand(value)) {
28
+ return;
29
+ }
30
+ throw new Error(`${context} has invalid action value: ${String(value)}`);
31
+ };
32
+ const validateMessage = (message, context) => {
33
+ if (!MESSAGE_TRIGGERS.includes(message.trigger)) {
34
+ throw new Error(`${context} has invalid trigger: ${message.trigger}`);
35
+ }
36
+ if (typeof message.text !== "string" || !message.text.trim()) {
37
+ throw new Error(`${context} must contain text`);
38
+ }
39
+ if (message.text.length > 120) {
40
+ throw new Error(`${context} text is longer than 120 characters`);
41
+ }
42
+ if (message.state && !HAND_STATES.has(message.state)) {
43
+ throw new Error(`${context} has invalid state: ${message.state}`);
44
+ }
45
+ if (message.actionValue !== undefined) {
46
+ assertValidActionValue(message.actionValue, context);
47
+ }
48
+ };
49
+ const validateBotAction = (botAction, context) => {
50
+ var _a;
51
+ if (botAction.trigger !== "before_bot_action") {
52
+ throw new Error(`${context} has invalid trigger: ${botAction.trigger}`);
53
+ }
54
+ if (botAction.state && !HAND_STATES.has(botAction.state)) {
55
+ throw new Error(`${context} has invalid state: ${botAction.state}`);
56
+ }
57
+ if (!["card", "command"].includes((_a = botAction.action) === null || _a === void 0 ? void 0 : _a.type)) {
58
+ throw new Error(`${context} has invalid action type`);
59
+ }
60
+ if (botAction.action.type === "card" && !isCard(botAction.action.value)) {
61
+ throw new Error(`${context} card action must use a valid card id`);
62
+ }
63
+ if (botAction.action.type === "command" && !isCommand(botAction.action.value) && typeof botAction.action.value !== "number") {
64
+ throw new Error(`${context} command action must use a valid command`);
65
+ }
66
+ };
67
+ const validateHand = (hand, handIdx) => {
68
+ if (typeof hand.goal !== "string" || !hand.goal.trim()) {
69
+ throw new Error(`Tutorial hand ${handIdx} must have a goal`);
70
+ }
71
+ const usedCards = new Set();
72
+ for (const [playerIdx, cards] of Object.entries(hand.cardsByPlayerIdx || {})) {
73
+ if (!Number.isInteger(Number(playerIdx))) {
74
+ throw new Error(`Tutorial hand ${handIdx} has invalid player index: ${playerIdx}`);
75
+ }
76
+ if (!Array.isArray(cards) || cards.length !== 3) {
77
+ throw new Error(`Tutorial hand ${handIdx} player ${playerIdx} must have exactly 3 cards`);
78
+ }
79
+ for (const card of cards) {
80
+ if (!isCard(card)) {
81
+ throw new Error(`Tutorial hand ${handIdx} has invalid card: ${String(card)}`);
82
+ }
83
+ if (usedCards.has(card)) {
84
+ throw new Error(`Tutorial hand ${handIdx} has duplicate card: ${card}`);
85
+ }
86
+ usedCards.add(card);
87
+ }
88
+ }
89
+ hand.messages.forEach((message, index) => validateMessage(message, `Tutorial hand ${handIdx} message ${index}`));
90
+ hand.botActions.forEach((botAction, index) => validateBotAction(botAction, `Tutorial hand ${handIdx} bot action ${index}`));
91
+ };
92
+ export const validateTutorialScenario = (scenario) => {
93
+ if (!scenario.id || !scenario.title || !scenario.botProfile || !scenario.botName) {
94
+ throw new Error("Tutorial scenario must define id, title, botProfile, and botName");
95
+ }
96
+ if (scenario.options.maxPlayers !== 2) {
97
+ throw new Error("Tutorial v1 scenarios must be 1v1");
98
+ }
99
+ if (!Array.isArray(scenario.hands) || !scenario.hands.length) {
100
+ throw new Error(`Tutorial scenario ${scenario.id} must include hands`);
101
+ }
102
+ scenario.hands.forEach((hand, index) => validateHand(hand, index + 1));
103
+ return scenario;
104
+ };
105
+ export const TUTORIAL_SCENARIOS = {
106
+ [basicTrucoV1.id]: validateTutorialScenario(basicTrucoV1),
107
+ };
108
+ export const getTutorialScenario = (id = DEFAULT_TUTORIAL_SCENARIO_ID) => {
109
+ const scenarioId = id !== null && id !== void 0 ? id : DEFAULT_TUTORIAL_SCENARIO_ID;
110
+ const scenario = TUTORIAL_SCENARIOS[scenarioId];
111
+ if (!scenario) {
112
+ throw new Error(`Tutorial scenario not found: ${scenarioId}`);
113
+ }
114
+ return scenario;
115
+ };
package/dist/types.d.ts CHANGED
@@ -46,6 +46,49 @@ export interface ILobbyOptions {
46
46
  abandonTime: number;
47
47
  satsPerPlayer: number;
48
48
  }
49
+ export type TutorialScenarioId = "basic-truco-v1" | string;
50
+ export type ITutorialMessageTrigger = "hand_start" | "before_human_turn" | "after_human_action" | "before_bot_action" | "after_bot_action" | "hand_end";
51
+ export interface ITutorialMessage {
52
+ trigger: ITutorialMessageTrigger;
53
+ text: string;
54
+ roundIdx?: number;
55
+ state?: EHandState;
56
+ playerIdx?: number;
57
+ actionValue?: ECommand | ICard | number;
58
+ }
59
+ export interface ITutorialBotAction {
60
+ trigger: Extract<ITutorialMessageTrigger, "before_bot_action">;
61
+ roundIdx?: number;
62
+ state?: EHandState;
63
+ playerIdx?: number;
64
+ action: {
65
+ type: "card" | "command";
66
+ value: ICard | ECommand | number;
67
+ };
68
+ }
69
+ export interface ITutorialHand {
70
+ goal: string;
71
+ cardsByPlayerIdx: Record<string, [ICard, ICard, ICard]>;
72
+ messages: ITutorialMessage[];
73
+ botActions: ITutorialBotAction[];
74
+ }
75
+ export interface ITutorialScenario {
76
+ id: TutorialScenarioId;
77
+ title: string;
78
+ botProfile: BotProfile;
79
+ botName: string;
80
+ options: Pick<ILobbyOptions, "maxPlayers" | "matchPoint" | "flor" | "satsPerPlayer"> & Partial<Omit<ILobbyOptions, "maxPlayers" | "matchPoint" | "flor" | "satsPerPlayer">>;
81
+ hands: ITutorialHand[];
82
+ }
83
+ export interface ITutorialRuntime {
84
+ scenario: ITutorialScenario;
85
+ sentMessageKeys: Set<string>;
86
+ executedActionKeys: Set<string>;
87
+ messageQueue: Promise<void>;
88
+ hasQueuedMessage: boolean;
89
+ inputLocked: boolean;
90
+ messageGeneration: number;
91
+ }
49
92
  export interface IJoinQueueOptions {
50
93
  maxPlayers: 0 | 2 | 4 | 6;
51
94
  allowBots: boolean;
@@ -59,12 +102,40 @@ export interface IQueueStatus {
59
102
  queuedAt: number;
60
103
  botFallbackAt?: number;
61
104
  }
105
+ export interface IQueueProposalParticipant {
106
+ accountId?: number;
107
+ session: string;
108
+ name: string;
109
+ avatarUrl?: string | null;
110
+ bot: boolean;
111
+ ready: boolean;
112
+ }
62
113
  export interface IQueueMatchFound {
114
+ proposalId: string;
63
115
  matchSessionId: string;
64
116
  maxPlayers: 2 | 4 | 6;
65
117
  humanPlayers: number;
66
118
  botPlayers: number;
67
119
  filledWithBots: boolean;
120
+ readyExpiresAt: number;
121
+ lobbyOptions: ILobbyOptions;
122
+ participants: IQueueProposalParticipant[];
123
+ }
124
+ export interface IQueueReadyUpdate {
125
+ proposalId: string;
126
+ matchSessionId: string;
127
+ readyExpiresAt: number;
128
+ participants: IQueueProposalParticipant[];
129
+ }
130
+ export interface IQueueMatchStarting {
131
+ proposalId: string;
132
+ matchSessionId: string;
133
+ startsAt: number;
134
+ }
135
+ export interface IQueueMatchCancelled {
136
+ proposalId: string;
137
+ matchSessionId: string;
138
+ reason: "declined" | "timeout" | "cancelled" | "error";
68
139
  }
69
140
  export interface ISaidCommand {
70
141
  player: IPlayer | IPublicPlayer;
@@ -123,6 +194,12 @@ export interface IPublicMatch {
123
194
  lastCard?: ICard | null;
124
195
  lastCommand?: ECommand | number | null;
125
196
  awardedSatsPerPlayer?: number;
197
+ tutorial?: {
198
+ id: TutorialScenarioId;
199
+ title: string;
200
+ botKey: string;
201
+ inputLocked?: boolean;
202
+ };
126
203
  }
127
204
  export interface IPublicMatchStats {
128
205
  spectators: number;
@@ -136,6 +213,7 @@ export interface IPublicMatchInfo {
136
213
  winnerTeamIdx: 0 | 1 | undefined;
137
214
  createdFromQueue: boolean;
138
215
  queueOptions?: IJoinQueueOptions;
216
+ isTutorial?: boolean;
139
217
  }
140
218
  export type IPublicChatRoom = Pick<IChatRoom, "id" | "messages">;
141
219
  export interface IChatMessage {
@@ -150,6 +228,8 @@ export interface IChatMessage {
150
228
  hidden?: boolean;
151
229
  command?: boolean;
152
230
  card?: boolean;
231
+ tutorial?: boolean;
232
+ tutorialContext?: string;
153
233
  content: string;
154
234
  sound: string | boolean;
155
235
  }
@@ -162,6 +242,7 @@ export interface IChatRoom {
162
242
  send(user: IChatMessage["user"], message: string, sound?: string | boolean): void;
163
243
  card(user: IChatMessage["user"], card: ICard, sound?: string | boolean): void;
164
244
  command(team: 0 | 1, command: ECommand | number, sound?: string | boolean): void;
245
+ tutorial(user: IChatMessage["user"], message: string, sound?: string | boolean, tutorialContext?: string): void;
165
246
  system(message: string, sound?: string | boolean): void;
166
247
  sound(sound: string, toTeamIdx?: "0" | "1", fromUser?: IChatMessage["user"]): void;
167
248
  emit(message?: IChatMessage, teamIdxs?: string): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trucoshi",
3
- "version": "12.0.2",
3
+ "version": "12.1.1",
4
4
  "description": "Truco Game Server",
5
5
  "main": "dist/types.js",
6
6
  "license": "GPL-3.0",
@@ -65,6 +65,7 @@
65
65
  "files": [
66
66
  "dist/lib/*",
67
67
  "dist/cosmetics/*",
68
+ "dist/tutorials/*",
68
69
  "dist/types.js",
69
70
  "dist/types.d.ts",
70
71
  "dist/events.js",