Game Development 15 min read

Build a One‑Stroke Puzzle Game with Auto‑Graph Recognition

This article explains how to implement a one‑stroke puzzle game using graph theory, describes the two‑step rendering process, provides JavaScript pseudocode for touch interaction, and details an automatic image‑recognition tool that extracts level data from hand‑drawn diagrams while optimizing performance.

Aotu Lab
Aotu Lab
Aotu Lab
Build a One‑Stroke Puzzle Game with Auto‑Graph Recognition

Background

The one‑stroke puzzle originates from Euler’s Seven Bridges of Königsberg problem. In graph‑theoretic terms, a connected graph has an Eulerian trail that traverses every edge exactly once without repetition.

Game implementation

Base map rendering

Levels are defined through a configuration interface. Two representation options exist:

Point notation – stores an ordered list of vertex coordinates.

Line notation – stores unordered line segments as pairs of endpoints.

Example configuration for a five‑point star using point notation:

levels: [
  {
    name: "Five‑Star",
    coords: [
      {x: Ax, y: Ay},
      {x: Bx, y: By},
      {x: Cx, y: Cy},
      {x: Dx, y: Dy},
      {x: Ex, y: Ey},
      {x: Ax, y: Ay}
    ]
  }
]

Using line notation the same level looks like:

levels: [
  {
    name: "Five‑Star",
    lines: [
      {x1: Ax, y1: Ay, x2: Bx, y2: By},
      {x1: Bx, y1: By, x2: Cx, y2: Cy},
      {x1: Cx, y1: Cy, x2: Dx, y2: Dy},
      {x1: Dx, y1: Dy, x2: Ex, y2: Ey},
      {x1: Ex, y1: Ey, x2: Ax, y2: Ay}
    ]
  }
]

Line notation is preferred because it does not require storing a solved path.

Interactive drawing

The user draws a path by touching vertices. Two problems must be solved:

Detect whether a touch is on a vertex.

Determine whether the touched vertex can be connected to the currently selected vertex.

Collecting vertex coordinates from the line data:

let coords = [];
lines.forEach(({x1, y1, x2, y2}) => {
  if (!isExist(x1, y1)) coords.push([x1, y1]);
  if (!isExist(x2, y2)) coords.push([x2, y2]);
});

Touch‑move listener (simplified):

easel.addEventListener("touchmove", e => {
  let x0 = e.targetTouches[0].pageX,
      y0 = e.targetTouches[0].pageY;
  let r = radius * 2; // enlarged detection radius
  for (let [x, y] of coords) {
    if (Math.sqrt(Math.pow(x - x0, 2) + Math.pow(y - y0, 2)) <= r) {
      if (canConnect(x, y)) {
        // draw line logic here
      }
      break;
    }
  }
});

Each vertex stores a list of valid connection points derived from the level’s line data. Only vertices that are not already linked ("unconnected valid points") are considered for new connections.

Automatic image recognition

An auto‑recognition plugin scans a bitmap of the puzzle, extracts dominant colors, and identifies vertices and edges.

Color table extraction

let imageData = ctx.getImageData();
let data = imageData.data;
let clrs = new Map();
for (let i = 0, len = data.length; i < len; i += 4) {
  let [r, g, b, a] = [data[i], data[i+1], data[i+2], data[i+3]];
  let key = `rgba(${r},${g},${b},${a})`;
  let value = clrs.get(key) || {r,g,b,a,count:0};
  clrs.has(key) ? ++value.count : clrs.set(key, value);
}

The three most frequent colors correspond to background, edges, and vertices. By ignoring the background and selecting the second‑most and third‑most frequent colors, the algorithm determines edge and vertex colors.

Vertex detection

The algorithm scans pixels, records vertex‑color pixels, clears them, and continues. Small spurious vertices caused by compression are merged:

for (let i = 0, len = vertexes.length; i < len - 1; ++i) {
  let vertexA = vertexes[i];
  if (vertexA === undefined) continue;
  for (let j = 0; j < len; ++j) {
    let vertexB = vertexes[j];
    if (vertexB === undefined) continue;
    if (isCross(vertexA, vertexB)) {
      vertexA = merge(vertexA, vertexB);
      delete vertexB;
    }
  }
}

This merging yields accurate vertex detection even on lossy images.

Edge detection

For each pair of vertices, the algorithm samples N points along the line (where N = distance / (2 * R), R being the vertex radius). If all sampled pixels match the edge color, the segment is accepted:

for (let i = 0, len = vertexes.length; i < len - 1; ++i) {
  let {x: x1, y: y1} = vertexes[i];
  for (let j = i + 1; j < len; ++j) {
    let {x: x2, y: y2} = vertexes[j];
    let S = Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
    let N = S / (2 * R);
    let stepX = (x2 - x1) / N, stepY = (y2 - y1) / N;
    while (--N) {
      if (!isBelongLine(x1 + N * stepX, y1 + N * stepY)) break;
    }
    if (N === 0) lines.push({x1, y1, x2, y2});
  }
}

This approach avoids false positives when three vertices are collinear but only two are actually connected.

Performance optimisation

Scanning a full‑size image (e.g., 750 × 1334) processes roughly 4 million values. Down‑scaling the canvas reduces the pixel array size by the square of the scaling factor:

let resolution = 4;
let [width, height] = [img.width / resolution >> 0, img.height / resolution >> 0];
ctx.drawImage(img, 0, 0, width, height);
let imageData = ctx.getImageData();
let data = imageData.data;

Scaling by 4 reduces the array size 16‑fold, providing a substantial speed boost.

Usage recommendations

The auto‑recognition tool works well for the author’s own images but may struggle with arbitrary user‑uploaded graphics. It is advisable to expose it as a separate utility that generates level JSON files.

Demo page: https://leeenx.github.io/OneStroke/src/onestroke.html

Auto‑recognition tool: https://leeenx.github.io/OneStroke/src/plugin.html

Repository: https://github.com/leeenx/OneStroke

Game core source: https://github.com/leeenx/OneStroke/blob/master/src/script/onestroke.es6

Plugin source: https://github.com/leeenx/OneStroke/blob/master/src/script/oneStrokePlugin.es6

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaScriptImage ProcessingCanvasgraph theoryone-stroke
Aotu Lab
Written by

Aotu Lab

Aotu Lab, founded in October 2015, is a front-end engineering team serving multi-platform products. The articles in this public account are intended to share and discuss technology, reflecting only the personal views of Aotu Lab members and not the official stance of JD.com Technology.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.