funifier-mcp 0.2.26 → 0.2.27
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/.cursor/rules/funifier.mdc +38 -41
- package/.github/copilot-instructions.md +38 -41
- package/AGENTS.md +56 -49
- package/README.md +40 -22
- package/datasource-funifier-docs/.coverage.json +326 -0
- package/datasource-funifier-docs/.validation.json +593 -0
- package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
- package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
- package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
- package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
- package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
- package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
- package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
- package/datasource-funifier-docs/knowledge/index.md +4 -1
- package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
- package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
- package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
- package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
- package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
- package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
- package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
- package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
- package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
- package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
- package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
- package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
- package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
- package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
- package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
- package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
- package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
- package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
- package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
- package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
- package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
- package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
- package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
- package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
- package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
- package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
- package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
- package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
- package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
- package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
- package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
- package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
- package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
- package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
- package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
- package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
- package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
- package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
- package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
- package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
- package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +42 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/init.test.js +74 -3
- package/dist/cli/init.test.js.map +1 -1
- package/dist/cli/persona.d.ts +3 -0
- package/dist/cli/persona.d.ts.map +1 -0
- package/dist/cli/persona.js +25 -0
- package/dist/cli/persona.js.map +1 -0
- package/dist/mcp/bundle.js +119 -93
- package/dist/mcp/index.js +2 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/resources/documentation.d.ts +1 -1
- package/dist/mcp/resources/documentation.d.ts.map +1 -1
- package/dist/mcp/resources/documentation.js +39 -3
- package/dist/mcp/resources/documentation.js.map +1 -1
- package/dist/mcp/tools/connect.d.ts.map +1 -1
- package/dist/mcp/tools/connect.js +18 -8
- package/dist/mcp/tools/connect.js.map +1 -1
- package/dist/mcp/tools/database.d.ts.map +1 -1
- package/dist/mcp/tools/database.js +59 -47
- package/dist/mcp/tools/database.js.map +1 -1
- package/dist/mcp/tools/database.test.js +2 -2
- package/dist/mcp/tools/database.test.js.map +1 -1
- package/dist/mcp/tools/delete.d.ts.map +1 -1
- package/dist/mcp/tools/delete.js +13 -3
- package/dist/mcp/tools/delete.js.map +1 -1
- package/dist/mcp/tools/execute.d.ts.map +1 -1
- package/dist/mcp/tools/execute.js +20 -9
- package/dist/mcp/tools/execute.js.map +1 -1
- package/dist/mcp/tools/folder.d.ts.map +1 -1
- package/dist/mcp/tools/folder.js +22 -12
- package/dist/mcp/tools/folder.js.map +1 -1
- package/dist/mcp/tools/get.d.ts.map +1 -1
- package/dist/mcp/tools/get.js +16 -6
- package/dist/mcp/tools/get.js.map +1 -1
- package/dist/mcp/tools/index.d.ts +1 -1
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +3 -1
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/list.d.ts.map +1 -1
- package/dist/mcp/tools/list.js +38 -14
- package/dist/mcp/tools/list.js.map +1 -1
- package/dist/mcp/tools/logs.d.ts.map +1 -1
- package/dist/mcp/tools/logs.js +15 -5
- package/dist/mcp/tools/logs.js.map +1 -1
- package/dist/mcp/tools/save.d.ts.map +1 -1
- package/dist/mcp/tools/save.js +14 -4
- package/dist/mcp/tools/save.js.map +1 -1
- package/dist/mcp/tools/save.test.js +3 -3
- package/dist/mcp/tools/save.test.js.map +1 -1
- package/dist/mcp/tools/search-docs.d.ts +3 -0
- package/dist/mcp/tools/search-docs.d.ts.map +1 -0
- package/dist/mcp/tools/search-docs.js +102 -0
- package/dist/mcp/tools/search-docs.js.map +1 -0
- package/package.json +6 -2
- package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
- package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
- package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
- package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
- package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
- package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
- package/skills/funifier/SKILL.md +88 -0
- package/skills/funifier/references/configure-security.md +96 -0
- package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
- package/skills/funifier/references/create-aggregate.md +144 -0
- package/skills/funifier/references/create-challenge.md +116 -0
- package/skills/funifier/references/create-competition.md +98 -0
- package/skills/funifier/references/create-crossword.md +574 -0
- package/skills/funifier/references/create-custom-object.md +91 -0
- package/skills/funifier/references/create-custom-page.md +135 -0
- package/skills/funifier/references/create-folder.md +104 -0
- package/skills/funifier/references/create-lastmile.md +643 -0
- package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
- package/skills/funifier/references/create-level.md +94 -0
- package/skills/funifier/references/create-lottery.md +913 -0
- package/skills/funifier/references/create-mystery.md +769 -0
- package/skills/funifier/references/create-notification.md +75 -0
- package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
- package/skills/funifier/references/create-quiz.md +98 -0
- package/skills/funifier/references/create-scheduler.md +141 -0
- package/skills/funifier/references/create-story.md +636 -0
- package/skills/funifier/references/create-swap.md +95 -0
- package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
- package/skills/funifier/references/create-virtual-good.md +96 -0
- package/skills/funifier/references/create-webhook.md +72 -0
- package/skills/funifier/references/create-websocket.md +71 -0
- package/skills/funifier/references/create-widget.md +76 -0
- package/skills/funifier/references/debug.md +87 -0
- package/skills/funifier/references/help.md +81 -0
- package/skills/funifier/references/implement-frontend.md +106 -0
- package/skills/funifier/references/import-csv.md +75 -0
- package/skills/funifier/references/manage-player.md +82 -0
- package/skills/funifier/references/manage-team.md +76 -0
- package/skills/funifier/references/upload-file.md +91 -0
- package/skills/funifier-create-aggregate/SKILL.md +0 -127
- package/skills/funifier-create-challenge/SKILL.md +0 -88
- package/skills/funifier-create-custom-page/SKILL.md +0 -127
- package/skills/funifier-create-level/SKILL.md +0 -87
- package/skills/funifier-create-quiz/SKILL.md +0 -87
- package/skills/funifier-create-scheduler/SKILL.md +0 -127
- package/skills/funifier-create-virtual-good/SKILL.md +0 -87
- package/skills/funifier-debug/SKILL.md +0 -92
- package/skills/funifier-help/SKILL.md +0 -86
- package/skills/funifier-implement-frontend/SKILL.md +0 -90
- package/skills/funifier-index/SKILL.md +0 -58
|
@@ -0,0 +1,769 @@
|
|
|
1
|
+
# funifier-create-mystery
|
|
2
|
+
|
|
3
|
+
Create a Funifier mystery box — probability-based instant games (spin wheel, scratch card, coin flip) with configurable odds and rewards; use for random reward mechanics, not for ticket-based draws with scheduled dates (use lottery)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Documentação do módulo (já incluída — não precisa buscar)
|
|
8
|
+
|
|
9
|
+
**Acesso Studio:** `/studio/mystery`
|
|
10
|
+
**API Endpoint:** `/v3/mystery`
|
|
11
|
+
**Coleções MongoDB:** `mystery_box`, `mystery_box_log`, `mystery_folder` (as vitórias e recompensas ficam em `achievement` com `type=6`)
|
|
12
|
+
|
|
13
|
+
> Documentação de engenharia reversa, produzida a partir do código-fonte real do `funifier-service` (commit `830037e`). Os exemplos da API antiga (`MysteryBoxRest.java`) contêm campos que o runtime **ignora silenciosamente** — este documento prioriza o comportamento do código sobre o que os exemplos sugerem.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. Visão Geral
|
|
18
|
+
|
|
19
|
+
O módulo `mystery` (Mystery Box / "caça-níquel"/slot machine/raspadinha) implementa **recompensa aleatória por probabilidade**. O jogador "gira a roleta" (executa a mystery box) e o sistema sorteia, para cada coluna, uma das opções configuradas respeitando a probabilidade de cada uma. O conjunto de opções sorteadas é comparado contra a `win_chart` (tabela de combinações vencedoras); se houver casamento, o jogador recebe a recompensa associada.
|
|
20
|
+
|
|
21
|
+
Papel arquitetural:
|
|
22
|
+
|
|
23
|
+
- A configuração é um documento na coleção `mystery_box` (`MysteryBox.java`). **Não existe Service nem Repository/Dao dedicados** — toda persistência é feita diretamente via `Jongo` dentro de `MysteryBoxManager` (`MysteryBoxManager.java`, L31-754).
|
|
24
|
+
- **Não há coleção própria de "resultados"**: a jogada vencedora e cada recompensa viram documentos na coleção `achievement`, reutilizando o `AchievementManager` (totais, level-up, `player_status`, triggers, webhooks). Por isso o módulo depende fortemente de [achievement](achievement.md).
|
|
25
|
+
- Toda execução é registrada na coleção `mystery_box_log` (auditoria de tentativas / "attempts").
|
|
26
|
+
- A execução é **sempre síncrona e manual** (chamada de endpoint). Não há scheduler/job automático para mystery — diferente de [lottery](lottery.md).
|
|
27
|
+
|
|
28
|
+
Relação com outros módulos:
|
|
29
|
+
|
|
30
|
+
- [achievement](achievement.md) — a jogada vira `Achievement type=6` (somente em vitória); cada recompensa vira um achievement adicional (ponto/desafio/item).
|
|
31
|
+
- [trigger](../guides/triggers.md) — dispara `before_win`/`after_win`/`before_lose`/`after_lose` (entidade `mystery_box`), além do evento literal `before_win_reward` e dos eventos `before_win`/`after_win` na coleção da recompensa.
|
|
32
|
+
- [challenge](challenge.md) (`Requirement`) — define tanto o **custo de jogar** (`requirements`) quanto a **recompensa** de cada combinação (`win_chart[].reward`).
|
|
33
|
+
- `point_category`, `catalog_item` (virtual good) — tipos de recompensa suportados (além de `challenge`).
|
|
34
|
+
- [folder](folder.md) — agrupamento via coleção `mystery_folder`.
|
|
35
|
+
- `ai` — `POST /v3/ai/build/mystery` (`AIRest.buildMystery`, L2088) **gera** um JSON de mystery box via OpenAI a partir de linguagem natural, mas **não persiste** — apenas devolve o objeto desserializado.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 2. Arquitetura e Fluxos
|
|
40
|
+
|
|
41
|
+
### 2.1 Classes envolvidas
|
|
42
|
+
|
|
43
|
+
| Classe | Papel |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `com.funifier.engine.mystery.MysteryBox` | Entidade/POJO raiz — documento em `mystery_box` (`MysteryBox.java`, L24-89) |
|
|
46
|
+
| `com.funifier.engine.mystery.MysteryOption` | Uma opção sorteável (valor + probabilidade) (`MysteryOption.java`) |
|
|
47
|
+
| `com.funifier.engine.mystery.MysteryWin` | Linha da `win_chart` (combinação vencedora + recompensa + limites) (`MysteryWin.java`) |
|
|
48
|
+
| `com.funifier.engine.mystery.MysteryResult` | DTO de saída do `execute` — **não persistido** (`MysteryResult.java`) |
|
|
49
|
+
| `com.funifier.engine.mystery.MysteryBoxLog` | Log de tentativa — documento em `mystery_box_log` (`MysteryBoxLog.java`) |
|
|
50
|
+
| `com.funifier.engine.mystery.MysteryFolder` | Pasta de agrupamento — documento em `mystery_folder` |
|
|
51
|
+
| `com.funifier.engine.mystery.MysteryBoxFolder` | **Classe morta** — duplicata de `MysteryFolder`, sem nenhuma referência no código (ver §7) |
|
|
52
|
+
| `com.funifier.engine.mystery.ProbabilityUtil` | Sorteio ponderado por probabilidade (`getChoice`) (`ProbabilityUtil.java`) |
|
|
53
|
+
| `com.funifier.engine.mystery.MysteryBoxManager` | Manager monolítico: CRUD, execução, limites, rollback, evaluate (`MysteryBoxManager.java`, L31-754) |
|
|
54
|
+
| `com.funifier.rest.v3.rest.MysteryBoxRest` | Controller REST v3 (`/v3/mystery`) (`MysteryBoxRest.java`, L44-572) |
|
|
55
|
+
|
|
56
|
+
### 2.2 Pipeline de criação — `insert(MysteryBox)` (L39-70)
|
|
57
|
+
|
|
58
|
+
`POST /v3/mystery` → `MysteryBoxManager.insert`. Comportamento de **upsert** (Jongo `c.save` grava por `_id`; reenviar o mesmo `_id` **substitui o documento inteiro**).
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
[1] Se _id ausente/vazio → gera Guid.newShortGuid() (L42-44)
|
|
62
|
+
[2] Se active == null → active = true (L46-48)
|
|
63
|
+
[3] prepareOptions(mystery) — recalcula probabilidades das opções (L51 → L117-146)
|
|
64
|
+
[4] prepareWinchart(mystery) — atribui _id "o1","o2",... às combinações (L54 → L85-111)
|
|
65
|
+
[5] Se folder vazio → folder = "default" (L57-59)
|
|
66
|
+
[6] Garante que o documento da pasta exista em mystery_folder;
|
|
67
|
+
se não existir, cria MysteryFolder {id=folder, title=folder} (L60-67)
|
|
68
|
+
[7] c.save(mystery) na coleção mystery_box (L69)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
> **Não existe `PUT`.** Atualização = re-`POST` com o mesmo `_id`. Como é `save` (full replace), **omitir um campo o apaga** — não é patch parcial.
|
|
72
|
+
|
|
73
|
+
#### `prepareOptions` — recálculo de probabilidades (L117-146)
|
|
74
|
+
|
|
75
|
+
Apesar do comentário do método citar `type == option_probability`, ele é executado **sempre**, independentemente de qualquer `type` (que aliás é ignorado — ver §3/§7).
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
para cada opção o:
|
|
79
|
+
se o.probability == null OU == 0 → vai para zeroOptions
|
|
80
|
+
senão se 0 < o.probability <= 1 → vai para goodOptions; goodTotal += o.probability
|
|
81
|
+
senão (prob > 1 OU prob < 0) → DESCARTADA silenciosamente (!)
|
|
82
|
+
|
|
83
|
+
diffTotal = 1 - goodTotal
|
|
84
|
+
division = diffTotal / zeroOptions.size()
|
|
85
|
+
para cada opção em zeroOptions:
|
|
86
|
+
o.probability = division (divide o "resto" igualmente entre as opções sem prob.)
|
|
87
|
+
|
|
88
|
+
mystery.options = goodOptions ++ (zeroOptions já ajustadas) → ORDEM É ALTERADA (!)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Regras emergentes (não documentadas no schema):
|
|
92
|
+
|
|
93
|
+
- Opções com `probability > 1` ou `< 0` são **removidas silenciosamente** do documento salvo.
|
|
94
|
+
- Opções sem probabilidade (`null`/`0`) recebem partes iguais do "resto" (`1 - soma das demais`).
|
|
95
|
+
- Se a soma das probabilidades informadas for `< 1` **e não houver** opção de probabilidade zero, o "resto" vira a chance de **NONE** (sorteio devolve `null` — ver §2.4).
|
|
96
|
+
- **Foot-gun:** se `goodTotal > 1`, `diffTotal` fica **negativo** e as opções "zero" recebem probabilidade **negativa**, corrompendo `ProbabilityUtil.getChoice()` (ver §5).
|
|
97
|
+
- A lista é **reordenada** (boas primeiro, depois as ex-zero) — qualquer UI/Groovy que dependa da ordem de entrada verá a ordem mudar após o primeiro save.
|
|
98
|
+
|
|
99
|
+
### 2.3 Pipeline principal — `execute(id, player[, extra])` (L173-419)
|
|
100
|
+
|
|
101
|
+
Método **`synchronized`** (lock de **instância** do `MysteryBoxManager`, que é por API key / `ManagerFactory`). Serializa execuções concorrentes **dentro da mesma JVM**; **não é lock distribuído** — múltiplos pods podem competir. Síncrono, **sem transação MongoDB** (cada `save`/`addAchievement` é independente).
|
|
102
|
+
|
|
103
|
+
A validação de `active`, `limit` e `attempts` acontece **na camada REST** (`MysteryBoxRest.execute`, L244-317) **antes** de chamar o manager. O manager re-busca o documento e executa:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
[1] Re-carrega MysteryBox por id; carrega Player (L178-181)
|
|
107
|
+
Se mystery == null OU player == null → não faz nada, retorna result vazio (L184)
|
|
108
|
+
[2] Cria a1 = Achievement(novoGuid, player, total=1, TYPE_MYSTERY_BOX=6, item=id, now) (L187)
|
|
109
|
+
[3] SALVA MysteryBoxLog(a1.id, item=id, player, time, extra) em mystery_box_log (L191-192) (!) SEMPRE
|
|
110
|
+
[4] Se há requirements: evaluateRequirements; se OK → deductRequirements,
|
|
111
|
+
senão → throw "insufficient requirements" (HTTP 400) (L195-202)
|
|
112
|
+
(!) O log do passo [3] já foi gravado mesmo se falhar aqui.
|
|
113
|
+
[5] columns = (mystery.columns > 0) ? mystery.columns : 1 (L206)
|
|
114
|
+
[6] [Apenas se columns == 1] Exclusão de opções com recompensa esgotada (L213-283) — ver §2.5
|
|
115
|
+
[7] Monta ProbabilityUtil (opções viáveis OU originais) e sorteia
|
|
116
|
+
`columns` valores → lista `win` (pode conter null = NONE) (L286-298)
|
|
117
|
+
[8] result = {mystery:id, player, time, result:win} (L301-304)
|
|
118
|
+
[9] SE win_chart não-vazia (L307):
|
|
119
|
+
status = containsWinCombination(win) ? WIN : LOSE (L313-317)
|
|
120
|
+
SE WIN → result.achievements.add(a1);
|
|
121
|
+
trigger before_win (mystery_box);
|
|
122
|
+
addAchievement(a1) <- a1 só persiste em VITÓRIA (!) (L320-331)
|
|
123
|
+
SE LOSE → trigger before_lose (mystery_box) (L333-337)
|
|
124
|
+
Para cada win_chart[i] que casa com `win` (L340-397):
|
|
125
|
+
checkRewardIsInLimit && checkInventoryIsInLimit (L345-348)
|
|
126
|
+
se reward != null E disponível → cria a2, dispara triggers,
|
|
127
|
+
addAchievement(a2) (só p/ reward type 0/1/2) (L351-390)
|
|
128
|
+
senão → System.out.println("MysteryWin not registered because of limit") (L393)
|
|
129
|
+
SE WIN → trigger after_win (mystery_box) (L400-403)
|
|
130
|
+
SE LOSE → trigger after_lose (mystery_box) (L407-409)
|
|
131
|
+
[10] updatePlayerStatus(player) — SEMPRE que mystery!=null && player!=null (L414)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
> **Distinção crítica entre `limit` e `attempts`:** o achievement `a1` (`type=6`) só é gravado em **vitória** (passo [9], L330). Logo:
|
|
135
|
+
> - **`limit`** conta documentos `achievement type=6` → é um limite de **VITÓRIAS**.
|
|
136
|
+
> - **`attempts`** conta documentos `mystery_box_log` → é um limite de **TENTATIVAS/jogadas** (gravado sempre, passo [3]).
|
|
137
|
+
>
|
|
138
|
+
> Uma derrota **não** incrementa `limit`, mas **incrementa** `attempts`.
|
|
139
|
+
|
|
140
|
+
> **Sem `win_chart`:** se a box não tiver `win_chart` (vazia/nula), nenhum `status` é definido, **nenhum** achievement `a1` é gravado e **nenhum** trigger dispara. O `result` sai com `result[]` preenchido e `status` ausente.
|
|
141
|
+
|
|
142
|
+
### Fluxo de execução — `execute(id, player)`
|
|
143
|
+
|
|
144
|
+
```mermaid
|
|
145
|
+
flowchart TB
|
|
146
|
+
A["REST: validacoes<br/>(existe? active? limit? attempts?)"] --> B["Manager.execute<br/>(synchronized)"]
|
|
147
|
+
B --> C{"mystery != null<br/>&& player != null?"}
|
|
148
|
+
C -- nao --> Zr["retorna result vazio"]
|
|
149
|
+
C -- sim --> D["cria a1 (Achievement type=6)"]
|
|
150
|
+
D --> E["salva MysteryBoxLog (SEMPRE)"]
|
|
151
|
+
E --> F{requirements OK?}
|
|
152
|
+
F -- nao --> Z["throw 400<br/>(log ja persistido)"]
|
|
153
|
+
F -- sim --> G["deductRequirements"]
|
|
154
|
+
G --> H{columns == 1?}
|
|
155
|
+
H -- sim --> I["exclui opcoes c/ recompensa<br/>esgotada e renormaliza"]
|
|
156
|
+
H -- nao --> J["usa opcoes originais"]
|
|
157
|
+
I --> K["sorteia colunas<br/>(ProbabilityUtil)"]
|
|
158
|
+
J --> K
|
|
159
|
+
K --> L{win_chart vazia?}
|
|
160
|
+
L -- sim --> P["sem status, sem trigger"]
|
|
161
|
+
L -- nao --> M{containsWinCombination?}
|
|
162
|
+
M -- sim --> N["status=WIN<br/>before_win + addAchievement(a1)"]
|
|
163
|
+
M -- nao --> O["status=LOSE<br/>before_lose"]
|
|
164
|
+
N --> Q["loop win_chart: rewards (sec.6)"]
|
|
165
|
+
O --> Q
|
|
166
|
+
Q --> R{WIN?}
|
|
167
|
+
R -- sim --> S["after_win (mystery_box)"]
|
|
168
|
+
R -- nao --> T["after_lose (mystery_box)"]
|
|
169
|
+
P --> U["updatePlayerStatus (SEMPRE)"]
|
|
170
|
+
S --> U
|
|
171
|
+
T --> U
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Interação entre módulos — vitória com recompensa
|
|
175
|
+
|
|
176
|
+
```mermaid
|
|
177
|
+
sequenceDiagram
|
|
178
|
+
participant C as Cliente
|
|
179
|
+
participant R as MysteryBoxRest
|
|
180
|
+
participant M as MysteryBoxManager
|
|
181
|
+
participant Tr as TriggerManager
|
|
182
|
+
participant A as AchievementManager
|
|
183
|
+
participant DB as MongoDB
|
|
184
|
+
|
|
185
|
+
C->>R: GET /v3/mystery/execute/{id}?player=X
|
|
186
|
+
R->>R: valida active / limit (wins) / attempts (logs)
|
|
187
|
+
R->>M: execute(id, player)
|
|
188
|
+
M->>DB: save mystery_box_log (tentativa)
|
|
189
|
+
M->>A: evaluate+deduct requirements
|
|
190
|
+
M->>M: sorteia colunas (ProbabilityUtil)
|
|
191
|
+
M->>Tr: before_win (mystery_box)
|
|
192
|
+
M->>A: addAchievement(a1 type=6) [so em WIN]
|
|
193
|
+
loop cada win_chart que casa
|
|
194
|
+
M->>M: checkRewardIsInLimit + checkInventoryIsInLimit
|
|
195
|
+
M->>Tr: before_win_reward (mystery_box)
|
|
196
|
+
M->>Tr: before_win (colecao da recompensa)
|
|
197
|
+
M->>A: addAchievement(a2)
|
|
198
|
+
M->>Tr: after_win (colecao da recompensa)
|
|
199
|
+
end
|
|
200
|
+
M->>Tr: after_win (mystery_box)
|
|
201
|
+
M->>A: updatePlayerStatus(player)
|
|
202
|
+
M-->>R: MysteryResult
|
|
203
|
+
R-->>C: 200 {result, status, achievements}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 2.4 Sorteio — `ProbabilityUtil.getChoice()` (L95-104)
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
rand = Math.random() // [0,1)
|
|
210
|
+
para cada item (probs):
|
|
211
|
+
se rand < prob(item) → retorna item
|
|
212
|
+
rand -= prob(item)
|
|
213
|
+
retorna null // "resto" não coberto = NONE
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
- A iteração segue a ordem do `HashMap` (não determinística entre opções) — não é estável, mas é estatisticamente correta.
|
|
217
|
+
- Se a soma das probabilidades for `< 1`, sobra uma faixa para a qual `getChoice` devolve **`null`** → o valor `null` entra em `result.result[]` e representa "não ganhou nada" naquela coluna.
|
|
218
|
+
|
|
219
|
+
### 2.5 Exclusão de opções esgotadas (apenas `columns == 1`) (L213-283)
|
|
220
|
+
|
|
221
|
+
Melhoria solicitada pela CH Media. Quando `columns == 1`, antes do sorteio o sistema verifica, para cada combinação vencedora de **1 elemento**, se a recompensa atingiu `limit`/`inventory`. As opções cuja recompensa está esgotada são **removidas do pool** e a probabilidade delas é **redistribuída proporcionalmente** entre as opções viáveis (algoritmo "v2", L274-279):
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
para cada opção viável o:
|
|
225
|
+
novaProb(o) = o.probability + (o.probability / somaViáveis) * somaIgnoradas
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
A substituição só ocorre se `0 < #ignoradas < #opções` (se todas estiverem esgotadas, mantém as opções originais). **Para `columns > 1` esta proteção não existe** — pode-se sortear uma combinação cuja recompensa já está esgotada (a recompensa simplesmente não é creditada no passo de win_chart).
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## 3. Estrutura dos Objetos
|
|
233
|
+
|
|
234
|
+
### 3.1 `MysteryBox` — documento raiz (coleção `mystery_box`)
|
|
235
|
+
|
|
236
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
237
|
+
|---|---|---|---|---|
|
|
238
|
+
| `_id` | String | `Guid.newShortGuid()` | — | Gerado se ausente/vazio (L42-44). Reenviar o mesmo `_id` substitui o documento. |
|
|
239
|
+
| `title` | String | — | sim (de fato) | Título exibido. Não há validação de obrigatoriedade no backend, mas é esperado pela UI. |
|
|
240
|
+
| `image` | `Image` (objeto) | `null` | não | Imagem de capa. |
|
|
241
|
+
| `options` | `MysteryOption[]` | `[]` | sim | Opções sorteáveis. Reescritas por `prepareOptions` no save (ver §2.2). |
|
|
242
|
+
| `columns` | int | `1` | não | Quantos valores são sorteados por jogada (os "carretéis" do slot). `<= 0` é tratado como `1` na execução (L206). |
|
|
243
|
+
| `requirements` | `Requirement[]` | `[]` | não | **Custo** para jogar (ex.: 1 moeda). Verificados e debitados no `execute` (L195-202). |
|
|
244
|
+
| `win_chart` | `MysteryWin[]` | `[]` | não | Combinações vencedoras e recompensas. Sem `win_chart`, nunca há vitória nem trigger (ver §2.3). |
|
|
245
|
+
| `limit` | `Limit` | `null` | não | Limite de **vitórias** (conta `achievement type=6`). |
|
|
246
|
+
| `attempts` | `Limit` | `null` | não | Limite de **tentativas** (conta `mystery_box_log`). |
|
|
247
|
+
| `folder` | String | `"default"` | não | Pasta de agrupamento. Cria o doc em `mystery_folder` se não existir (L60-67). |
|
|
248
|
+
| `active` | Boolean | `true` (no insert) | não | **Só `false` explícito bloqueia** o execute: `!(active==null \|\| active==true)` (L244). `null`/ausente = ativo. |
|
|
249
|
+
| `extra` | Map | `{}` | não | Metadado livre. **Persistido mas nunca lido** pela lógica do módulo mystery — passthrough puro (ver §7). |
|
|
250
|
+
| `techniques` | String[] | `null` | não | **Legado/inativo.** Nunca escrito nem lido pelo módulo (ver §3.7 e §7). |
|
|
251
|
+
|
|
252
|
+
#### Campos aceitos no JSON mas **silenciosamente ignorados**
|
|
253
|
+
|
|
254
|
+
`MysteryBox` é anotado com `@JsonIgnoreProperties(ignoreUnknown=true)`. Logo, qualquer campo não mapeado é descartado **sem erro**. Em particular:
|
|
255
|
+
|
|
256
|
+
- **`type`** (`"option_probability"` / `"combination_probability"`) — aparece nos exemplos antigos da API (L117, L149) mas **não existe** na entidade. É descartado. O comportamento de recálculo de probabilidades é o mesmo para qualquer "type".
|
|
257
|
+
|
|
258
|
+
#### Métodos de domínio em `MysteryBox`
|
|
259
|
+
|
|
260
|
+
- `containsWinCombination(result)` (L67-76) — `true` se **alguma** linha de `win_chart` casa com o resultado.
|
|
261
|
+
- `getOption(value)` (L78-88) — localiza a `MysteryOption` pelo `value`.
|
|
262
|
+
|
|
263
|
+
### 3.2 `MysteryOption` — uma opção sorteável
|
|
264
|
+
|
|
265
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
266
|
+
|---|---|---|---|---|
|
|
267
|
+
| `value` | String | — | sim | Identificador da opção (usado nas combinações da `win_chart`). |
|
|
268
|
+
| `probability` | Double | `null` | não | Entre 0 e 1. `null`/`0` → calculada como parte igual do "resto"; `>1` ou `<0` → opção **descartada** (§2.2). |
|
|
269
|
+
| `title` | String | `null` | não | Rótulo exibível. |
|
|
270
|
+
| `image` | String | `null` | não | URL da imagem da opção. |
|
|
271
|
+
|
|
272
|
+
### 3.3 `MysteryWin` — linha da `win_chart`
|
|
273
|
+
|
|
274
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
275
|
+
|---|---|---|---|---|
|
|
276
|
+
| `_id` | String | `"o1"`,`"o2"`,… | — | Atribuído por `prepareWinchart` se ausente (L96-108). Usado no escopo de `limit`/`inventory` isolados. |
|
|
277
|
+
| `combination` | String[] | — | sim | Lista de `value`s que formam a combinação vencedora. |
|
|
278
|
+
| `orderSensitive` | boolean | `false` | não | `true` = igualdade exata (ordem importa); `false` = **contagem por tipo** (multiset/subconjunto) — ver §5. |
|
|
279
|
+
| `reward` | `Requirement` | `null` | não | Recompensa. **Usa apenas `total`, `type`, `item`** (o `operation` é ignorado — comentário L21). |
|
|
280
|
+
| `limit` | `Limit` | `null` | não | Limite de quantas vezes a recompensa pode ser obtida. **Só `player` e `gamification`** (ver §5/§7). |
|
|
281
|
+
| `inventory` | Double | `null` | não | Estoque global da recompensa (cap absoluto de unidades distribuídas). |
|
|
282
|
+
| `isolated` | boolean | `false` | não | Se `true`, `limit`/`inventory` contam apenas conquistas **desta** mystery box (e, com `_id`, desta combinação). |
|
|
283
|
+
|
|
284
|
+
#### Campo aceito mas ignorado
|
|
285
|
+
|
|
286
|
+
- **`probability`** em cada item de `win_chart` — aparece nos exemplos antigos (L175, L179…) mas **não existe** em `MysteryWin`. É descartado. A chance de uma combinação vencedora **deriva exclusivamente** das probabilidades em `options` × `columns`, **não** de uma probabilidade por combinação.
|
|
287
|
+
|
|
288
|
+
#### `isWinCombination(result)` (L36-78)
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
se orderSensitive:
|
|
292
|
+
retorna String(combination) == String(result) // igualdade EXATA e ordenada
|
|
293
|
+
senão:
|
|
294
|
+
// conta ocorrências por tipo em result e em combination
|
|
295
|
+
para cada tipo da combination:
|
|
296
|
+
se result não tem aquele tipo OU tem MENOS que o exigido → perdeu
|
|
297
|
+
retorna ganhou
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
(!) Com `orderSensitive=false` é **contenção de multiconjunto**, não igualdade: `result=[a,a,b]` **satisfaz** `combination=[a,a]`. Com várias colunas, **uma única jogada pode casar várias linhas** da `win_chart` e creditar **múltiplas recompensas** (o loop L340-397 percorre todas).
|
|
301
|
+
|
|
302
|
+
### 3.4 `MysteryResult` — saída do `execute` (não persistido)
|
|
303
|
+
|
|
304
|
+
| Campo | Tipo | Descrição |
|
|
305
|
+
|---|---|---|
|
|
306
|
+
| `mystery` | String | id da box. |
|
|
307
|
+
| `player` | String | id do jogador. |
|
|
308
|
+
| `time` | String | timestamp ISO (zoned). |
|
|
309
|
+
| `result` | String[] | valores sorteados (1 por coluna). **Pode conter `null`** = NONE. |
|
|
310
|
+
| `status` | String | `"WIN"` / `"LOSE"`. **Ausente** se `win_chart` vazia. |
|
|
311
|
+
| `achievements` | `Achievement[]` | a jogada (`a1`, só em WIN) + recompensas creditadas (`a2`). |
|
|
312
|
+
|
|
313
|
+
Constantes: `STATUS_WIN = "WIN"`, `STATUS_LOSE = "LOSE"`.
|
|
314
|
+
|
|
315
|
+
### 3.5 `MysteryBoxLog` — log de tentativa (coleção `mystery_box_log`)
|
|
316
|
+
|
|
317
|
+
| Campo | Tipo | Descrição |
|
|
318
|
+
|---|---|---|
|
|
319
|
+
| `_id` | String | = id do achievement `a1` da jogada. |
|
|
320
|
+
| `item` | String | = id da mystery box. |
|
|
321
|
+
| `player` | String | jogador. |
|
|
322
|
+
| `time` | Date | momento da tentativa. |
|
|
323
|
+
| `extra` | Map | metadado enviado no `execute` (POST). |
|
|
324
|
+
|
|
325
|
+
### 3.6 `MysteryFolder` — pasta (coleção `mystery_folder`)
|
|
326
|
+
|
|
327
|
+
| Campo | Tipo | Descrição |
|
|
328
|
+
|---|---|---|
|
|
329
|
+
| `_id` | String | id da pasta (= valor de `MysteryBox.folder`). |
|
|
330
|
+
| `title` | String | título; no auto-create recebe o mesmo valor do id (L64-65). |
|
|
331
|
+
|
|
332
|
+
### 3.7 Técnicas de jogo (`techniques`)
|
|
333
|
+
|
|
334
|
+
O campo `techniques` existe em `MysteryBox` mas, **diferente de point/challenge/level/leaderboard/lastmile/virtual_good/lottery/question/competition**, o `mystery_box` **não** está na rotina de atribuição de código GT do `GameTechniqueManager` (L63-181). **Não existe constante de GT code para mystery box** na lista (L67-75). O `GameTechniqueManager` apenas lê as boxes para montar o **grafo de relacionamentos** (nós = boxes, links = box → item de recompensa, L295-297 e L636-647) — nunca grava `techniques`. Portanto: **não há código GT operacional para mystery; o campo é legado.**
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## 4. Endpoints
|
|
339
|
+
|
|
340
|
+
Todos sob `@Path("v3/mystery")`, `Produces: application/json; charset=UTF-8`. Autenticação via Bearer token (`AuthBean`).
|
|
341
|
+
|
|
342
|
+
### `GET /v3/mystery/{id}` — buscar uma box
|
|
343
|
+
|
|
344
|
+
| Aspecto | Detalhe |
|
|
345
|
+
|---|---|
|
|
346
|
+
| Finalidade | Retorna a configuração da box (`find`, L59). |
|
|
347
|
+
| Autenticação | Bearer token |
|
|
348
|
+
| (!) id inexistente | Retorna **`200 OK` com corpo `null`** — **não** 404 (não há null-check). |
|
|
349
|
+
|
|
350
|
+
### `GET /v3/mystery` — listar todas
|
|
351
|
+
|
|
352
|
+
Retorna todas as boxes (`findAll`, L76). Coleção vazia → `200` com `[]`. Sem paginação na API (o header `Range: items=0-1000` é aplicado globalmente no nível de infra).
|
|
353
|
+
|
|
354
|
+
### `POST /v3/mystery` — criar/atualizar (upsert)
|
|
355
|
+
|
|
356
|
+
| Aspecto | Detalhe |
|
|
357
|
+
|---|---|
|
|
358
|
+
| Finalidade | `insert` (L208-213). |
|
|
359
|
+
| Full replace ou patch | **Full replace** (Jongo `save` por `_id`). Omitir campo o apaga. |
|
|
360
|
+
| Status | `201 Created`, devolve a box já com `_id`/`options` recalculados. |
|
|
361
|
+
| Efeitos colaterais | Recalcula `options`, atribui `_id` às combinações, cria pasta se ausente. |
|
|
362
|
+
|
|
363
|
+
### `DELETE /v3/mystery/{id}` — remover box
|
|
364
|
+
|
|
365
|
+
`delete` (L94). Remove o documento de `mystery_box` (`remove {_id:#}`, L82). **Não** remove logs nem achievements relacionados. Status `200`.
|
|
366
|
+
|
|
367
|
+
### `GET /v3/mystery/execute/{id}?player={player}` — jogar (GET)
|
|
368
|
+
|
|
369
|
+
| Aspecto | Detalhe |
|
|
370
|
+
|---|---|
|
|
371
|
+
| Finalidade | Executa a box e retorna `MysteryResult` (L229-321). |
|
|
372
|
+
| `player=me` | Resolve o jogador a partir do token (L234-235). |
|
|
373
|
+
| Validações (REST, antes do manager) | box existe (400 se não), `active` (400 se `false`), `limit` de vitórias (400 se excedido), `attempts` (400 se excedido). |
|
|
374
|
+
|
|
375
|
+
### `POST /v3/mystery/execute` — jogar (POST, com `extra`)
|
|
376
|
+
|
|
377
|
+
Body: `{ "_id": "...", "player": "...", "extra": { ... } }` (L324-421). Permite anexar `extra` (gravado no log). `player=me` → token.
|
|
378
|
+
|
|
379
|
+
### `POST /v3/mystery/execute_by_player/{player}` — jogar por jogador na URL
|
|
380
|
+
|
|
381
|
+
Body: `{ "_id": "...", "extra": { ... } }` (L424-521). O jogador vem do path param (não do body). Mesmas validações.
|
|
382
|
+
|
|
383
|
+
### `DELETE /v3/mystery/execute` — rollback de jogada
|
|
384
|
+
|
|
385
|
+
| Aspecto | Detalhe |
|
|
386
|
+
|---|---|
|
|
387
|
+
| Finalidade | Desfaz uma jogada (`undoExecute`, L525-540 → L158-171). |
|
|
388
|
+
| Body | `{ "_id": "<achievementId da jogada>" }`. |
|
|
389
|
+
| Comportamento | Só age se o achievement for `type=6`. Remove os achievements de recompensa (`extra.origin == id`) **e** o achievement da jogada. |
|
|
390
|
+
| (!) Não faz | **Não** estorna `requirements` debitados; **não** remove o `mystery_box_log`. |
|
|
391
|
+
| Resposta | `200 {"message":"Execution Rollback Done"}` |
|
|
392
|
+
|
|
393
|
+
### `GET /v3/mystery/evaluate/{id}?player={player}&time={time}` — diagnóstico
|
|
394
|
+
|
|
395
|
+
| Aspecto | Detalhe |
|
|
396
|
+
|---|---|
|
|
397
|
+
| Finalidade | Avalia elegibilidade sem jogar (`evaluate` → `evaluateAnalyze`, L543-571 / L524-563). |
|
|
398
|
+
| `time` | Aceita epoch ms, data ISO/zoned, keyword (ex. `-1d`) ou vazio (→ agora) (L556-560). |
|
|
399
|
+
| Retorno | `{ params, exists, limit_wins, limit_attempts, limit, attempts, requirements, active, milliseconds }`. |
|
|
400
|
+
| (!) Bug | A chave `active` é **sempre `{}`** e o status de `active` é gravado por engano em `exists.status` (L556-558). Box inativa faz `exists.status="UNAUTHORIZED"` mesmo existindo (ver §7/§9). |
|
|
401
|
+
|
|
402
|
+
#### Exemplo de execução (request/response)
|
|
403
|
+
|
|
404
|
+
```
|
|
405
|
+
GET /v3/mystery/execute/heads_or_tails?player=ricardo@acme.com
|
|
406
|
+
Authorization: Bearer eyJhbGciOiJIUzUxMiIs...
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
```json
|
|
410
|
+
{
|
|
411
|
+
"mystery": "heads_or_tails",
|
|
412
|
+
"player": "ricardo@acme.com",
|
|
413
|
+
"time": "2026-05-20T13:40:11-03:00",
|
|
414
|
+
"result": ["heads"],
|
|
415
|
+
"status": "WIN",
|
|
416
|
+
"achievements": [
|
|
417
|
+
{ "_id": "aZ8...", "player": "ricardo@acme.com", "total": 1, "type": 6, "item": "heads_or_tails", "time": "..." },
|
|
418
|
+
{ "_id": "bY9...", "player": "ricardo@acme.com", "total": 10, "type": 0, "item": "coin", "time": "...",
|
|
419
|
+
"extra": { "origin": "aZ8...", "origin_item": "heads_or_tails", "origin_type": 6, "mystery_win_id": "o1" } }
|
|
420
|
+
]
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## 5. Regras de Negócio
|
|
427
|
+
|
|
428
|
+
Regras que **existem no código** e não no schema:
|
|
429
|
+
|
|
430
|
+
1. **`limit` = vitórias, `attempts` = tentativas.** `limit` conta `achievement type=6` (gravado só em WIN); `attempts` conta `mystery_box_log` (gravado em toda jogada). Ver §2.3.
|
|
431
|
+
2. **`limit.total` pode ser fórmula.** Aceita número **ou** string Mustache+exp4j avaliada por jogador, ex.: `"{{player.extra.max_plays}} * 2"` (L255-264). Erro de avaliação → 400 ("It was impossible to identify the limit…").
|
|
432
|
+
3. **Tentativa falha por requisito ainda gera log.** O `mystery_box_log` é gravado antes da verificação de `requirements` (L191-192 vs L195-202). Insuficiência lança 400 **depois** do log → conta como attempt.
|
|
433
|
+
4. **`limit`/`reward.limit` com `per:"team"` não é aplicado.** `checkRewardIsInLimit` só trata `PER_PLAYER` e `PER_GAME` (L483-511); com `team`, nenhum branch roda, `count=0` e o limite **nunca** bloqueia (silenciosamente permissivo).
|
|
434
|
+
5. **Só recompensas `type` 0/1/2 são creditadas.** Em vitória, o `a2` só persiste se `type` for `point(0)` → `point_category`, `challenge(1)` → `challenge`, `catalog_item(2)` → `catalog_item` (L366-390). Outros tipos: `a2` é criado em memória mas `collection==null` pula triggers e `addAchievement` — recompensa **silenciosamente não registrada**.
|
|
435
|
+
6. **Combinação multiset.** `orderSensitive=false` casa por contagem de tipos (subconjunto), não por igualdade — uma jogada pode disparar **múltiplas** linhas vencedoras (§3.3).
|
|
436
|
+
7. **NONE é implícito.** Se as probabilidades não somam 1 (e não há opção zero para absorver), há chance de `result` conter `null` (sem prêmio) — §2.4.
|
|
437
|
+
8. **Foot-gun de probabilidade > 1.** Se a soma das probabilidades informadas exceder 1, `prepareOptions` gera probabilidade **negativa** para opções zero, corrompendo o sorteio (`getChoice` pode nunca retornar certas opções / retornar `null` quase sempre). Não há validação que impeça isso. Ver §2.2.
|
|
438
|
+
9. **`inventory` é cap global** por `reward.type+item` (ou isolado por box / box+combinação se `isolated`). Conta `achievement` (L421-449).
|
|
439
|
+
10. **`updatePlayerStatus` sempre roda** ao final do execute (mesmo em LOSE / sem win_chart), desde que box e player existam (L414).
|
|
440
|
+
11. **Multi-tenant:** isolamento por API key resolvido no `FrontController.getInstance(apiKey)`; cada tenant tem seu `ManagerFactory`/conexão Jongo. O `synchronized` do `execute` é por instância (tenant), **não** distribuído.
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## 6. Comportamentos Automáticos
|
|
445
|
+
|
|
446
|
+
| Comportamento | Trigger/Origem | Impacto | Persistência |
|
|
447
|
+
|---|---|---|---|
|
|
448
|
+
| Gera `_id` da box | `insert` (L42-44) | id curto se ausente | `mystery_box` |
|
|
449
|
+
| `active=true` default | `insert` (L46-48) | box nasce ativa | `mystery_box` |
|
|
450
|
+
| Recalcula `options` | `prepareOptions` (L51) | probabilidades normalizadas, opções inválidas removidas, ordem alterada | `mystery_box` |
|
|
451
|
+
| Atribui `_id` às combinações | `prepareWinchart` (L54) | `o1`,`o2`,… | `mystery_box` |
|
|
452
|
+
| Cria pasta default | `insert` (L57-67) | doc em `mystery_folder` | `mystery_folder` |
|
|
453
|
+
| Log de tentativa | `execute` (L191-192) | toda jogada registrada | `mystery_box_log` |
|
|
454
|
+
| Débito de `requirements` | `execute` (L195-202) | custo deduzido (achievements negativos) | `achievement` |
|
|
455
|
+
| Achievement da jogada (`a1`) | `execute` em WIN (L330) | conta para `limit` | `achievement` (`type=6`) |
|
|
456
|
+
| Achievement da recompensa (`a2`) | `execute` por combinação (L386) | crédito ao jogador | `achievement` (point/challenge/catalog_item) |
|
|
457
|
+
| `updatePlayerStatus` | `execute` (L414) | recalcula status do jogador | `player_status` |
|
|
458
|
+
| Triggers | `execute` | ver fluxo abaixo | conforme handler |
|
|
459
|
+
|
|
460
|
+
### Encadeamento de triggers numa vitória com recompensa
|
|
461
|
+
|
|
462
|
+
Eventos disparados, **nesta ordem** (L320-409):
|
|
463
|
+
|
|
464
|
+
1. `before_win` — entidade `mystery_box` (a jogada `a1`).
|
|
465
|
+
2. `addAchievement(a1)` — persiste a jogada.
|
|
466
|
+
3. Para **cada** combinação vencedora que casa:
|
|
467
|
+
1. `before_win_reward` — entidade `mystery_box` ((!) **string literal hardcoded** em L376, **não** é constante `Trigger.EVENT_*`; grep por constante não acha).
|
|
468
|
+
2. `before_win` — entidade = **coleção da recompensa** (`point_category`/`challenge`/`catalog_item`).
|
|
469
|
+
3. `addAchievement(a2)` — persiste a recompensa.
|
|
470
|
+
4. `after_win` — entidade = coleção da recompensa (**após** persistir).
|
|
471
|
+
4. `after_win` — entidade `mystery_box`.
|
|
472
|
+
|
|
473
|
+
Em derrota: `before_lose` (antes do loop, que não credita nada) e `after_lose` (no fim) — entidade `mystery_box`.
|
|
474
|
+
|
|
475
|
+
```mermaid
|
|
476
|
+
flowchart LR
|
|
477
|
+
W{WIN?} -- sim --> BW["before_win (mystery_box)"]
|
|
478
|
+
BW --> AA["addAchievement(a1)"]
|
|
479
|
+
AA --> LP["loop win_chart casadas"]
|
|
480
|
+
LP --> BWR["before_win_reward<br/>(literal, mystery_box)"]
|
|
481
|
+
BWR --> BWC["before_win<br/>(colecao da recompensa)"]
|
|
482
|
+
BWC --> AA2["addAchievement(a2)"]
|
|
483
|
+
AA2 --> AWC["after_win<br/>(colecao da recompensa)"]
|
|
484
|
+
AWC --> AW["after_win (mystery_box)"]
|
|
485
|
+
W -- nao --> BL["before_lose (mystery_box)"]
|
|
486
|
+
BL --> AL["after_lose (mystery_box)"]
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
## 7. Suportado vs NÃO Suportado
|
|
492
|
+
|
|
493
|
+
### ✅ Suportado
|
|
494
|
+
|
|
495
|
+
- CRUD de mystery box (`GET`/`GET all`/`POST` upsert/`DELETE`).
|
|
496
|
+
- Execução por GET, POST (com `extra`) e `execute_by_player`.
|
|
497
|
+
- Sorteio ponderado por probabilidade, multi-coluna (`columns`).
|
|
498
|
+
- `win_chart` com combinação ordenada (`orderSensitive=true`) ou por contagem (`false`).
|
|
499
|
+
- Recompensas `point` (0), `challenge` (1), `catalog_item` (2).
|
|
500
|
+
- `limit` (vitórias) e `attempts` (jogadas) por `player` e `gamification`, com janela `every` e `total` numérico **ou** fórmula.
|
|
501
|
+
- `reward.limit` e `reward.inventory`, com isolamento por box/combinação (`isolated`).
|
|
502
|
+
- Exclusão dinâmica de opções com recompensa esgotada (somente `columns==1`).
|
|
503
|
+
- Rollback de jogada (`DELETE /v3/mystery/execute`).
|
|
504
|
+
- Diagnóstico (`GET /v3/mystery/evaluate/{id}`).
|
|
505
|
+
- Pastas (`mystery_folder`) com auto-criação.
|
|
506
|
+
- Triggers `before_win`/`after_win`/`before_lose`/`after_lose` + `before_win_reward`.
|
|
507
|
+
- Geração assistida por IA (`POST /v3/ai/build/mystery`) — **não persiste**, só devolve o JSON.
|
|
508
|
+
|
|
509
|
+
### ❌ NÃO Suportado / Pegadinhas
|
|
510
|
+
|
|
511
|
+
- **`type`** (`option_probability`/`combination_probability`) — não existe na entidade; **descartado** silenciosamente (`@JsonIgnoreProperties`).
|
|
512
|
+
- **`probability` por combinação** em `win_chart` — não existe em `MysteryWin`; **descartado**. Odds vêm de `options`×`columns`.
|
|
513
|
+
- **`reward.operation`** — ignorado; só `total/type/item` são usados (comentário L21 de `MysteryWin`).
|
|
514
|
+
- **Recompensa com `type` ≠ 0/1/2** (level, crown, lottery, etc.) — `a2` criado mas **nunca persistido** (collection null).
|
|
515
|
+
- **`limit.per = "team"`** em `limit` da box e em `reward.limit` — branch inexistente; limite **nunca aplicado** (permissivo). `evaluateLimitAnalyze`/`evaluateAttemptsAnalyze` também só tratam `player`/`gamification`.
|
|
516
|
+
- **`Limit.query`** — campo existe no POJO `Limit` mas **nunca é consumido** por nenhuma verificação de mystery.
|
|
517
|
+
- **`MysteryBox.extra`** — persistido, mas **nunca lido** pela lógica do módulo (passthrough).
|
|
518
|
+
- **`MysteryBox.techniques`** — legado; nunca escrito/lido pelo módulo; sem GT code (§3.7).
|
|
519
|
+
- **`MysteryBoxFolder.java`** — classe **morta**, duplicata de `MysteryFolder`, sem referências.
|
|
520
|
+
- **`PUT`** — inexistente; atualização é re-`POST` (full replace).
|
|
521
|
+
- **404 em id inexistente** — `GET /{id}` devolve `200` com `null`.
|
|
522
|
+
- **Proteção de recompensa esgotada para `columns > 1`** — não existe (só `columns==1`).
|
|
523
|
+
- **Validação de probabilidade > 1** — inexistente; gera probabilidade negativa (foot-gun, §2.2/§5).
|
|
524
|
+
- **Estorno de `requirements` no rollback** — `undoExecute` não devolve o custo nem apaga o log.
|
|
525
|
+
- **Bug no `evaluate`**: chave `active` sempre `{}`; status de `active` sobrescreve `exists.status` (L556-558).
|
|
526
|
+
- **Copy-paste em `evaluateAttemptsAnalyze`**: mensagem de erro referencia `a.limit.total` em vez de `a.attempts.total` (L674) — NPE se `limit==null` e `attempts.total` for string malformada.
|
|
527
|
+
- **Scheduler/job automático** — não existe para mystery (toda execução é via endpoint).
|
|
528
|
+
|
|
529
|
+
---
|
|
530
|
+
|
|
531
|
+
## 8. Segurança e Permissões
|
|
532
|
+
|
|
533
|
+
- **Autenticação:** Bearer token em todos os endpoints (`@BeanParam AuthBean`). Operações de CRUD/`delete` exigem credenciais de administrador (token do jogo); `execute` pode usar `player=me` resolvendo o jogador pelo token.
|
|
534
|
+
- **Isolamento multi-tenant:** resolvido por `FrontController.getInstance(apiKey)` → `ManagerFactory`/conexão Jongo por tenant. Não há vazamento entre organizações desde que a API key esteja correta.
|
|
535
|
+
- **Concorrência:** `execute` é `synchronized` na instância do manager (por tenant/JVM). **Não** há lock distribuído — sob múltiplos pods, dois `execute` simultâneos podem ultrapassar `limit`/`inventory` (condição de corrida em estoques pequenos). Mitigação parcial: o `limit` é re-checado tanto no REST quanto no manager, mas sem atomicidade.
|
|
536
|
+
- **Injeção:** as queries MongoDB usam **parâmetros posicionais** do Jongo (`"{_id:#}"`, `count("{type:#,...}", ...)`), o que evita injeção via valores. Porém **`limit.total`/`attempts.total` como fórmula** são avaliados por `MustacheUtils.parse` + `exp4j` — uma fórmula maliciosa configurada por um admin pode lançar exceção (tratada → 400) ou consumir CPU; o conteúdo é definido por administrador, não pelo jogador final, então a superfície é limitada a quem já tem acesso de configuração.
|
|
537
|
+
- **Comportamento permissivo inseguro documentado:** `reward.limit.per="team"` **não bloqueia nada** (§5/§7) — não confie nele para conter distribuição de prêmios.
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
## 9. Observabilidade e Troubleshooting
|
|
542
|
+
|
|
543
|
+
### Diagnóstico rápido
|
|
544
|
+
|
|
545
|
+
```
|
|
546
|
+
GET /v3/mystery/{id} # a box existe? (200 null = não existe)
|
|
547
|
+
GET /v3/mystery/evaluate/{id}?player=ME # elegibilidade: limit_wins, limit_attempts, requirements
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
> Ao ler o `evaluate`, lembre-se do bug do `active` (§4/§7): a chave `active` vem vazia e, se a box estiver inativa, `exists.status` aparecerá como `UNAUTHORIZED`.
|
|
551
|
+
|
|
552
|
+
### Queries MongoDB úteis
|
|
553
|
+
|
|
554
|
+
```js
|
|
555
|
+
// jogadas (tentativas) de um jogador numa box
|
|
556
|
+
db.mystery_box_log.find({ item: "<boxId>", player: "<player>" }).sort({ time: -1 })
|
|
557
|
+
|
|
558
|
+
// vitórias contam aqui (type=6)
|
|
559
|
+
db.achievement.count({ type: 6, item: "<boxId>", player: "<player>" })
|
|
560
|
+
|
|
561
|
+
// recompensas creditadas por esta box
|
|
562
|
+
db.achievement.find({ "extra.origin_item": "<boxId>" })
|
|
563
|
+
|
|
564
|
+
// recompensas de uma combinação específica
|
|
565
|
+
db.achievement.find({ "extra.origin_item": "<boxId>", "extra.mystery_win_id": "o1" })
|
|
566
|
+
|
|
567
|
+
// estoque já distribuído de uma recompensa (inventory)
|
|
568
|
+
db.achievement.count({ type: 0, item: "coin" })
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### Erros comuns e causas
|
|
572
|
+
|
|
573
|
+
| Sintoma | Causa provável |
|
|
574
|
+
|---|---|
|
|
575
|
+
| `400 insufficient requirements` | Jogador não tem o custo (`requirements`) — mas o `mystery_box_log` já foi gravado. |
|
|
576
|
+
| `400 You have exced the limit … earned` | `limit` (vitórias) atingido no período `every`. |
|
|
577
|
+
| `400 You have exced the limit … executed` | `attempts` (jogadas) atingido. |
|
|
578
|
+
| `400 mysterybox is not active` | `active=false` explícito. |
|
|
579
|
+
| `400 mysterybox does not exist` | id inválido no `execute` (no `GET /{id}` retornaria 200/null). |
|
|
580
|
+
| Prêmio não creditado mesmo "ganhando" | `reward.type` ∉ {0,1,2}; ou `limit`/`inventory` da recompensa esgotado (log no console: `"MysteryWin not registered because of limit"`). |
|
|
581
|
+
| Limite "não funciona" | `reward.limit.per="team"` (não suportado); ou soma de probabilidades > 1 corrompendo o sorteio. |
|
|
582
|
+
| Opção nunca sai / `result` quase sempre `null` | Probabilidades somam > 1 → probabilidade negativa em opções zero (§2.2). |
|
|
583
|
+
| Resultado contém `null` | Probabilidades somam < 1 → faixa NONE (esperado, §2.4). |
|
|
584
|
+
|
|
585
|
+
### O que verificar quando "não funciona"
|
|
586
|
+
|
|
587
|
+
1. A box está `active`? (`GET /{id}` → campo `active`, lembrando que `null` = ativa).
|
|
588
|
+
2. As probabilidades das `options` somam ≤ 1? Há opções `>1`/`<0` que foram descartadas no save?
|
|
589
|
+
3. A combinação da `win_chart` realmente casa com o `result`? Confira `orderSensitive`.
|
|
590
|
+
4. O `reward.type` é 0/1/2? Caso contrário não é creditado.
|
|
591
|
+
5. `limit`/`inventory` da recompensa esgotou? Veja o console (`MysteryWin not registered because of limit`).
|
|
592
|
+
|
|
593
|
+
---
|
|
594
|
+
|
|
595
|
+
## 10. Exemplos Práticos
|
|
596
|
+
|
|
597
|
+
### 10.1 Mínimo funcional — cara ou coroa
|
|
598
|
+
|
|
599
|
+
```json
|
|
600
|
+
POST /v3/mystery
|
|
601
|
+
{
|
|
602
|
+
"_id": "heads_or_tails",
|
|
603
|
+
"title": "Heads or Tails",
|
|
604
|
+
"options": [
|
|
605
|
+
{ "title": "Heads", "value": "heads", "probability": 0.5 },
|
|
606
|
+
{ "title": "Tails", "value": "tails", "probability": 0.5 }
|
|
607
|
+
],
|
|
608
|
+
"columns": 1,
|
|
609
|
+
"win_chart": [
|
|
610
|
+
{ "combination": ["heads"], "orderSensitive": false,
|
|
611
|
+
"reward": { "total": 1, "type": 0, "item": "coin" } }
|
|
612
|
+
]
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
Jogar: `GET /v3/mystery/execute/heads_or_tails?player=me`.
|
|
617
|
+
|
|
618
|
+
### 10.2 Avançado — slot 3 colunas, custo, limites e estoque
|
|
619
|
+
|
|
620
|
+
```json
|
|
621
|
+
POST /v3/mystery
|
|
622
|
+
{
|
|
623
|
+
"_id": "office_prizes",
|
|
624
|
+
"title": "Office Prizes",
|
|
625
|
+
"options": [
|
|
626
|
+
{ "title": "Notebook", "value": "notebook", "probability": 0.1 },
|
|
627
|
+
{ "title": "Backpack", "value": "backpack", "probability": 0.3 },
|
|
628
|
+
{ "title": "Mouse", "value": "mouse", "probability": 0.6 }
|
|
629
|
+
],
|
|
630
|
+
"columns": 3,
|
|
631
|
+
"requirements": [
|
|
632
|
+
{ "total": 4, "type": 0, "item": "coin", "operation": 1 }
|
|
633
|
+
],
|
|
634
|
+
"win_chart": [
|
|
635
|
+
{ "combination": ["notebook","notebook","notebook"], "orderSensitive": false,
|
|
636
|
+
"reward": { "total": 1, "type": 2, "item": "notebook" },
|
|
637
|
+
"inventory": 5, "isolated": true,
|
|
638
|
+
"limit": { "total": 1, "per": "player", "every": "1M" } },
|
|
639
|
+
{ "combination": ["mouse","mouse","mouse"], "orderSensitive": false,
|
|
640
|
+
"reward": { "total": 1, "type": 2, "item": "mouse" } }
|
|
641
|
+
],
|
|
642
|
+
"limit": { "total": 1, "per": "player", "every": "1M" },
|
|
643
|
+
"attempts": { "total": 1, "per": "player", "every": "1d" }
|
|
644
|
+
}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
- `requirements`: custa 4 `coin` por jogada (`operation:1` = deduct).
|
|
648
|
+
- `limit`: máx. **1 vitória** por jogador por mês.
|
|
649
|
+
- `attempts`: máx. **1 jogada** por jogador por dia.
|
|
650
|
+
- `inventory: 5` + `isolated`: só 5 notebooks distribuídos por esta box.
|
|
651
|
+
|
|
652
|
+
### 10.3 Anti-pattern — o que NÃO fazer
|
|
653
|
+
|
|
654
|
+
```json
|
|
655
|
+
{
|
|
656
|
+
"_id": "broken_box",
|
|
657
|
+
"title": "Broken",
|
|
658
|
+
"type": "option_probability", // (X) ignorado (não existe na entidade)
|
|
659
|
+
"options": [
|
|
660
|
+
{ "value": "a", "probability": 0.7 },
|
|
661
|
+
{ "value": "b", "probability": 0.6 } // (X) soma 1.3 > 1 -> prob. negativa, sorteio corrompido
|
|
662
|
+
],
|
|
663
|
+
"columns": 3,
|
|
664
|
+
"win_chart": [
|
|
665
|
+
{ "combination": ["a","a","a"],
|
|
666
|
+
"probability": 0.01, // (X) ignorado (não existe em MysteryWin)
|
|
667
|
+
"reward": { "total": 1, "type": 3, "item": "lvl" }, // (X) type=3 (level) nunca é creditado
|
|
668
|
+
"limit": { "total": 5, "per": "team" } // (X) per=team não bloqueia nada
|
|
669
|
+
}
|
|
670
|
+
]
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
Por que é errado:
|
|
675
|
+
|
|
676
|
+
- `type` no topo e `probability` na combinação são **descartados** silenciosamente.
|
|
677
|
+
- Probabilidades das `options` somam **> 1** → `prepareOptions` quebra o sorteio.
|
|
678
|
+
- `reward.type=3` não está em {0,1,2} → o prêmio **nunca** é registrado.
|
|
679
|
+
- `limit.per="team"` é **silenciosamente ignorado** → sem limite real.
|
|
680
|
+
- Sem `_id` nas combinações, mas isso é OK (o sistema gera `o1`,`o2`…) — só não dependa dele antes do primeiro save.
|
|
681
|
+
|
|
682
|
+
---
|
|
683
|
+
|
|
684
|
+
## Checklist de Configuração
|
|
685
|
+
|
|
686
|
+
- [ ] `title` preenchido.
|
|
687
|
+
- [ ] `options` com `value` único por opção; probabilidades somando **≤ 1** (evite `> 1` → quebra o sorteio).
|
|
688
|
+
- [ ] Nenhuma opção com `probability > 1` ou `< 0` (seriam **descartadas** no save).
|
|
689
|
+
- [ ] `columns` definido conforme o jogo (1 = roleta simples; >1 = slot).
|
|
690
|
+
- [ ] `win_chart[].combination` usa exatamente os `value`s das `options`.
|
|
691
|
+
- [ ] `orderSensitive` definido conscientemente (`false` = contagem por tipo, pode casar várias linhas).
|
|
692
|
+
- [ ] `reward.type` ∈ {0 (point), 1 (challenge), 2 (catalog_item)} — outros tipos **não** são creditados.
|
|
693
|
+
- [ ] Itens/pontos/desafios de `reward.item` e `requirements.item` **existem** antes de criar a box.
|
|
694
|
+
- [ ] Para limites por jogador/jogo use `per: "player"` ou `"gamification"` — **`team` não funciona**.
|
|
695
|
+
- [ ] Diferencie `limit` (vitórias) de `attempts` (jogadas): se quer limitar quantas vezes pode jogar, use `attempts`.
|
|
696
|
+
- [ ] Ciente de que `extra`, `techniques` e `type` no payload são metadados/ignorados.
|
|
697
|
+
- [ ] Ciente de que `POST` é **full replace** — reenvie o documento completo ao atualizar.
|
|
698
|
+
- [ ] Ciente de que rollback (`DELETE /execute`) **não** estorna `requirements`.
|
|
699
|
+
|
|
700
|
+
## Steps
|
|
701
|
+
|
|
702
|
+
### Rules — follow exactly, no exceptions
|
|
703
|
+
|
|
704
|
+
**Technique:** always `["GT72"]`.
|
|
705
|
+
**`options[].probability` must sum to exactly `1.0`** — build will not enforce, runtime will produce wrong results.
|
|
706
|
+
**`win_chart[].combination`:** array of option values that triggers the reward.
|
|
707
|
+
**Evaluate before execute:** `GET /v3/mystery/evaluate/:id?player=<id>` checks eligibility.
|
|
708
|
+
|
|
709
|
+
---
|
|
710
|
+
|
|
711
|
+
### 1. Check if mystery box already exists
|
|
712
|
+
|
|
713
|
+
```
|
|
714
|
+
funifier_list type=mystery search=<title>
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
### 2. Design options and win combinations
|
|
718
|
+
|
|
719
|
+
Coin flip example (50/50):
|
|
720
|
+
```json
|
|
721
|
+
{
|
|
722
|
+
"title": "Coin Flip",
|
|
723
|
+
"options": [
|
|
724
|
+
{ "title": "Heads", "value": "heads", "probability": 0.5 },
|
|
725
|
+
{ "title": "Tails", "value": "tails", "probability": 0.5 }
|
|
726
|
+
],
|
|
727
|
+
"columns": 1,
|
|
728
|
+
"requirements": [],
|
|
729
|
+
"win_chart": [
|
|
730
|
+
{
|
|
731
|
+
"combination": ["heads"],
|
|
732
|
+
"orderSensitive": false,
|
|
733
|
+
"reward": { "total": 10, "type": 0, "item": "coin" }
|
|
734
|
+
}
|
|
735
|
+
],
|
|
736
|
+
"techniques": ["GT72"]
|
|
737
|
+
}
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
Spin wheel with 3 slots (unequal odds):
|
|
741
|
+
```json
|
|
742
|
+
{
|
|
743
|
+
"title": "Spin Wheel",
|
|
744
|
+
"options": [
|
|
745
|
+
{ "title": "Gold", "value": "gold", "probability": 0.1 },
|
|
746
|
+
{ "title": "Silver", "value": "silver", "probability": 0.3 },
|
|
747
|
+
{ "title": "Bronze", "value": "bronze", "probability": 0.6 }
|
|
748
|
+
],
|
|
749
|
+
"columns": 3,
|
|
750
|
+
"win_chart": [
|
|
751
|
+
{ "combination": ["gold", "gold", "gold"], "orderSensitive": true, "reward": { "total": 100, "type": 0, "item": "coin" } },
|
|
752
|
+
{ "combination": ["silver", "silver", "silver"], "orderSensitive": true, "reward": { "total": 50, "type": 0, "item": "coin" } }
|
|
753
|
+
],
|
|
754
|
+
"techniques": ["GT72"]
|
|
755
|
+
}
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
### 3. Save
|
|
759
|
+
|
|
760
|
+
```
|
|
761
|
+
funifier_save type=mystery payload=<json>
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
### 4. Test
|
|
765
|
+
|
|
766
|
+
```
|
|
767
|
+
GET /v3/mystery/evaluate/<_id>?player=<player_id>
|
|
768
|
+
GET /v3/mystery/execute/<_id>?player=<player_id>
|
|
769
|
+
```
|