trucoshi 12.1.0 → 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 +5 -0
- package/dist/events.js +1 -0
- package/dist/lib/classes/Deck.d.ts +1 -1
- package/dist/lib/classes/Deck.js +46 -3
- package/dist/tutorials/basic-truco-v1.json +408 -0
- package/dist/tutorials/index.d.ts +5 -0
- package/dist/tutorials/index.js +115 -0
- package/dist/types.d.ts +53 -0
- package/package.json +2 -1
package/dist/events.d.ts
CHANGED
|
@@ -56,6 +56,7 @@ export declare enum EClientEvent {
|
|
|
56
56
|
LOGOUT = "LOGOUT",
|
|
57
57
|
LEAVE_MATCH = "LEAVE_MATCH",
|
|
58
58
|
CREATE_MATCH = "CREATE_MATCH",
|
|
59
|
+
CREATE_TUTORIAL_MATCH = "CREATE_TUTORIAL_MATCH",
|
|
59
60
|
FETCH_ACCOUNT_DETAILS = "FETCH_ACCOUNT_DETAILS",
|
|
60
61
|
FETCH_MATCH_DETAILS = "FETCH_MATCH_DETAILS",
|
|
61
62
|
SET_MATCH_OPTIONS = "SET_MATCH_OPTIONS",
|
|
@@ -103,6 +104,10 @@ export interface ClientToServerEvents {
|
|
|
103
104
|
match?: IPublicMatch;
|
|
104
105
|
activeMatches?: IPublicMatchInfo[];
|
|
105
106
|
}>) => void;
|
|
107
|
+
[EClientEvent.CREATE_TUTORIAL_MATCH]: (tutorialId: string | null | undefined, callback: IEventCallback<{
|
|
108
|
+
match?: IPublicMatch;
|
|
109
|
+
activeMatches?: IPublicMatchInfo[];
|
|
110
|
+
}>) => void;
|
|
106
111
|
[EClientEvent.SET_MATCH_OPTIONS]: (matchSessionId: string, options: Partial<ILobbyOptions>, callback: IEventCallback<{
|
|
107
112
|
match?: IPublicMatch;
|
|
108
113
|
activeMatches?: IPublicMatchInfo[];
|
package/dist/events.js
CHANGED
|
@@ -28,6 +28,7 @@ export var EClientEvent;
|
|
|
28
28
|
EClientEvent["LOGOUT"] = "LOGOUT";
|
|
29
29
|
EClientEvent["LEAVE_MATCH"] = "LEAVE_MATCH";
|
|
30
30
|
EClientEvent["CREATE_MATCH"] = "CREATE_MATCH";
|
|
31
|
+
EClientEvent["CREATE_TUTORIAL_MATCH"] = "CREATE_TUTORIAL_MATCH";
|
|
31
32
|
EClientEvent["FETCH_ACCOUNT_DETAILS"] = "FETCH_ACCOUNT_DETAILS";
|
|
32
33
|
EClientEvent["FETCH_MATCH_DETAILS"] = "FETCH_MATCH_DETAILS";
|
|
33
34
|
EClientEvent["SET_MATCH_OPTIONS"] = "SET_MATCH_OPTIONS";
|
|
@@ -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;
|
package/dist/lib/classes/Deck.js
CHANGED
|
@@ -38,11 +38,54 @@ export function Deck() {
|
|
|
38
38
|
return deck;
|
|
39
39
|
}
|
|
40
40
|
export const getAllCards = () => Object.keys(CARDS);
|
|
41
|
-
|
|
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
|
|
74
|
-
player.setHand(playerHands[
|
|
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;
|
|
@@ -151,6 +194,12 @@ export interface IPublicMatch {
|
|
|
151
194
|
lastCard?: ICard | null;
|
|
152
195
|
lastCommand?: ECommand | number | null;
|
|
153
196
|
awardedSatsPerPlayer?: number;
|
|
197
|
+
tutorial?: {
|
|
198
|
+
id: TutorialScenarioId;
|
|
199
|
+
title: string;
|
|
200
|
+
botKey: string;
|
|
201
|
+
inputLocked?: boolean;
|
|
202
|
+
};
|
|
154
203
|
}
|
|
155
204
|
export interface IPublicMatchStats {
|
|
156
205
|
spectators: number;
|
|
@@ -164,6 +213,7 @@ export interface IPublicMatchInfo {
|
|
|
164
213
|
winnerTeamIdx: 0 | 1 | undefined;
|
|
165
214
|
createdFromQueue: boolean;
|
|
166
215
|
queueOptions?: IJoinQueueOptions;
|
|
216
|
+
isTutorial?: boolean;
|
|
167
217
|
}
|
|
168
218
|
export type IPublicChatRoom = Pick<IChatRoom, "id" | "messages">;
|
|
169
219
|
export interface IChatMessage {
|
|
@@ -178,6 +228,8 @@ export interface IChatMessage {
|
|
|
178
228
|
hidden?: boolean;
|
|
179
229
|
command?: boolean;
|
|
180
230
|
card?: boolean;
|
|
231
|
+
tutorial?: boolean;
|
|
232
|
+
tutorialContext?: string;
|
|
181
233
|
content: string;
|
|
182
234
|
sound: string | boolean;
|
|
183
235
|
}
|
|
@@ -190,6 +242,7 @@ export interface IChatRoom {
|
|
|
190
242
|
send(user: IChatMessage["user"], message: string, sound?: string | boolean): void;
|
|
191
243
|
card(user: IChatMessage["user"], card: ICard, sound?: string | boolean): void;
|
|
192
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;
|
|
193
246
|
system(message: string, sound?: string | boolean): void;
|
|
194
247
|
sound(sound: string, toTeamIdx?: "0" | "1", fromUser?: IChatMessage["user"]): void;
|
|
195
248
|
emit(message?: IChatMessage, teamIdxs?: string): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trucoshi",
|
|
3
|
-
"version": "12.1.
|
|
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",
|