Example: canvas scratch card
A complete scratch-card game in one HTML file, one script and the helper from the quick start. About 120 lines, no build step.
Put index.html, game.js and incentia-sdk.js at the root of your zip.
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Acme Scratch</title>
<style>
body { margin: 0; font-family: system-ui, sans-serif; background: #111; color: #eee; }
main { display: flex; flex-direction: column; align-items: center; padding: 24px; gap: 16px; }
canvas { border-radius: 8px; touch-action: none; }
button { padding: 10px 24px; font-size: 16px; border-radius: 6px; border: 0; background: #5c6bff; color: white; cursor: pointer; }
button:disabled { opacity: 0.5; cursor: not-allowed; }
.panel { min-height: 20px; opacity: 0.8; }
</style>
</head>
<body>
<main>
<canvas id="scratch" width="320" height="200"></canvas>
<div class="panel" id="status">Loading...</div>
<button id="claim" disabled>Claim outcome</button>
</main>
<script type="module" src="./game.js"></script>
</body>
</html>
game.js
import { readPlayContext, complete } from './incentia-sdk.js';
const ctx = readPlayContext();
const canvas = document.getElementById('scratch');
const cx = canvas.getContext('2d');
const status = document.getElementById('status');
const claim = document.getElementById('claim');
// Paint the scratch-off layer.
cx.fillStyle = '#5c6bff';
cx.fillRect(0, 0, canvas.width, canvas.height);
cx.fillStyle = 'white';
cx.font = 'bold 24px system-ui';
cx.textAlign = 'center';
cx.fillText('Scratch me!', canvas.width / 2, canvas.height / 2 + 8);
// Track how much area the user has scratched as the play signal.
let scratchedPixels = 0;
let drawing = false;
function scratchAt(x, y) {
cx.globalCompositeOperation = 'destination-out';
cx.beginPath();
cx.arc(x, y, 18, 0, Math.PI * 2);
cx.fill();
scratchedPixels += Math.PI * 18 * 18;
cx.globalCompositeOperation = 'source-over';
if (scratchedPixels > canvas.width * canvas.height * 0.4) {
claim.disabled = false;
}
}
function pointerMove(e) {
if (!drawing) return;
const rect = canvas.getBoundingClientRect();
const x = (e.clientX ?? e.touches[0].clientX) - rect.left;
const y = (e.clientY ?? e.touches[0].clientY) - rect.top;
scratchAt(x, y);
}
canvas.addEventListener('pointerdown', (e) => { drawing = true; pointerMove(e); });
canvas.addEventListener('pointermove', pointerMove);
canvas.addEventListener('pointerup', () => { drawing = false; });
canvas.addEventListener('pointercancel', () => { drawing = false; });
if (!ctx) {
status.textContent = 'Dev mode — outcomes are mocked';
claim.textContent = 'Claim mock outcome';
claim.addEventListener('click', () => {
status.textContent = 'Outcome: win (dev) — 50 pts';
claim.disabled = true;
});
} else {
status.textContent = 'Scratch ~40% then claim your outcome';
claim.addEventListener('click', async () => {
claim.disabled = true;
status.textContent = 'Waiting for outcome...';
try {
const outcome = await complete(ctx, {
scratchedRatio: Math.min(1, scratchedPixels / (canvas.width * canvas.height)),
});
status.textContent = outcome.outcome === 'win'
? `You won ${outcome.prize?.name ?? 'a prize'}!`
: 'Better luck next time.';
} catch (err) {
status.textContent = `Error: ${err.message}`;
}
});
}
Signal schema for this game
In the External HTML5 game panel of the game, set Signal schema (advanced) to:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["scratchedRatio"],
"properties": {
"scratchedRatio": { "type": "number", "minimum": 0, "maximum": 1 }
}
}
Without the scratchedRatio field the server rejects the play with a schema validation error.