game.json mô tả game của bạn cho portal: tên, mô tả, phân loại, hướng màn hình, ảnh đại diện và các giới hạn điểm mà server dùng để chống gian lận. File nằm ở gốc thư mục game, cạnh thư mục build. Đội vận hành kiểm tra nó bằng wsg validate trước khi phát hành; trường nào không có sẽ nhận giá trị mặc định.
Ví dụ đầy đủ
{
"slug": "flappy-demo",
"version": "1.0.0",
"title": "Flappy Demo",
"description": {
"vi": "Chạm để bay qua ống. Ống càng lúc càng hẹp.",
"en": "Tap to fly through the pipes. Gaps get tighter."
},
"category": "arcade",
"tags": ["flappy", "one-tap", "pixel", "tap"],
"orientation": "portrait",
"controls": ["tap", "keyboard"],
"thumbnail": "./thumbnail.png",
"cover": null,
"sdk": { "major": 1 },
"limits": {
"maxScore": 9999,
"maxScorePerSecond": 3,
"minDurationMs": 1000,
"scoreOrder": "desc"
},
"build": { "command": "pnpm build", "outDir": "dist" },
"features": { "leaderboard": true, "rewarded": true, "interstitial": true, "storage": true },
"minPlayers": 1,
"maxPlayers": 1,
"author": { "name": "WSG", "url": null },
"generated": { "by": null, "pipelineRunId": null }
}game.json là JSON thuần: không có comment, không có dấu phẩy thừa.
Các trường
| Trường | Kiểu | Bắt buộc | Mặc định | Ràng buộc |
|---|---|---|---|---|
slug | string | Có | — | Khớp ^_?[a-z0-9][a-z0-9-]{1,48}$: 2–49 ký tự chữ thường, số, gạch ngang, không bắt đầu bằng gạch ngang. Tiền tố _ dành cho game nội bộ. Phải trùng tên thư mục game |
version | string | Có | — | Semver MAJOR.MINOR.PATCH, cho phép hậu tố như 1.2.0-beta.1. Mỗi lần phát hành dùng một version mới |
title | string | Có | — | 1–80 ký tự. Một chuỗi duy nhất, không theo ngôn ngữ |
description | { vi, en } | Có | — | Cả vi và en đều bắt buộc, mỗi bản 1–500 ký tự |
category | string | Có | — | Một slug trong bảng category |
tags | string[] | Có | — | 1–8 slug trong bảng tag; slug lặp lại bị cảnh báo |
orientation | string | Có | — | portrait, landscape hoặc any |
controls | string[] | Có | — | Ít nhất một trong tap, swipe, keyboard, mouse, gamepad |
thumbnail | string | Có | — | Đường dẫn tương đối trong thư mục game tới ảnh .png, .jpg, .jpeg hoặc .webp; 512×512, tối đa 150 KB |
cover | string | null | Không | null | Đường dẫn tương đối tới ảnh bìa, nên là 1200×630 |
sdk.major | integer | Không | 1 | ≥ 1; major của SDK game dùng |
limits | object | Có | — | Xem Chọn limits |
build.command | string | Không | "pnpm build" | Lệnh build, chạy trong thư mục game |
build.outDir | string | Không | "dist" | Thư mục chứa bundle, phải nằm trong thư mục game; "." nghĩa là không có bước build |
features | object | Không | cả bốn true | leaderboard, rewarded, interstitial, storage dạng boolean |
minPlayers, maxPlayers | integer | Không | 1, 1 | ≥ 1 và minPlayers ≤ maxPlayers |
author.name | string | Không | "unknown" | 1–80 ký tự |
author.url | string | null | Không | null | URL hợp lệ hoặc null |
generated | object | Không | { "by": null, "pipelineRunId": null } | Dành cho pipeline sinh game tự động; để null |
Ghi chú về một số trường:
sluglà định danh vĩnh viễn của game: đường dẫn bundle/<slug>/<version>/, tranghttps://leonogames.com/games/<slug>vàgameIdtruyền vàoWSG.init(). Đổi slug là tạo một game khác.orientationquyết định khung iframe trên portal:portraitgiữ tỉ lệ 9:16 và cao tối đa bằng màn hình,landscapegiữ 16:9,anylấp đầy vùng chơi. Trên điện thoại, portal gợi ý xoay máy khi người chơi cầm sai chiều.thumbnailvàcoverchỉ dùng cho trang danh sách, không cần nằm trong bundle. Khi phát hành, ảnh được tải lênhttps://games.leonogames.com/<slug>/media/vàgame.jsongửi tới API mang URL thật.wsg validatechỉ đọc được kích thước của ảnh PNG; ảnh định dạng khác chỉ nhận cảnh báo.featureschỉ được phản ánh quaWSG.isAvailable(); portal hiện không chặn lời gọi theo các cờ này.build: với gói nộp đã build sẵn, đặtcommandlà lệnh không làm gì (ví dụ"echo prebuilt") vàoutDirtrỏ tới thư mục build. Xem Nộp và phát hành.
Category
category nhận đúng một giá trị trong bảng sau:
| Slug | Tên |
|---|---|
arcade | Arcade |
puzzle | Giải đố |
action | Hành động |
casual | Giải trí |
sports | Thể thao |
racing | Đua xe |
strategy | Chiến thuật |
card-board | Bài và cờ |
kids | Trẻ em |
io | .io |
Tag
tags nhận 1–8 slug thuộc bất kỳ nhóm nào dưới đây. Trường controls chỉ nhận các slug của nhóm điều khiển. Slug swipe có ở cả nhóm cơ chế và nhóm điều khiển nhưng chỉ là một giá trị.
| Nhóm | Slug hợp lệ |
|---|---|
Thể loại (genre) | endless-runner, flappy, match-3, merge, stack, snake, breakout, 2048, shooter, platformer, idle, quiz, word |
Cơ chế (mechanic) | one-tap, timing, physics, drag-drop, swipe, aim, rhythm |
Chủ đề (theme) | space, animals, food, pixel, neon, fantasy, sports |
Điều khiển (controls) | tap, swipe, keyboard, mouse, gamepad |
Taxonomy được mở rộng dần; cần category hay tag mới, hãy đề xuất với đội vận hành thay vì dùng giá trị gần đúng.
Chọn limits
Server dùng limits để từ chối điểm không thể đạt được bằng cách chơi thật. Đặt quá chặt thì người chơi giỏi bị từ chối oan; quá lỏng thì bảng xếp hạng dễ bị gian lận. Luật đầy đủ ở trang Điểm và bảng xếp hạng.
| Trường | Kiểu | Ý nghĩa | Lý do từ chối |
|---|---|---|---|
maxScore | integer, 0–2^53 | Điểm cao nhất một lượt có thể đạt | over_max |
maxScorePerSecond | number > 0 | Tốc độ ghi điểm tối đa: điểm chia cho số giây chơi | rate_too_high |
minDurationMs | integer ≥ 0 | Thời gian chơi tối thiểu trước khi nộp điểm | too_fast |
scoreOrder | desc | asc | desc (mặc định): cao hơn là tốt hơn; asc: thấp hơn là tốt hơn, ví dụ thời gian | — |
Server đo thời gian chơi (elapsed) từ lần gameplayStart() gần nhất tới lúc nhận điểm, tối thiểu 1 giây. Vì vậy minDurationMs từ 1000 trở xuống không bao giờ gây too_fast (wsg validate cảnh báo khi giá trị dưới 1000).
Cách làm khuyến nghị:
- Tính tốc độ ghi điểm tối đa từ thiết kế, không từ tay người: nhịp sinh vật phẩm, thời gian khoá giữa hai lần ghi điểm, điểm thưởng cuối màn. Nếu game cho phép bấm nhanh không giới hạn, hãy thêm giới hạn trong game.
- Nhân 1,5 để có biên, ra
maxScorePerSecond. maxScorelà điểm cao nhất có thể đạt; game không có trần (chạy vô tận) thì ước lượng thật rộng, vìmaxScorePerSecondđã giới hạn điểm theo thời gian. Nên kẹp điểm trong game ởmaxScoređể không bao giờ nộp vượt.minDurationMslà thời gian nhanh nhất một lượt hợp lệ có thể kết thúc, trừ đi vài giây cho độ trễ mạng.
Ví dụ tính
Chạy vô tận. +1 điểm mỗi 300 ms sống sót (3,33 điểm/giây) và +5 cho mỗi đồng xu; game sinh tối đa một đồng xu mỗi 2 giây (≤ 2,5 điểm/giây). Tốc độ tối đa là 5,83 điểm/giây, nhân 1,5 ra maxScorePerSecond: 9. Chết sớm nhất sau khoảng 4 giây, chọn minDurationMs: 3000. Điểm kẹp trong game ở 5000, nên maxScore: 5000.
Giải đố có thưởng cuối màn. Mỗi cặp thẻ khớp +10, mỗi lần lật phải chờ ít nhất 750 ms, qua màn được thưởng thêm. Bot biết hết đáp án qua màn 1 được 148 điểm trong 6,9 giây, tức 21,4 điểm/giây; đặt maxScorePerSecond: 25 (biên nhỏ hơn 1,5 vì con số này đã là giới hạn tuyệt đối của thiết kế). Lưu ý điểm thưởng dồn vào cuối màn vẫn được chia cho toàn bộ thời gian từ gameplayStart().
Đua thời gian (asc). Điểm là số mili giây hoàn thành màn. Vì điểm xấp xỉ bằng thời gian chơi, điểm chia cho số giây luôn gần 1000, nên maxScorePerSecond phải từ 1000 trở lên, ví dụ 1200. minDurationMs là thời gian hoàn thành nhanh nhất có thể (ví dụ 20000), maxScore là thời gian chậm nhất còn được ghi nhận (ví dụ 600000, tức 10 phút).
{
"limits": { "maxScore": 600000, "maxScorePerSecond": 1200, "minDurationMs": 20000, "scoreOrder": "asc" }
}Cảnh báo
Các luật hiện tại được thiết kế cho desc. Với asc, một thời gian khai báo thấp hơn thực tế (ví dụ chơi 30 giây nhưng nộp 1 ms) vẫn qua cả ba kiểm tra. Nếu bảng xếp hạng quan trọng, cân nhắc quy đổi sang điểm desc, ví dụ điểm = số giây còn lại × 10.
Kiểm tra game.json
Đội vận hành (hoặc bạn, nếu có repo) chạy wsg validate trên thư mục game. Lệnh kiểm tra: JSON hợp lệ và đúng schema; slug trùng tên thư mục; category và tags có trong taxonomy (tag trùng chỉ cảnh báo); ảnh thumbnail tồn tại trong thư mục game, đúng định dạng, tối đa 150 KB, đúng 512×512 nếu là PNG; cover nên là 1200×630 (chỉ cảnh báo); build.outDir nằm trong thư mục game; minPlayers ≤ maxPlayers; minDurationMs dưới 1000 thì cảnh báo.
$ pnpm wsg validate tap-rush
OK game.json hợp lệ: tap-rush@1.0.0 "Tap Rush"
OK thumbnail ./thumbnail.png 512×512, 3.6 KB
validate: OK (0 lỗi, 0 cảnh báo)$ pnpm wsg validate tap-rush
ERR game.json version: version phải là semver, ví dụ 1.0.0
ERR game.json category: "shooter" không có trong category trong taxonomy (arcade, puzzle, action, casual, sports, racing, strategy, card-board, kids, io)
validate: FAIL (2 lỗi, 0 cảnh báo)