Golf Solitaire is a quick patience game: seven columns of five cards, a pile, and one rule. You can move the last card of any column onto the pile if it's one rank higher or lower, and when nothing fits you draw from the stock. Clear all 35 cards to win. It's a simple HTML5 card game with canvas and JavaScript, small enough to read in one sitting, and this HTML5 card game tutorial uses it to show the parts every card game needs: cards as data, a fair shuffle, dealing, rules, hit-testing taps and an end state.
How to run it
- Press Copy code above, or Download .html.
- Paste it into a new file called
card-game.htmlin any text editor. - Double-click the file to open it in your browser, then tap or press Enter to deal.
You can also paste it into the HTML game code runner and change a setting, such as WRAP = false for the stricter rule where Aces and Kings don't connect.
HTML5 card game tutorial: how the code works
The script keeps the rules and the drawing apart. Cards live in plain arrays (columns, stock and pile), the rules move them between arrays, and draw() paints whatever the arrays say. If you want to create an HTML5 card game of your own, that split is the most useful habit to copy: the rules can be tested without a screen, and the art can change without touching the rules.
1. Cards are data
A card is an object with two numbers: rank from 0 (Ace) to 12 (King), and suit from 0 to 3. Names and symbols live in lookup tables, so comparing cards is plain arithmetic and the display is a separate concern.
const COLS = 7, DEPTH = 5; // 7 columns of 5 face-up cards; the other 17 cards start the stock and the pile
const WRAP = true; // Ace and King count as neighbours
const RANKS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"];
const SUITS = ["♠", "♥", "♦", "♣"]; // hearts and diamonds are red
const POINTS = 10; // points for a card, times the length of the current run
const STOCK_BONUS = 50; // bonus per stock card left when you clear the board2. A fair shuffle with Fisher–Yates
The most common shuffle bug in JavaScript card game code is deck.sort(() => Math.random() - 0.5). It looks random, but sorting algorithms compare cards an uneven number of times, so some orders come up noticeably more often than others. The Fisher–Yates shuffle fixes that: walk from the last card to the first and swap each one with a random card at or before it.
// ----- The deck -----
function newDeck() {
const deck = [];
for (let suit = 0; suit < 4; suit++) for (let rank = 0; rank < 13; rank++) deck.push({ rank, suit });
// Fisher–Yates shuffle: walk backwards and swap each card with a random card at or before it.
// Every one of the 52! orders is equally likely, unlike sorting with a random comparator.
for (let i = deck.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[deck[i], deck[j]] = [deck[j], deck[i]];
}
return deck;
}The i + 1 matters: a card must be allowed to stay where it is. We checked the shuffle by running it 20,000 times. Each of the 52 cards ended up on top between 339 and 421 times, close to the 385 you'd expect from a fair deck.
3. Dealing from the top
The deck is a stack: the end of the array is the top, so pop() takes the top card. Dealing goes row by row across the seven columns, one card goes to the pile and the remaining 16 become the stock. Every card is in exactly one array at any time, which is what proper deck handling means in practice: cards are moved, never copied or recreated.
function deal() {
const deck = newDeck();
columns = Array.from({ length: COLS }, () => []);
// Deal row by row from the top of the deck, like dealing by hand.
for (let row = 0; row < DEPTH; row++) for (const col of columns) col.push(deck.pop());
pile = [deck.pop()];
stock = deck; // the 16 cards left; the top of the stock is the end of the array
score = 0; streak = 0; cursor = 0; anim = null;
state = "playing";
}4. The rule in five lines
Golf has one rule, so it gets one function. The rank difference must be 1, or 12 when Ace-King wrapping is on (Ace is 0 and King is 12).
// The only rule: a card fits if it is one rank above or below the top of the pile.
function fits(card) {
const diff = Math.abs(card.rank - topCard(pile).rank);
return diff === 1 || (WRAP && diff === 12);
}The helper is called topCard rather than top for a reason worth knowing: top is already a read-only property of window (it points at the top-level frame), and declaring const top in a script throws an error before the game starts.
5. Playing a card and drawing from the stock
Both moves take a card from one array and push it onto the pile. Playing a column card extends your run and scores 10 × run length, so a run of five is worth 10 + 20 + 30 + 40 + 50 = 150 points. Drawing from the stock breaks the run. That small scoring choice gives the game its strategy: when two cards fit, pick the one that opens the longer chain.
function playColumn(c) {
if (state !== "playing") return;
const col = columns[c];
if (!col.length) return;
const card = topCard(col);
if (!fits(card)) { nope = { c, t: 0.3 }; return; }
col.pop();
streak++;
score += POINTS * streak; // long runs without drawing score more
startMove(card, colPos[c].x, colPos[c].y + col.length * fan);
pile.push(card);
checkEnd();
}
function drawFromStock() {
if (state !== "playing" || !stock.length) return;
const card = stock.pop();
streak = 0;
startMove(card, stockBox.x, stockBox.y);
pile.push(card);
checkEnd();
}A wrong tap shakes the card (nope) instead of doing nothing, so the player learns the rule by trying.
6. Winning, and knowing when you're stuck
After every move, checkEnd() asks two questions. Are the columns empty? Then you've won, with 50 bonus points per card still in the stock. Is the stock empty with no column card that fits? Then the game is over, and the game says so instead of leaving the player hunting for a move that doesn't exist.
function checkEnd() {
const left = columns.reduce((n, col) => n + col.length, 0);
if (left === 0) {
score += stock.length * STOCK_BONUS;
state = "won";
} else if (!stock.length && !columns.some((col) => col.length && fits(topCard(col)))) {
state = "stuck";
} else return;
best = Math.max(best, score);
endedAt = performance.now();
}Golf is a game of luck and planning. We played 2,000 deals with a bot that always takes the first card that fits: it cleared the board 7% of the time. A bot that looks ahead for the longest run cleared 17% of 1,000 deals, so planning roughly doubles your chances.
7. Drawing cards on a canvas
Each card is a rounded rectangle (ctx.roundRect, see MDN), a corner label and a large suit symbol. The corner label sits in the strip that stays visible when cards overlap, so a covered card is still readable, and the big symbol is skipped for covered cards.
function drawCard(card, x, y, glow, covered) {
roundBox(x, y, cw, ch, "#fbfaf4", "#b9b3a2");
if (glow) { ctx.lineWidth = 3; ctx.strokeStyle = glow; ctx.stroke(); }
ctx.fillStyle = card.suit === 1 || card.suit === 2 ? "#cc2f2a" : "#1d1d26";
ctx.textBaseline = "middle"; ctx.textAlign = "left";
ctx.font = `700 ${Math.max(12, cw * 0.25)}px system-ui, sans-serif`;
ctx.fillText(RANKS[card.rank] + SUITS[card.suit], x + cw * 0.08, y + fan * 0.5); // readable when covered
if (covered) return;
ctx.textAlign = "center";
ctx.font = `${cw * 0.5}px system-ui, sans-serif`;
ctx.fillText(SUITS[card.suit], x + cw / 2, y + ch * 0.62);
}Cards that fit glow gold, which teaches the rule faster than any text. This is an HTML5 canvas card game, but cards also work well as DOM elements with CSS transitions; our comparison of canvas, SVG and the DOM covers when each one fits.
8. Animation that never blocks the rules
When a card moves, the rules update the arrays immediately and startMove() only records where the card came from. For 0.22 seconds the loop draws it sliding to the pile with an ease-out curve, while the pile shows the card underneath. Because the game state is already correct, a fast player can tap again mid-animation and nothing breaks.
// ----- Main loop: only animations move, the rules run on input -----
function loop(now) {
const dt = Math.min(0.05, (now - last) / 1000);
last = now;
if (anim && (anim.t += dt / 0.22) >= 1) anim = null;
if (nope && (nope.t -= dt) <= 0) nope = null;
draw();
requestAnimationFrame(loop);
}Make it work on phones
Card games suit phones, but small cards make small targets. On a 390-pixel screen each card is only about 48 pixels wide, so the code doesn't make you hit the last card exactly: a tap anywhere in a column plays that column. Pointer events give mouse, pen and touch one code path.
canvas.addEventListener("pointerdown", (e) => {
const x = e.clientX, y = e.clientY;
keyboardUsed = false;
if (state !== "playing") { startOrRestart(); return; }
if (inBox(x, y, newBtn)) { deal(); return; }
if (inBox(x, y, stockBox)) { drawFromStock(); return; }
// A tap anywhere on a column plays its last card: big targets for fingers.
const c = colPos.findIndex((p) => inBox(x, y, { x: p.x, y: p.y, w: cw, h: ch + (DEPTH - 1) * fan }));
if (c >= 0) { cursor = c; playColumn(c); }
});resize() picks the largest card size that fits seven columns across and the whole table down, and on tall screens it fans the columns out further so more of each card shows. On a keyboard, the arrow keys choose a column, Up plays it, Space draws and the keys 1 to 7 play a column directly. For more on layouts that work in both orientations, see our guide to HTML5 mobile game development.
Do you need an HTML5 card game engine?
Not for a game like this. A card game is mostly arrays and rules, and the canvas API covers the drawing. A JavaScript card game engine or framework starts to pay off when you need drag-and-drop with snapping, many animated cards at once, particle effects, sound, scenes and menus. Phaser handles all of those and is what our 2D games use. If you're looking for HTML5 card game open source projects to study, this file is a good small one: MIT-licensed, one file, no dependencies.
Ideas to extend it
- Add undo: push each move onto a history array and pop it to reverse the move.
- Let players drag cards to the pile as well as tapping them.
- Turn it into TriPeaks by laying the 28 tableau cards in three overlapping pyramids and tracking which cards are uncovered.
- Add a hint button that runs the same look-ahead as the 17% bot and highlights its choice.
- Deal from a seed, so two players can race on the same deal.
- Play a soft card sound with the Web Audio API on each move, started after the first tap.
From prototype to a game you can sell
This file is a complete little game, but a commercial card game is a different scale of project:
| This Golf Solitaire file | A production card game | |
|---|---|---|
| Content | One game type, one deck | Many cards with effects, unlocks, collections |
| Systems | One rule and a score | Decks, combos, enemies, shops, progression |
| Art and audio | Drawn rectangles and symbols | Illustrated cards, animation, music and effects |
| Saves | Best score lost on reload | Runs and unlocks saved, resume any time |
| Onboarding | A rules screen | A guided tutorial |
| Platforms | Desktop and phone browsers | Portal SDK hooks, localisation, gamepad |
| Licence | MIT | A commercial licence for your product |
Card Combo Rogue is our deckbuilding roguelite: you build a deck during each run and combine fire, machine, nature, magic and poison cards, with relics, a branching map, bosses and a guided tutorial. Like all our games, it ships as a TypeScript project built on Phaser 4.2.1, with its balance in JSON data files.
Card Combo Rogue
Build a temporary deck during each run and exploit tag-based synergies across fire, machine, nature, magic and poison cards.
For more turn-based games, see our strategy games. For other free code, browse the HTML game code collection; the memory game shuffles its deck with Fisher–Yates too.
The complete code
One file, no libraries. Save it as card-game.html and open it in any browser. Free to use under the MIT license: copy it, change it, ship it.
<!doctype html>
<!-- SPDX-License-Identifier: MIT -->
<!-- Golf Solitaire: a complete card game in one HTML file. Free to use, change and ship (MIT license). -->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Golf Solitaire (HTML5 card game)</title>
<style>
html, body { margin: 0; height: 100%; background: #0f3b2c; color: #f4f1e8; font: 16px system-ui, sans-serif; overflow: hidden; touch-action: none; user-select: none; -webkit-user-select: none; }
canvas { display: block; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<script>
// ----- Settings -----
const COLS = 7, DEPTH = 5; // 7 columns of 5 face-up cards; the other 17 cards start the stock and the pile
const WRAP = true; // Ace and King count as neighbours
const RANKS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"];
const SUITS = ["♠", "♥", "♦", "♣"]; // hearts and diamonds are red
const POINTS = 10; // points for a card, times the length of the current run
const STOCK_BONUS = 50; // bonus per stock card left when you clear the board
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
let W = 0, H = 0, cw = 60, ch = 84, gap = 8, fan = 24, colPos = [], stockBox, pileBox, newBtn;
// ----- Sizing: the biggest cards that fit 7 columns across and the whole table down -----
function resize() {
const dpr = window.devicePixelRatio || 1;
W = window.innerWidth; H = window.innerHeight;
canvas.style.width = W + "px"; canvas.style.height = H + "px";
canvas.width = Math.round(W * dpr); canvas.height = Math.round(H * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
cw = Math.max(30, Math.min(120, (W - 20) / (COLS + (COLS - 1) * 0.12), (H - 90) / 5.2));
ch = cw * 1.4; gap = cw * 0.12;
fan = Math.min(ch * 0.5, Math.max(ch * 0.3, (H - 100 - ch * 2.45) / (DEPTH - 1))); // tall screens fan out more
const tableH = ch + (DEPTH - 1) * fan + ch * 0.45 + ch + 26;
const top = Math.max(50, 44 + (H - 44 - tableH) / 2);
const left = (W - (COLS * cw + (COLS - 1) * gap)) / 2;
colPos = Array.from({ length: COLS }, (_, c) => ({ x: left + c * (cw + gap), y: top }));
const rowY = top + ch + (DEPTH - 1) * fan + ch * 0.45;
stockBox = { x: W / 2 - cw - gap * 2, y: rowY, w: cw, h: ch };
pileBox = { x: W / 2 + gap * 2, y: rowY, w: cw, h: ch };
newBtn = { x: W - 104, y: 8, w: 94, h: 30 };
}
// ----- The deck -----
function newDeck() {
const deck = [];
for (let suit = 0; suit < 4; suit++) for (let rank = 0; rank < 13; rank++) deck.push({ rank, suit });
// Fisher–Yates shuffle: walk backwards and swap each card with a random card at or before it.
// Every one of the 52! orders is equally likely, unlike sorting with a random comparator.
for (let i = deck.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[deck[i], deck[j]] = [deck[j], deck[i]];
}
return deck;
}
// ----- Game state -----
let columns = [], stock = [], pile = [], score = 0, best = 0, streak = 0, cursor = 0;
let state = "ready"; // ready → playing → won | stuck → playing …
let anim = null, nope = null, endedAt = 0, last = 0, keyboardUsed = false;
function deal() {
const deck = newDeck();
columns = Array.from({ length: COLS }, () => []);
// Deal row by row from the top of the deck, like dealing by hand.
for (let row = 0; row < DEPTH; row++) for (const col of columns) col.push(deck.pop());
pile = [deck.pop()];
stock = deck; // the 16 cards left; the top of the stock is the end of the array
score = 0; streak = 0; cursor = 0; anim = null;
state = "playing";
}
const topCard = (arr) => arr[arr.length - 1];
// The only rule: a card fits if it is one rank above or below the top of the pile.
function fits(card) {
const diff = Math.abs(card.rank - topCard(pile).rank);
return diff === 1 || (WRAP && diff === 12);
}
function playColumn(c) {
if (state !== "playing") return;
const col = columns[c];
if (!col.length) return;
const card = topCard(col);
if (!fits(card)) { nope = { c, t: 0.3 }; return; }
col.pop();
streak++;
score += POINTS * streak; // long runs without drawing score more
startMove(card, colPos[c].x, colPos[c].y + col.length * fan);
pile.push(card);
checkEnd();
}
function drawFromStock() {
if (state !== "playing" || !stock.length) return;
const card = stock.pop();
streak = 0;
startMove(card, stockBox.x, stockBox.y);
pile.push(card);
checkEnd();
}
function checkEnd() {
const left = columns.reduce((n, col) => n + col.length, 0);
if (left === 0) {
score += stock.length * STOCK_BONUS;
state = "won";
} else if (!stock.length && !columns.some((col) => col.length && fits(topCard(col)))) {
state = "stuck";
} else return;
best = Math.max(best, score);
endedAt = performance.now();
}
// A card flying to the pile; the logic has already moved it, this is only for the eyes.
function startMove(card, x, y) { anim = { card, x0: x, y0: y, t: 0 }; }
// ----- Drawing -----
function roundBox(x, y, w, h, fill, stroke) {
ctx.beginPath(); ctx.roundRect(x, y, w, h, Math.max(3, w * 0.08));
ctx.fillStyle = fill; ctx.fill();
if (stroke) { ctx.lineWidth = 1; ctx.strokeStyle = stroke; ctx.stroke(); }
}
function drawCard(card, x, y, glow, covered) {
roundBox(x, y, cw, ch, "#fbfaf4", "#b9b3a2");
if (glow) { ctx.lineWidth = 3; ctx.strokeStyle = glow; ctx.stroke(); }
ctx.fillStyle = card.suit === 1 || card.suit === 2 ? "#cc2f2a" : "#1d1d26";
ctx.textBaseline = "middle"; ctx.textAlign = "left";
ctx.font = `700 ${Math.max(12, cw * 0.25)}px system-ui, sans-serif`;
ctx.fillText(RANKS[card.rank] + SUITS[card.suit], x + cw * 0.08, y + fan * 0.5); // readable when covered
if (covered) return;
ctx.textAlign = "center";
ctx.font = `${cw * 0.5}px system-ui, sans-serif`;
ctx.fillText(SUITS[card.suit], x + cw / 2, y + ch * 0.62);
}
function drawBack(x, y) {
roundBox(x, y, cw, ch, "#24508f", "#0d2547");
ctx.strokeStyle = "rgba(255, 255, 255, 0.35)"; ctx.lineWidth = 2;
ctx.strokeRect(x + cw * 0.12, y + cw * 0.12, cw * 0.76, ch - cw * 0.24);
}
function text(str, x, y, size, align = "center", color = "#f4f1e8", weight = 600) {
ctx.font = `${weight} ${size}px system-ui, sans-serif`;
ctx.fillStyle = color; ctx.textAlign = align; ctx.textBaseline = "middle";
ctx.fillText(str, x, y);
}
function draw() {
ctx.fillStyle = "#0f3b2c"; ctx.fillRect(0, 0, W, H);
text(`Score ${score}`, 12, 23, 17, "left");
text(`Best ${best}`, W / 2, 23, 15, "center", "#bcd9c6", 500);
roundBox(newBtn.x, newBtn.y, newBtn.w, newBtn.h, "#1d5a43", "#3f8a6b");
text("New deal", newBtn.x + newBtn.w / 2, newBtn.y + newBtn.h / 2, 14);
// Columns: only the last card of each one can be played, and it glows when it fits.
columns.forEach((col, c) => {
const p = colPos[c];
const shake = nope && nope.c === c ? Math.sin(nope.t * 60) * 4 : 0;
if (!col.length) roundBox(p.x, p.y, cw, ch, "rgba(255, 255, 255, 0.06)", "rgba(255, 255, 255, 0.15)");
col.forEach((card, k) => {
const last = k === col.length - 1;
const glow = last && state === "playing" ? (fits(card) ? "#ffc94a" : c === cursor && keyboardUsed ? "#7fb2ff" : null) : null;
drawCard(card, p.x + (last ? shake : 0), p.y + k * fan, glow, !last);
});
if (keyboardUsed && c === cursor && state === "playing") {
ctx.fillStyle = "#7fb2ff"; ctx.fillRect(p.x + cw * 0.2, p.y - 10, cw * 0.6, 4);
}
});
// Stock (face down) and pile (face up). While a card flies, the pile shows the card under it.
if (stock.length) drawBack(stockBox.x, stockBox.y);
else roundBox(stockBox.x, stockBox.y, cw, ch, "rgba(255, 255, 255, 0.06)", "rgba(255, 255, 255, 0.15)");
text(`Stock ${stock.length}`, stockBox.x + cw / 2, stockBox.y + ch + 14, 13, "center", "#bcd9c6", 500);
const shown = anim ? pile[pile.length - 2] : topCard(pile);
if (shown) drawCard(shown, pileBox.x, pileBox.y);
text(streak > 1 ? `Run ×${streak}` : "Pile", pileBox.x + cw / 2, pileBox.y + ch + 14, 13, "center", streak > 1 ? "#ffc94a" : "#bcd9c6", 500);
if (anim) {
const e = 1 - Math.pow(1 - anim.t, 3); // ease-out
drawCard(anim.card, anim.x0 + (pileBox.x - anim.x0) * e, anim.y0 + (pileBox.y - anim.y0) * e);
}
if (state !== "playing" && !anim) {
ctx.fillStyle = "rgba(4, 22, 15, 0.9)"; ctx.fillRect(0, 0, W, H);
const cx = W / 2, cy = H / 2, s = Math.min(1, W / 460);
if (state === "ready") {
text("Golf Solitaire", cx, cy - 92 * s, 38 * s, "center", "#ffc94a", 800);
text("Clear all 35 cards from the seven columns.", cx, cy - 40 * s, 16 * s);
text("Play the bottom card of a column if it is", cx, cy - 14 * s, 16 * s);
text("one rank above or below the pile. A and K connect.", cx, cy + 10 * s, 16 * s);
text("No move? Draw a card from the stock.", cx, cy + 36 * s, 16 * s);
text("Keys: ← → choose, ↑ play, Space draw, 1–7 play", cx, cy + 72 * s, 14 * s, "center", "#bcd9c6", 500);
text("Tap or press Enter to deal", cx, cy + 110 * s, 18 * s, "center", "#ffc94a", 700);
} else {
const cleared = 35 - columns.reduce((n, col) => n + col.length, 0);
text(state === "won" ? "Board cleared!" : "No moves left", cx, cy - 60 * s, 36 * s, "center", "#ffc94a", 800);
text(`${cleared} of 35 cards · score ${score} · best ${best}`, cx, cy - 10 * s, 17 * s);
if (state === "won") text(`Includes ${STOCK_BONUS} points for each card left in the stock`, cx, cy + 18 * s, 14 * s, "center", "#bcd9c6", 500);
text("Tap or press Enter to deal again", cx, cy + 64 * s, 18 * s, "center", "#ffc94a", 700);
}
}
}
// ----- Main loop: only animations move, the rules run on input -----
function loop(now) {
const dt = Math.min(0.05, (now - last) / 1000);
last = now;
if (anim && (anim.t += dt / 0.22) >= 1) anim = null;
if (nope && (nope.t -= dt) <= 0) nope = null;
draw();
requestAnimationFrame(loop);
}
// ----- Input -----
const inBox = (x, y, b) => x >= b.x && x <= b.x + b.w && y >= b.y && y <= b.y + b.h;
function startOrRestart() {
if (state === "ready" || performance.now() - endedAt > 600) deal();
}
canvas.addEventListener("pointerdown", (e) => {
const x = e.clientX, y = e.clientY;
keyboardUsed = false;
if (state !== "playing") { startOrRestart(); return; }
if (inBox(x, y, newBtn)) { deal(); return; }
if (inBox(x, y, stockBox)) { drawFromStock(); return; }
// A tap anywhere on a column plays its last card: big targets for fingers.
const c = colPos.findIndex((p) => inBox(x, y, { x: p.x, y: p.y, w: cw, h: ch + (DEPTH - 1) * fan }));
if (c >= 0) { cursor = c; playColumn(c); }
});
window.addEventListener("keydown", (e) => {
const k = e.code;
if (["Space", "Enter", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(k)) e.preventDefault();
if (state !== "playing") { if (k === "Enter" || k === "Space") startOrRestart(); return; }
keyboardUsed = true;
if (k === "ArrowLeft" || k === "KeyA") cursor = (cursor + COLS - 1) % COLS;
else if (k === "ArrowRight" || k === "KeyD") cursor = (cursor + 1) % COLS;
else if (k === "ArrowUp" || k === "KeyW" || k === "Enter") playColumn(cursor);
else if (k === "ArrowDown" || k === "KeyS" || k === "Space") drawFromStock();
else if (k === "KeyN") deal();
else if (/^Digit[1-7]$/.test(k)) { cursor = Number(k.slice(5)) - 1; playColumn(cursor); }
});
window.addEventListener("resize", resize);
resize();
deal(); state = "ready"; // show a dealt table behind the start screen
requestAnimationFrame((t) => { last = t; loop(t); });
</script>
</body>
</html>
Frequently asked questions
Is this card game source code free and open source?
Yes. The file is MIT-licensed, so you can study it, change it and use it in personal or commercial projects, including games you sell. Keep the licence line at the top of the file.
Can I make an HTML5 card game in C#?
Browsers run JavaScript and WebAssembly, so C# reaches the browser through WebAssembly, with tools such as Blazor or Unity's Web build. For a small card game, plain JavaScript is simpler and much lighter to load. Our guide to making HTML5 games in other languages compares the options.
Is Math.random good enough for shuffling cards?
For a game like this, yes. For anything involving real money, use crypto.getRandomValues for the random index, and follow the certification rules of your market, which go well beyond the shuffle.
How do I put this card game on my website?
Upload the HTML file to your host and link to it, or show it in an <iframe>. Our guide to embedding an HTML5 game in a website covers WordPress, Wix and Google Sites.
How do I keep the best score after a reload?
Save best with localStorage.setItem in checkEnd() and read it back before the first deal. Wrap both calls in try/catch, because sandboxed frames like the player on this page block storage.
We make and sell HTML5 game source code; links to our games are marked as ours.
