Guides

Build and publish your first game

AdminUpdated Sep 22, 2026

Build and publish your first game

This walkthrough takes you from an empty folder to a published HTML5 game on Cool GPT Games. You'll build a small tap-the-dot game that uses the SDK the way a real game should: it tells the site when it's ready and when play starts, records a score, saves the player's best, and pauses when the tab is hidden or an ad is showing.

It takes about 15 minutes. You need:

  • a Cool GPT Games account (sign in at coolgptgames.com),

  • a text editor,

  • a way to make a .zip file,

  • a cover image for your game (PNG, JPEG, GIF or WebP, up to 5 MB). A cover is required to publish.


1. Create the folder

tap-the-dot/
├── index.html
└── game.js

Keep all your game's files inside this folder. Everything in it goes into the zip, and index.html is the entry point.


2. Write index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
  <title>Tap the Dot</title>
  <style>
    html, body { margin: 0; height: 100%; overflow: hidden; background: #10132b; }
    canvas { display: block; position: fixed; inset: 0; touch-action: none; }
  </style>
</head>
<body>
  <canvas id="game"></canvas>


  <script src="/sdk/game-sdk.js"></script>
  <script src="game.js"></script>
</body>
</html>

Three things matter here:

  • <script src="/sdk/game-sdk.js"> loads the SDK from the game CDN, and it must come before game.js. The relative path is recommended. The absolute https://cdn.coolgptgames.com/sdk/game-sdk.js also works and passes the automated scan.

  • game.js is a relative path with no leading slash. Your files are served from a version-specific folder, so /game.js wouldn't find it.

  • The viewport meta tag and a full-window canvas make the game fit the player area on desktop and phones.


3. Write game.js

(function () {
  "use strict";

  // --- 1. Get the SDK, or a stub when running outside Cool GPT Games -------
  var SDK = window.GameSDK || {
    ready: function () {},
    start: function () {},
    gameOver: function () {},
    submitScore: function () {},
    saveData: function () {},
    loadData: function () { return Promise.resolve(null); },
    getPlayer: function () { return Promise.resolve({ signedIn: false, level: 1, handle: null }); },
    on: function () { return this; }
  };

  // --- 2. Canvas that always fills the game area ---------------------------
  var canvas = document.getElementById("game");
  var ctx = canvas.getContext("2d");
  var W = 0, H = 0;

  function resize() {
    var dpr = Math.min(window.devicePixelRatio || 1, 2);
    W = window.innerWidth;
    H = window.innerHeight;
    canvas.width = Math.floor(W * dpr);
    canvas.height = Math.floor(H * dpr);
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }
  window.addEventListener("resize", resize);
  resize();

  // --- 3. Game state -------------------------------------------------------
  var ROUND_SECONDS = 30;
  var state = "title";          // "title" | "playing" | "over"
  var paused = false;
  var score = 0;
  var best = 0;
  var timeLeft = 0;
  var dot = { x: 0, y: 0, r: 30 };
  var playerLine = "";

  function moveDot() {
    dot.r = 22 + Math.random() * 18;
    dot.x = dot.r + Math.random() * Math.max(1, W - 2 * dot.r);
    dot.y = 70 + dot.r + Math.random() * Math.max(1, H - 70 - 2 * dot.r);
  }

  function startRound() {
    score = 0;
    timeLeft = ROUND_SECONDS;
    state = "playing";
    moveDot();
    SDK.start();                // starts the play session (first call only)
  }

  function endRound() {
    state = "over";
    SDK.gameOver(score);        // lifecycle signal: "a run just ended"
    SDK.submitScore(score);     // records the score on the leaderboard
    if (score > best) {
      best = score;
      SDK.saveData("best", String(best));   // values are strings
    }
  }

  // --- 4. Input (mouse and touch) -----------------------------------------
  canvas.addEventListener("pointerdown", function (e) {
    if (state !== "playing") { startRound(); return; }
    if (paused) return;
    var dx = e.clientX - dot.x;
    var dy = e.clientY - dot.y;
    if (dx * dx + dy * dy <= dot.r * dot.r) {
      score += 1;
      moveDot();
    }
  });

  // --- 5. Pause when the tab is hidden or an ad is showing ------------------
  var last = performance.now();
  function pause() { paused = true; }
  function resume() { paused = false; last = performance.now(); }
  SDK.on("pause", pause).on("resume", resume);
  SDK.on("adStarted", pause).on("adComplete", resume).on("adError", resume);

  // --- 6. Load saved progress and player info -------------------------------
  SDK.loadData("best").then(function (value) {     // null if nothing saved (or no answer in 8 s)
    best = parseInt(value, 10) || 0;
  });

  function showPlayer(p) {
    playerLine = p.signedIn
      ? "Playing as " + (p.handle ? "@" + p.handle : "a signed-in player")
      : "Playing as guest - sign in to get on the leaderboard";
  }
  SDK.on("player", showPlayer);
  SDK.getPlayer().then(showPlayer);

  // --- 7. Game loop ----------------------------------------------------------
  function update(dt) {
    if (state !== "playing" || paused) return;
    timeLeft -= dt;
    if (timeLeft <= 0) { timeLeft = 0; endRound(); }
  }

  function text(str, x, y, size, color) {
    ctx.fillStyle = color || "#ffffff";
    ctx.font = "bold " + size + "px system-ui, sans-serif";
    ctx.textAlign = "center";
    ctx.fillText(str, x, y);
  }

  function draw() {
    ctx.fillStyle = "#10132b";
    ctx.fillRect(0, 0, W, H);

    if (state === "playing") {
      ctx.fillStyle = "#ff3d68";
      ctx.beginPath();
      ctx.arc(dot.x, dot.y, dot.r, 0, Math.PI * 2);
      ctx.fill();
      text("Score " + score + "   Time " + Math.ceil(timeLeft), W / 2, 40, 22);
      if (paused) text("Paused", W / 2, H / 2, 40);
      return;
    }

    var title = state === "title" ? "TAP THE DOT" : "SCORE " + score;
    text(title, W / 2, H / 2 - 40, 44, "#ff9a3d");
    text(state === "title" ? "Tap to start" : "Tap to play again", W / 2, H / 2 + 10, 22);
    text("Best " + best, W / 2, H / 2 + 50, 18, "#8a90b8");
    text(playerLine, W / 2, H - 24, 14, "#8a90b8");
  }

  var readySent = false;
  function frame(now) {
    var dt = Math.min((now - last) / 1000, 0.1);   // cap big gaps
    last = now;
    update(dt);
    draw();
    if (!readySent) { readySent = true; SDK.ready(); }   // first frame is on screen
    requestAnimationFrame(frame);
  }
  requestAnimationFrame(frame);
})();

What each SDK call does

Call

When the game calls it

What happens on Cool GPT Games

SDK.ready()

After the first frame is drawn

The site removes its loading overlay.

SDK.start()

When a round starts

The first call starts a play session, which is how plays and play time are measured. Later calls reuse the same session.

SDK.gameOver(score)

When a round ends

Signals the end of a run. It doesn't save the score.

SDK.submitScore(score)

When a round ends

Records the score on your game's leaderboard (signed-in players only).

SDK.saveData("best", …)

On a new best

Saves to the player's account if they're signed in, otherwise to their browser. Returns a promise you can ignore, or check for { ok: false, error }.

SDK.loadData("best")

On load

Reads the saved value back (null if there's none, or if nothing answers within 8 seconds).

SDK.getPlayer() + the player event

On load

Tells you whether the player is signed in, and their handle.

SDK.on("pause" / "resume" / "adStarted" / "adComplete" / "adError")

Once, at startup

Freezes the round while the tab is hidden or an ad is on screen.

Full details are in GameSDK core reference, Saves & cloud progress, and Scores, leaderboards & anti-cheat.

Why the stub, and why draw first?

  • When you open the game on your own machine, /sdk/game-sdk.js doesn't exist, so window.GameSDK is undefined. The stub keeps the game playable locally.

  • Every SDK request has a built-in timeout. loadData() resolves null if nothing answers within 8 seconds (for example, when the game is opened outside the site), so it never hangs. The game still draws its title screen straight away rather than waiting for it.

  • The automated review opens your game without the site around it and takes screenshots. A game that waits for an SDK answer before drawing anything looks blank and gets rejected. Draw your first screen straight away, as this example does.


4. Test it locally

Open index.html directly in your browser, or serve the folder:

cd tap-the-dot
python3 -m http.server 8000   # then open http://localhost:8000

You should see the title screen. Tap to play a 30-second round. Locally the SDK isn't there, so nothing is saved or submitted, and that's expected. The player line reads "Playing as guest".

Before zipping, check that:

  • the first screen draws immediately (no blank page, no "click to load"),

  • the game resizes correctly when you resize the window or use your browser's phone emulation,

  • there are no errors in the browser console.


5. Zip it

Zip the files, not your whole project. Only web file types are allowed in the zip, and a stray README.md fails the upload. OS clutter such as .DS_Store or __MACOSX/ is skipped automatically.

macOS / Linux

cd tap-the-dot
zip -r ../tap-the-dot.zip . -x ".*" -x "*/.*"

Windows (PowerShell)

cd tap-the-dot
Compress-Archive -Path index.html, game.js -DestinationPath ..\tap-the-dot.zip

Check the result: index.html should be at the top level of the zip (a single wrapping folder is also fine).

The allowed file types, size limits and other packaging rules are listed on Packaging & publishing.


6. Upload it on the website

  1. Sign in and go to coolgptgames.com/upload. 2. 1 · Your game file: drop tap-the-dot.zip. It's detected as an HTML5 game. 3. 2 · Basics: enter a title (up to 120 characters), pick a category, and write a short description. 4. 3 · How to play (optional): for example, "Tap the red dot as many times as you can in 30 seconds." 5. 4 · Media: add your cover image (required). Screenshots are optional and can be added later. 6. 5 · Store listing (optional): tagline, long description, websites, FAQ. 7. 6 · AI disclosure: tick the box if AI tools helped build the game, and optionally describe how. 8. 7 · Options & publish: leave "This is a multiplayer game" unticked, complete the human-verification check if one appears, and click Publish game.

The page then uploads your cover and your zip, and the automated review starts.

Prefer the command line?

The CLI can publish a folder in one command, cover included:

arcadey publish tap-the-dot --title "Tap the Dot" --category arcade --cover cover.png

It prints your new game's id. Ship updates later with arcadey publish tap-the-dot --game <id>. The title, cover and other details can also live in a game.json file next to your files. It's the easiest way to publish from a build script or CI, and the key it uses needs the publish scope. See CLI. To publish from your own code instead, use the Publisher SDK.


7. What happens after you upload

Every upload goes through an automated review before it goes live:

  1. Validation. The zip is unpacked and checked: allowed file types, size limits, an index.html entry point. Problems show up immediately on the upload page with a specific message (for example, "No index.html was found at the top of the build").

  2. Code scan. Your HTML and JavaScript are scanned. Referencing scripts, images or other resources on external hosts is an automatic rejection. Riskier patterns (like eval) send the game to human review.

  3. Play check. Your game is opened in a real browser, given some scripted key presses and clicks, and screenshotted. A game that stays blank, or doesn't look like a playable game, is rejected. 4. Content and duplicate checks. The screenshots, title and description are checked against the content rules and against existing games.

There are three possible outcomes. The upload page waits a few seconds for the result and then tells you which one you got:

Result

What it means

What to do

Published

Passed automatically. Your game is live and publicly listed.

Click Play it.

Still scanning

The review is taking longer than the upload page waits.

Check your dashboard in a minute. You don't need to keep the page open.

For the full review process, reason codes and how to ship new versions, see Packaging & publishing.


8. See it live

Once the status is Published:

  1. Open your game's page: https://coolgptgames.com/game/<your-game-slug>. The upload page's Play it button takes you there, and your dashboard lists all your games with their status. 2. Sign in, play a round, and check:

    • the loading overlay disappears as soon as the title screen shows,

    • switching to another tab and back shows "Paused" and then continues,

    • your best score is still there after reloading the page (saved to your account).

  2. Test the leaderboard properly. A score is only recorded when the play session has about one minute of play behind it, to stop drive-by score farming. The first 30-second round of a visit is usually too early to count. Play a few rounds in the same visit, and later rounds' scores will appear. See How games run for the exact rules.

  3. Try it signed out as well. Guests can play, and their best score is kept in their browser, but their scores aren't submitted. The site shows guests a prompt to sign in.

Note: the Preview build button in your dashboard opens the game in its own tab, outside the site's player. The SDK gets no answers there, so saves, scores and player info don't work in a preview (SDK calls resolve their fallbacks after their timeouts). Always test SDK features on the published game page.


Next steps

Was this page helpful?
Build and publish your first game