@remix-gg/mcp 0.4.3
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/README.md +81 -0
- package/dist/client-helpers/index.d.ts +2 -0
- package/dist/client-helpers/index.d.ts.map +1 -0
- package/dist/client-helpers/index.js +2 -0
- package/dist/client-helpers/index.js.map +1 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +58 -0
- package/dist/config.js.map +1 -0
- package/dist/core/api-client.d.ts +4 -0
- package/dist/core/api-client.d.ts.map +1 -0
- package/dist/core/api-client.js +12 -0
- package/dist/core/api-client.js.map +1 -0
- package/dist/core/config.d.ts +6 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +19 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +4 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/skills.d.ts +22 -0
- package/dist/core/skills.d.ts.map +1 -0
- package/dist/core/skills.js +49 -0
- package/dist/core/skills.js.map +1 -0
- package/dist/core/tool-defs.d.ts +12 -0
- package/dist/core/tool-defs.d.ts.map +1 -0
- package/dist/core/tool-defs.js +356 -0
- package/dist/core/tool-defs.js.map +1 -0
- package/dist/core/tools/create-game.d.ts +9 -0
- package/dist/core/tools/create-game.d.ts.map +1 -0
- package/dist/core/tools/create-game.js +21 -0
- package/dist/core/tools/create-game.js.map +1 -0
- package/dist/core/tools/create-shop-item.d.ts +14 -0
- package/dist/core/tools/create-shop-item.d.ts.map +1 -0
- package/dist/core/tools/create-shop-item.js +78 -0
- package/dist/core/tools/create-shop-item.js.map +1 -0
- package/dist/core/tools/delete-shop-item.d.ts +9 -0
- package/dist/core/tools/delete-shop-item.d.ts.map +1 -0
- package/dist/core/tools/delete-shop-item.js +19 -0
- package/dist/core/tools/delete-shop-item.js.map +1 -0
- package/dist/core/tools/generate-image.d.ts +8 -0
- package/dist/core/tools/generate-image.d.ts.map +1 -0
- package/dist/core/tools/generate-image.js +32 -0
- package/dist/core/tools/generate-image.js.map +1 -0
- package/dist/core/tools/generate-sprite-sheet.d.ts +14 -0
- package/dist/core/tools/generate-sprite-sheet.d.ts.map +1 -0
- package/dist/core/tools/generate-sprite-sheet.js +29 -0
- package/dist/core/tools/generate-sprite-sheet.js.map +1 -0
- package/dist/core/tools/helpers.d.ts +60 -0
- package/dist/core/tools/helpers.d.ts.map +1 -0
- package/dist/core/tools/helpers.js +68 -0
- package/dist/core/tools/helpers.js.map +1 -0
- package/dist/core/tools/index.d.ts +13 -0
- package/dist/core/tools/index.d.ts.map +1 -0
- package/dist/core/tools/index.js +13 -0
- package/dist/core/tools/index.js.map +1 -0
- package/dist/core/tools/list-shop-items.d.ts +8 -0
- package/dist/core/tools/list-shop-items.d.ts.map +1 -0
- package/dist/core/tools/list-shop-items.js +17 -0
- package/dist/core/tools/list-shop-items.js.map +1 -0
- package/dist/core/tools/update-game.d.ts +11 -0
- package/dist/core/tools/update-game.d.ts.map +1 -0
- package/dist/core/tools/update-game.js +22 -0
- package/dist/core/tools/update-game.js.map +1 -0
- package/dist/core/tools/update-shop-item.d.ts +15 -0
- package/dist/core/tools/update-shop-item.d.ts.map +1 -0
- package/dist/core/tools/update-shop-item.js +24 -0
- package/dist/core/tools/update-shop-item.js.map +1 -0
- package/dist/core/tools/upload-game-asset.d.ts +8 -0
- package/dist/core/tools/upload-game-asset.d.ts.map +1 -0
- package/dist/core/tools/upload-game-asset.js +43 -0
- package/dist/core/tools/upload-game-asset.js.map +1 -0
- package/dist/core/tools/upload-version.d.ts +8 -0
- package/dist/core/tools/upload-version.d.ts.map +1 -0
- package/dist/core/tools/upload-version.js +20 -0
- package/dist/core/tools/upload-version.js.map +1 -0
- package/dist/core/tools/validate-game.d.ts +6 -0
- package/dist/core/tools/validate-game.d.ts.map +1 -0
- package/dist/core/tools/validate-game.js +41 -0
- package/dist/core/tools/validate-game.js.map +1 -0
- package/dist/core/tools.test.d.ts +2 -0
- package/dist/core/tools.test.d.ts.map +1 -0
- package/dist/core/tools.test.js +825 -0
- package/dist/core/tools.test.js.map +1 -0
- package/dist/generated/server-api.d.ts +3673 -0
- package/dist/generated/server-api.d.ts.map +1 -0
- package/dist/generated/server-api.js +2 -0
- package/dist/generated/server-api.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +365 -0
- package/dist/index.js.map +1 -0
- package/dist/server/create-server.d.ts +12 -0
- package/dist/server/create-server.d.ts.map +1 -0
- package/dist/server/create-server.js +29 -0
- package/dist/server/create-server.js.map +1 -0
- package/dist/server/create-server.test.d.ts +2 -0
- package/dist/server/create-server.test.d.ts.map +1 -0
- package/dist/server/create-server.test.js +37 -0
- package/dist/server/create-server.test.js.map +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +8 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/lib/config.d.ts +2 -0
- package/dist/server/lib/config.d.ts.map +1 -0
- package/dist/server/lib/config.js +2 -0
- package/dist/server/lib/config.js.map +1 -0
- package/dist/server/resources/skills.d.ts +3 -0
- package/dist/server/resources/skills.d.ts.map +1 -0
- package/dist/server/resources/skills.js +60 -0
- package/dist/server/resources/skills.js.map +1 -0
- package/dist/server/resources/skills.test.d.ts +2 -0
- package/dist/server/resources/skills.test.d.ts.map +1 -0
- package/dist/server/resources/skills.test.js +44 -0
- package/dist/server/resources/skills.test.js.map +1 -0
- package/dist/server/tools/register.d.ts +3 -0
- package/dist/server/tools/register.d.ts.map +1 -0
- package/dist/server/tools/register.js +26 -0
- package/dist/server/tools/register.js.map +1 -0
- package/dist/server/tools/register.test.d.ts +2 -0
- package/dist/server/tools/register.test.d.ts.map +1 -0
- package/dist/server/tools/register.test.js +85 -0
- package/dist/server/tools/register.test.js.map +1 -0
- package/dist/types.d.ts +73 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +31 -0
- package/dist/types.js.map +1 -0
- package/package.json +38 -0
- package/skills/SKILL.md +82 -0
- package/skills/actions/open-game.md +18 -0
- package/skills/workflows/add-image-to-game.md +121 -0
- package/skills/workflows/add-sprite-to-game.md +127 -0
- package/skills/workflows/game-creation.md +124 -0
- package/skills/workflows/implement-multiplayer.md +355 -0
- package/skills/workflows/integrate-save-game.md +135 -0
- package/skills/workflows/manage-shop-items.md +246 -0
- package/skills/workflows/upload-game.md +74 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# Implement Multiplayer Workflow
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This skill guides you through integrating turn-based multiplayer into an HTML
|
|
6
|
+
game on the Remix platform. Multiplayer games use the RemixSDK's `multiplayer`
|
|
7
|
+
namespace instead of `singlePlayer`. Games are two-player and fully turn-based.
|
|
8
|
+
|
|
9
|
+
## Prerequisites
|
|
10
|
+
|
|
11
|
+
- The game must include the RemixSDK script tag:
|
|
12
|
+
```html
|
|
13
|
+
<script src="https://cdn.jsdelivr.net/npm/@remix-gg/sdk@latest/dist/index.min.js"></script>
|
|
14
|
+
```
|
|
15
|
+
- The game must already be playable (follow the **game-creation** workflow first
|
|
16
|
+
if starting from scratch).
|
|
17
|
+
- The `REMIX_API_KEY` environment variable must be set.
|
|
18
|
+
- `.remix-settings.json` must contain a `gameId`.
|
|
19
|
+
|
|
20
|
+
## Constraints
|
|
21
|
+
|
|
22
|
+
- Multiplayer is **2-player only**. Do not design for more than two players.
|
|
23
|
+
- All games must be **fully turn-based**. Real-time multiplayer is not supported.
|
|
24
|
+
|
|
25
|
+
## Steps
|
|
26
|
+
|
|
27
|
+
### 1. Check `.remix-settings.json`
|
|
28
|
+
|
|
29
|
+
Read the file and extract `gameId`. If it is missing, follow the
|
|
30
|
+
**game-creation** workflow first to create and register the game.
|
|
31
|
+
|
|
32
|
+
### 2. Mark the Game as Multiplayer
|
|
33
|
+
|
|
34
|
+
Call the `updateGame` tool with the `gameId` and `isMultiplayer: true`. This
|
|
35
|
+
registers the game as multiplayer on the platform.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
updateGame({ gameId: "<gameId>", isMultiplayer: true })
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 3. Initialize the SDK and Check for Existing Game State
|
|
42
|
+
|
|
43
|
+
Call `await window.RemixSDK.ready()` before the game loop starts. Then check
|
|
44
|
+
`window.RemixSDK.gameState` to restore in-progress matches. If state exists,
|
|
45
|
+
resume the game from that state.
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
await window.RemixSDK.ready();
|
|
49
|
+
|
|
50
|
+
const savedState = window.RemixSDK.gameState;
|
|
51
|
+
if (savedState) {
|
|
52
|
+
// Resume in-progress match from savedState
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 4. Identify the Current Player and Opponent
|
|
57
|
+
|
|
58
|
+
Use `window.RemixSDK.player` for the current player and
|
|
59
|
+
`window.RemixSDK.players` for all players. Derive the opponent:
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
const player = window.RemixSDK.player;
|
|
63
|
+
const players = window.RemixSDK.players;
|
|
64
|
+
const opponent = players.find(p => p.id !== player.id);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Important:** The first player in the `players` array (`players[0]`) is always
|
|
68
|
+
the player who should act first in a new game. Use this ordering to determine
|
|
69
|
+
who takes the opening turn rather than sorting or randomizing.
|
|
70
|
+
|
|
71
|
+
### 5. Display Player Names and Avatars
|
|
72
|
+
|
|
73
|
+
Each `Player` object has a `name` (string) and an optional `imageUrl` (string)
|
|
74
|
+
for the player's avatar. Use these to personalize the game UI.
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
const player = window.RemixSDK.player;
|
|
78
|
+
const opponent = players.find(p => p.id !== player.id);
|
|
79
|
+
|
|
80
|
+
// Access name and avatar
|
|
81
|
+
player.name; // e.g. "Alice"
|
|
82
|
+
player.imageUrl; // e.g. "https://…/avatar.png" or undefined
|
|
83
|
+
opponent.name;
|
|
84
|
+
opponent.imageUrl;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Display names in status messages so players know who they are playing against:
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
statusEl.textContent = isMyTurn
|
|
91
|
+
? `Your turn, ${player.name}`
|
|
92
|
+
: `Waiting for ${opponent.name}…`;
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Show avatars with a fallback for when `imageUrl` is missing:
|
|
96
|
+
|
|
97
|
+
```html
|
|
98
|
+
<div class="player-info">
|
|
99
|
+
<img src="${player.imageUrl}" alt="${player.name}"
|
|
100
|
+
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex'">
|
|
101
|
+
<div class="avatar-fallback" style="display:none">${player.name[0]}</div>
|
|
102
|
+
<span>${player.name}</span>
|
|
103
|
+
</div>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 6. Save Game State After Each Turn
|
|
107
|
+
|
|
108
|
+
Use `multiplayer.actions.saveGameState` with `alertUserIds` containing the
|
|
109
|
+
opponent's ID so they are notified it is their turn:
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
window.RemixSDK.multiplayer.actions.saveGameState({
|
|
113
|
+
gameState: { /* current board/game state */ },
|
|
114
|
+
alertUserIds: [opponent.id],
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 7. Listen for Opponent's Moves
|
|
119
|
+
|
|
120
|
+
Register `onGameStateUpdated` to handle incoming state changes from the
|
|
121
|
+
opponent:
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
window.RemixSDK.onGameStateUpdated((data) => {
|
|
125
|
+
if (!data) return;
|
|
126
|
+
const { id, gameState } = data;
|
|
127
|
+
// Validate and apply the new state
|
|
128
|
+
|
|
129
|
+
// If the move is invalid, refute it:
|
|
130
|
+
// window.RemixSDK.multiplayer.actions.refuteGameState({ gameStateId: id });
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 8. End the Game with Multiplayer gameOver
|
|
135
|
+
|
|
136
|
+
Report scores for both players using `multiplayer.actions.gameOver`:
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
window.RemixSDK.multiplayer.actions.gameOver({
|
|
140
|
+
scores: [
|
|
141
|
+
{ playerId: player.id, score: playerScore },
|
|
142
|
+
{ playerId: opponent.id, score: opponentScore },
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### 9. Handle Play Again and Mute
|
|
148
|
+
|
|
149
|
+
Register `onPlayAgain` and `onToggleMute` callbacks — same as single player:
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
window.RemixSDK.onPlayAgain(() => {
|
|
153
|
+
// Reset game state and start a new match
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
window.RemixSDK.onToggleMute(({ isMuted }) => {
|
|
157
|
+
// Mute or unmute audio
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Examples
|
|
162
|
+
|
|
163
|
+
### Tic-Tac-Toe
|
|
164
|
+
|
|
165
|
+
```html
|
|
166
|
+
<!DOCTYPE html>
|
|
167
|
+
<html lang="en">
|
|
168
|
+
<head>
|
|
169
|
+
<meta charset="UTF-8">
|
|
170
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
171
|
+
<title>Tic-Tac-Toe</title>
|
|
172
|
+
<script src="https://cdn.jsdelivr.net/npm/@remix-gg/sdk@latest/dist/index.min.js"></script>
|
|
173
|
+
<style>
|
|
174
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
175
|
+
body { width: 100vw; height: 100vh; display: flex; flex-direction: column;
|
|
176
|
+
align-items: center; justify-content: center; font-family: sans-serif;
|
|
177
|
+
background: #1a1a2e; color: #eee; }
|
|
178
|
+
#status { margin-bottom: 16px; font-size: 1.2rem; }
|
|
179
|
+
#board { display: grid; grid-template-columns: repeat(3, 100px);
|
|
180
|
+
grid-template-rows: repeat(3, 100px); gap: 4px; }
|
|
181
|
+
.cell { background: #16213e; display: flex; align-items: center;
|
|
182
|
+
justify-content: center; font-size: 2.5rem; cursor: pointer;
|
|
183
|
+
border-radius: 4px; }
|
|
184
|
+
.cell:hover { background: #0f3460; }
|
|
185
|
+
#players { display: flex; gap: 24px; margin-bottom: 12px; }
|
|
186
|
+
.player-info { display: flex; align-items: center; gap: 8px; }
|
|
187
|
+
.player-info img { width: 32px; height: 32px; border-radius: 50%; }
|
|
188
|
+
.avatar-fallback { width: 32px; height: 32px; border-radius: 50%;
|
|
189
|
+
background: #0f3460; display: flex; align-items: center;
|
|
190
|
+
justify-content: center; font-size: 0.9rem; font-weight: bold; }
|
|
191
|
+
</style>
|
|
192
|
+
</head>
|
|
193
|
+
<body>
|
|
194
|
+
<div id="players"></div>
|
|
195
|
+
<div id="status">Loading…</div>
|
|
196
|
+
<div id="board"></div>
|
|
197
|
+
<script>
|
|
198
|
+
let board = Array(9).fill(null);
|
|
199
|
+
let player, opponent, mySymbol, isMyTurn;
|
|
200
|
+
const playersEl = document.getElementById("players");
|
|
201
|
+
const statusEl = document.getElementById("status");
|
|
202
|
+
const boardEl = document.getElementById("board");
|
|
203
|
+
|
|
204
|
+
function avatarHTML(p) {
|
|
205
|
+
if (p.imageUrl) {
|
|
206
|
+
return `<img src="${p.imageUrl}" alt="${p.name}"
|
|
207
|
+
onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">
|
|
208
|
+
<div class="avatar-fallback" style="display:none">${p.name[0]}</div>`;
|
|
209
|
+
}
|
|
210
|
+
return `<div class="avatar-fallback">${p.name[0]}</div>`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function renderPlayers() {
|
|
214
|
+
if (!player || !opponent) return;
|
|
215
|
+
playersEl.innerHTML = [player, opponent].map(p =>
|
|
216
|
+
`<div class="player-info">${avatarHTML(p)}<span>${p.name}</span></div>`
|
|
217
|
+
).join("");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function render() {
|
|
221
|
+
boardEl.innerHTML = "";
|
|
222
|
+
board.forEach((val, i) => {
|
|
223
|
+
const cell = document.createElement("div");
|
|
224
|
+
cell.className = "cell";
|
|
225
|
+
cell.textContent = val ?? "";
|
|
226
|
+
cell.addEventListener("click", () => handleClick(i));
|
|
227
|
+
boardEl.appendChild(cell);
|
|
228
|
+
});
|
|
229
|
+
statusEl.textContent = isMyTurn
|
|
230
|
+
? `Your turn, ${player.name}`
|
|
231
|
+
: `Waiting for ${opponent.name}…`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function checkWinner() {
|
|
235
|
+
const lines = [
|
|
236
|
+
[0,1,2],[3,4,5],[6,7,8],
|
|
237
|
+
[0,3,6],[1,4,7],[2,5,8],
|
|
238
|
+
[0,4,8],[2,4,6],
|
|
239
|
+
];
|
|
240
|
+
for (const [a,b,c] of lines) {
|
|
241
|
+
if (board[a] && board[a] === board[b] && board[a] === board[c]) {
|
|
242
|
+
return board[a];
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return board.every(Boolean) ? "draw" : null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function handleClick(i) {
|
|
249
|
+
if (!isMyTurn || board[i]) return;
|
|
250
|
+
board[i] = mySymbol;
|
|
251
|
+
isMyTurn = false;
|
|
252
|
+
render();
|
|
253
|
+
|
|
254
|
+
const winner = checkWinner();
|
|
255
|
+
if (winner) {
|
|
256
|
+
endGame(winner);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
window.RemixSDK.multiplayer.actions.saveGameState({
|
|
261
|
+
gameState: { board, lastPlayer: player.id },
|
|
262
|
+
alertUserIds: [opponent.id],
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function endGame(winner) {
|
|
267
|
+
const iWin = winner === mySymbol;
|
|
268
|
+
const isDraw = winner === "draw";
|
|
269
|
+
statusEl.textContent = isDraw ? "Draw!" : iWin ? "You win!" : "You lose!";
|
|
270
|
+
window.RemixSDK.multiplayer.actions.gameOver({
|
|
271
|
+
scores: [
|
|
272
|
+
{ playerId: player.id, score: iWin ? 1 : 0 },
|
|
273
|
+
{ playerId: opponent.id, score: !iWin && !isDraw ? 1 : 0 },
|
|
274
|
+
],
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function init() {
|
|
279
|
+
await window.RemixSDK.ready();
|
|
280
|
+
|
|
281
|
+
player = window.RemixSDK.player;
|
|
282
|
+
const players = window.RemixSDK.players;
|
|
283
|
+
opponent = players.find(p => p.id !== player.id);
|
|
284
|
+
|
|
285
|
+
// First player in the array takes the first turn (X)
|
|
286
|
+
mySymbol = players[0].id === player.id ? "X" : "O";
|
|
287
|
+
renderPlayers();
|
|
288
|
+
|
|
289
|
+
// Restore in-progress match
|
|
290
|
+
const saved = window.RemixSDK.gameState;
|
|
291
|
+
if (saved && saved.board) {
|
|
292
|
+
board = saved.board;
|
|
293
|
+
isMyTurn = saved.lastPlayer !== player.id;
|
|
294
|
+
} else {
|
|
295
|
+
board = Array(9).fill(null);
|
|
296
|
+
isMyTurn = mySymbol === "X";
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
render();
|
|
300
|
+
|
|
301
|
+
window.RemixSDK.onGameStateUpdated((data) => {
|
|
302
|
+
if (!data) return;
|
|
303
|
+
const { gameState } = data;
|
|
304
|
+
board = gameState.board;
|
|
305
|
+
isMyTurn = gameState.lastPlayer !== player.id;
|
|
306
|
+
render();
|
|
307
|
+
|
|
308
|
+
const winner = checkWinner();
|
|
309
|
+
if (winner) endGame(winner);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
window.RemixSDK.onPlayAgain(() => {
|
|
313
|
+
board = Array(9).fill(null);
|
|
314
|
+
isMyTurn = mySymbol === "X";
|
|
315
|
+
render();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
window.RemixSDK.onToggleMute(({ isMuted }) => {
|
|
319
|
+
// No audio in this example
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
init();
|
|
324
|
+
</script>
|
|
325
|
+
</body>
|
|
326
|
+
</html>
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Wrong Patterns
|
|
330
|
+
|
|
331
|
+
- **Using `singlePlayer.actions.gameOver`** instead of
|
|
332
|
+
`multiplayer.actions.gameOver` — scores will not be recorded for both players.
|
|
333
|
+
- **Using `singlePlayer.actions.saveGameState`** instead of
|
|
334
|
+
`multiplayer.actions.saveGameState` — the opponent will not receive state
|
|
335
|
+
updates.
|
|
336
|
+
- **Forgetting `alertUserIds`** when saving state — the opponent will not be
|
|
337
|
+
notified that it is their turn.
|
|
338
|
+
- **Not checking `gameState` on startup** — breaks resuming in-progress matches.
|
|
339
|
+
Always check `window.RemixSDK.gameState` after `ready()`.
|
|
340
|
+
- **Hardcoding player IDs** — always read from `window.RemixSDK.player` and
|
|
341
|
+
`window.RemixSDK.players`.
|
|
342
|
+
- **Not showing player identity** — multiplayer games should display names and
|
|
343
|
+
avatars so players know who they are playing against.
|
|
344
|
+
|
|
345
|
+
## Tips
|
|
346
|
+
|
|
347
|
+
- **Keep state JSON-serializable.** No functions, class instances, or circular
|
|
348
|
+
references.
|
|
349
|
+
- **Validate opponent moves.** Check that the incoming state is legal before
|
|
350
|
+
applying it. Use `refuteGameState` if a move is invalid.
|
|
351
|
+
- **Save at turn boundaries**, not every frame.
|
|
352
|
+
- **Assign symbols deterministically** — use `players[0]` as the first actor
|
|
353
|
+
(e.g. X) so both clients agree on who goes first.
|
|
354
|
+
- **Handle missing `imageUrl`** — the field is optional; use a placeholder or
|
|
355
|
+
initials fallback when it is `undefined`.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Integrate Save Game State Workflow
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This skill guides you through integrating save game state into an HTML game on
|
|
6
|
+
the Remix platform. Save game state lets players persist progress across
|
|
7
|
+
sessions — scores, unlocked levels, inventory, and more — using the RemixSDK.
|
|
8
|
+
|
|
9
|
+
## Prerequisites
|
|
10
|
+
|
|
11
|
+
- The game must include the RemixSDK script tag:
|
|
12
|
+
```html
|
|
13
|
+
<script src="https://cdn.jsdelivr.net/npm/@remix-gg/sdk@latest/dist/index.min.js"></script>
|
|
14
|
+
```
|
|
15
|
+
- The game must already be playable (follow the **game-creation** workflow first
|
|
16
|
+
if starting from scratch).
|
|
17
|
+
|
|
18
|
+
## Steps
|
|
19
|
+
|
|
20
|
+
### 1. Initialize the SDK
|
|
21
|
+
|
|
22
|
+
Call `await window.RemixSDK.ready()` **before** the game loop starts. This
|
|
23
|
+
ensures the SDK is loaded and any existing saved state is available.
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
await window.RemixSDK.ready();
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 2. Load Existing State
|
|
30
|
+
|
|
31
|
+
Read `window.RemixSDK.gameState` to get previously saved data. It returns
|
|
32
|
+
`Record<string, unknown> | null | undefined` — always check for null before
|
|
33
|
+
using it.
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
const savedState = window.RemixSDK.gameState;
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Use the saved state to restore game progress, or fall back to defaults if the
|
|
40
|
+
player is starting fresh:
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
const state = savedState ?? { score: 0, level: 1 };
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 3. Save State During Gameplay
|
|
47
|
+
|
|
48
|
+
Call `saveGameState` whenever the player reaches a meaningful checkpoint. The
|
|
49
|
+
`gameState` value must be a JSON-serializable object.
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
window.RemixSDK.singlePlayer.actions.saveGameState({
|
|
53
|
+
gameState: { score: player.score, level: player.level },
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Examples
|
|
58
|
+
|
|
59
|
+
### Simple Clicker Game
|
|
60
|
+
|
|
61
|
+
```html
|
|
62
|
+
<script>
|
|
63
|
+
let clicks = 0;
|
|
64
|
+
|
|
65
|
+
async function init() {
|
|
66
|
+
await window.RemixSDK.ready();
|
|
67
|
+
|
|
68
|
+
// Load
|
|
69
|
+
const gameState = window.RemixSDK.gameState;
|
|
70
|
+
if (gameState && typeof gameState.clicks === "number") {
|
|
71
|
+
clicks = gameState.clicks;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
document.getElementById("count").textContent = clicks;
|
|
75
|
+
document.getElementById("btn").addEventListener("click", () => {
|
|
76
|
+
clicks++;
|
|
77
|
+
document.getElementById("count").textContent = clicks;
|
|
78
|
+
|
|
79
|
+
// Save after every click
|
|
80
|
+
window.RemixSDK.singlePlayer.actions.saveGameState({
|
|
81
|
+
gameState: { clicks },
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
init();
|
|
87
|
+
</script>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Platformer with Level Progression
|
|
91
|
+
|
|
92
|
+
```html
|
|
93
|
+
<script>
|
|
94
|
+
let level = 1;
|
|
95
|
+
let coins = 0;
|
|
96
|
+
|
|
97
|
+
async function init() {
|
|
98
|
+
await window.RemixSDK.ready();
|
|
99
|
+
|
|
100
|
+
// Load
|
|
101
|
+
const saved = window.RemixSDK.gameState;
|
|
102
|
+
if (saved) {
|
|
103
|
+
level = saved.level ?? 1;
|
|
104
|
+
coins = saved.coins ?? 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
startLevel(level);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function onLevelComplete() {
|
|
111
|
+
level++;
|
|
112
|
+
coins += 10;
|
|
113
|
+
|
|
114
|
+
// Save at level transitions
|
|
115
|
+
window.RemixSDK.singlePlayer.actions.saveGameState({
|
|
116
|
+
gameState: { level, coins },
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
startLevel(level);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
init();
|
|
123
|
+
</script>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Tips
|
|
127
|
+
|
|
128
|
+
- **Keep state small.** Only save what's needed to restore the session — avoid
|
|
129
|
+
storing large arrays or transient UI state.
|
|
130
|
+
- **Save at meaningful moments** — level completion, checkpoints, purchases —
|
|
131
|
+
not every frame.
|
|
132
|
+
- **State must be JSON-serializable.** No functions, class instances, or
|
|
133
|
+
circular references.
|
|
134
|
+
- **Always guard against null.** The first time a player loads the game there is
|
|
135
|
+
no saved state.
|