NON-PRODUCTION

Plinko — tài liệu ghép client

Thả bóng một shot. Chọn row 8/12/16 và risk 1/2/3. Server trừ bet và cộng win trong cùng một response. Trang này chỉ được serve khi ENVIRONMENT != production.

1. Tổng quan

MụcGiá trị
Boardsconfig[{row, risk, multipliers[]}] từ /gameplay
Row8 · 12 · 16 — số hàng peg. Số bucket = multipliers.length (= row + 1)
Risk1 low · 2 medium · 3 high. Cùng row, risk khác → bảng hệ số khác
PositionIndex bucket đích (0 = mép trái). Dùng để animate bóng rơi
Tiền−bet (BET) rồi +win_amount (WIN) cùng response. user_money đã net
Winwin_amount = multiplier × bet (float × int64, server round)
Base path/api/v1
Một drop = một HTTP. Không có session giữa chừng. Vẫn gọi GET /gameplay trước drop đầu để lấy multiplier table và set session (thiếu → code 69).

2. Demo peg pyramid

Hàng peg lệch nhau. Bucket vàng = bóng vừa rơi vào (position = 4, risk low row 8).

29
4
1.5
0.3
0.2
0.3
1.5
4
29

Index bucket 0…8 trái → phải. Animate path tới position rồi hiện multiplier × bet = win_amount. Đừng tự random bucket.

3. Auth & envelope

Authorization: Bearer <access_token>
Content-Type: application/json

Mọi API dưới /api/v1 cần JWT. HTTP 200 + code = 0 là thành công; payload nằm trong msg.

{
  "code": 0,
  "msg": { ... }
}
codeÝ nghĩa
0OK
-1Unknown / thiếu JWT / token invalid (HTTP 401)
-2Invalid input (body JSON sai)
69Wrong game state — chưa /gameplay, bet không thuộc bets, cặp row/risk không có trong config
1000Popup error (i18n)
POST /bet bị middleware.Block: user đang room PvP khác → HTTP 400 body "blocked".

4. GET /api/v1/gameplay

Gọi khi vào game. Trả bảng hệ số cho mọi (row, risk). Client render selector + paytable từ đây, không hardcode.

FieldTypeClient dùng để
user_moneyint64Chip hiện tại
betsint64[]Mức bet hợp lệ
show_betsint64[]Giá trị hiển thị (cùng index)
configBoard[]9 board: 3 row × 3 risk
config[].rowint8 / 12 / 16
config[].riskint1 / 2 / 3
config[].multipliersfloat[]Hệ số từng bucket trái → phải. Length = số bucket
{
  "code": 0,
  "msg": {
    "user_money": 500000,
    "bets": [200, 1000, 2000, 10000],
    "show_bets": [200, 1000, 2000, 10000],
    "config": [
      {"row": 8, "risk": 1, "multipliers": [29, 4, 1.5, 0.3, 0.2, 0.3, 1.5, 4, 29]},
      {"row": 8, "risk": 2, "multipliers": [13, 3, 1.3, 0.7, 0.4, 0.7, 1.3, 3, 13]},
      {"row": 8, "risk": 3, "multipliers": [5.6, 2.1, 1.1, 1, 0.5, 1, 1.1, 2.1, 5.6]},
      {"row": 12, "risk": 1, "multipliers": [170, 24, 8.1, 2, 0.7, 0.2, 0.2, 0.2, 0.7, 2, 8.1, 24, 170]},
      {"row": 16, "risk": 1, "multipliers": [1000, 130, 26, 9, 4, 2, 0.2, 0.2, 0.2, 0.2, 0.2, 2, 4, 9, 26, 130, 1000]}
    ]
  }
}

Response thật đủ 9 board. Rút gọn ở đây. Risk 2/3 của row 12/16 cũng nằm trong config.

5. POST /api/v1/bet

Một drop. Debit bet rồi credit win trong cùng handler. user_money đã là số dư sau cả hai bước.

RequestTypeÝ nghĩa
betint64Phải thuộc bets
rowint8 / 12 / 16
riskint1 / 2 / 3
cheat_positionint?Chỉ non-prod / cheater. Server chỉ nhận khi > 0 (bucket 0 không cheat được qua field này)
ResponseTypeÝ nghĩa
user_moneyint64Chip sau −bet +win
betint64Bet đã dùng
row, riskintEcho board
positionintBucket đích — animate bóng tới đây
multiplierfloatconfig.multipliers[position]
win_amountint64Chip thắng (có thể < bet nếu multiplier < 1)
POST /api/v1/bet
{ "bet": 1000, "row": 8, "risk": 1 }

{
  "code": 0,
  "msg": {
    "user_money": 499200,
    "bet": 1000,
    "row": 8,
    "risk": 1,
    "position": 4,
    "multiplier": 0.2,
    "win_amount": 200
  }
}
Ví dụ trên: −1000 rồi +200 → net −800, user_money phản ánh net. High multiplier (bucket 0 = 29×) thì win lớn hơn bet.
{
  "code": 0,
  "msg": {
    "user_money": 528000,
    "bet": 1000,
    "row": 8,
    "risk": 1,
    "position": 0,
    "multiplier": 29,
    "win_amount": 29000
  }
}

Cheat (non-prod)

{ "bet": 1000, "row": 8, "risk": 1, "cheat_position": 8 }

Production bỏ qua field này, luôn random. Client không gửi trên build live. cheat_position phải > 0 và < số bucket.

Số bucket: row 8 → 9, row 12 → 13, row 16 → 17. Index 0 = mép trái (thường multiplier cao nhất với risk low).

6. GET /top-win · GET /history

APImsg
GET /api/v1/top-winwinners[{rank, user_id, username, total_win}]
GET /api/v1/historylogs[{user_money, win_amount, bet, multiplier, row, created_at}]
GET /api/v1/top-win
{
  "code": 0,
  "msg": { "winners": [{ "rank": 1, "user_id": 1001, "username": "alice", "total_win": 2400000 }] }
}

GET /api/v1/history
{
  "code": 0,
  "msg": {
    "logs": [
      { "user_money": 499200, "win_amount": 200, "bet": 1000, "multiplier": 0.2, "row": 8, "created_at": 1720000000 }
    ]
  }
}

History không có field risk / position. created_at là unix second.

7. Luồng ghép

  1. Mở game → GET /gameplay. Render selector row/risk, vẽ bucket theo multipliers, hiện money + bet chips.
  2. User chọn bet (trong bets), row, risk. Disable Drop nếu user_money < bet.
  3. POST /bet. Khóa nút tới khi xong animation.
  4. Animate bóng rơi (peg pyramid) tới bucket position. Highlight bucket đó.
  5. Hiện multiplierwin_amount. Cập nhật chip = user_money (đã net).
  6. Mở khóa Drop. Không cần gọi lại /gameplay trừ khi code 69 / reconnect.

8. Error & tiền

Tình huốngHành vi client
HTTP 401Token hết hạn — relogin
code 69GET /gameplay lại, đồng bộ bets/config
code -1 khi betHết tiền hoặc lỗi nội bộ — refresh money
HTTP 400 blockedUser đang PvP — không cho drop
Optimistic UI trừ bet trước response sẽ lệch vì win cũng credit cùng lúc. Chỉ tin user_money trên response.

9. Checklist client