en:authors_main:task_editor:advanced:gps

Codes by GPS Coordinates

The engine can submit a code by itself once the player reaches the right spot on the ground. There are two ways to do it:

  • A server trigger — the zone is written as a {! on gps … !} marker in the task text. The target's coordinates are nowhere in the player's page: not in the text, not in the script, not in the server's answers. This is the main way.
  • Points in the script — the g_targets array in the “Task script”. The check runs in the browser, so the target's coordinates sit in the page source. The way is older, but you need it when the browser itself has to know where the target is: to show the distance or draw the point on a map.
Important: geolocation is enabled only with the player's permission. If the player refuses, no zone will fire, so leave a fallback way to pass the level (an ordinary code, a hint that gives the code away) or warn the teams in advance that GPS is required.
The team's track. While tracking is on, the engine records the team's path — per device, as “stood” and “walked” segments, plus zone hits and rejected fixes. The game page gives the author a “GPS tracks” link: a report with signs of spoofed geolocation per team (the verdict is not a sentence — every sign has honest explanations), disputed moments such as “reached the zone, but the code was not entered”, and a map. A track is kept for 90 days from its last entry; it can be deleted earlier on the same page.

Both ways start the same — with a gps_start() call in the “Task script” field:

gps_start();

Without it the browser won't ask for coordinates and no zone will fire. If a task has triggers but no gps_start() anywhere, the editor warns about it on save.

The engine loads the geolocation library only for tasks that need it — there is nothing else to switch on.

A task with GPS has a satellite icon next to its title. It shows the player whether geolocation works and turns it on as well: on a phone the browser hands out coordinates only after a tap, which is why the icon is there from the very start, before the first fix.

The GPS icon in the task header: green means you can go

Colour What it means What a tap does
grey tracking is off turns it on
red no position: searching for satellites, the player denied access, or geolocation is unavailable starts the search again or explains how to allow access
yellow there is a position, but its accuracy is worse than 30 metres — the engine doesn't count such a fix refreshes the position
green everything works, you can go refreshes the position

Hovering over the icon shows the state in words and the current accuracy.

A zone is declared with a marker in the task text:

{! on gps 51.5299 -0.1275 radius 30: code newton !}
  • 51.5299 -0.1275 — the centre of the zone: latitude and longitude, the way they are copied from Google Maps or OpenStreetMap. A comma between them is optional.
  • radius 30 — the radius in metres. You can leave it out — the default is 30.
  • code newton — what to do when the player is in the zone: enter a code. The code must be a code or a bonus of this task.
  • A label for yourself goes into a comment inside the marker: {! on gps /* back yard */ 51.5299 -0.1275: code newton !}. The player won't see it; the tester will see it on the map.

The general marker rules — where it fires, what the save checks, how several actions are written — are on the Server Actions page.

A yard, a square, a block — a polygon zone. After the word polygon come the vertex coordinates in a row, in “latitude longitude” pairs, from 3 to 30 vertices. A polygon has no radius — its vertices set the boundary.

{! on gps /* square */ polygon 51.5305 -0.1280 51.5320 -0.1262 51.5313 -0.1230 51.5291 -0.1238: code square !}

While the player walks, the page sends their coordinates to the server — when they have moved at least a few metres, but no more often than every couple of seconds. The server answers which zone the player is standing in, and the page enters that zone's code — silently, as an ordinary answer. In the answer log such a code is recorded under the player who got there.

  • The engine does not show the distance to the target — from the distances at a few spots the target is easy to work out. If the player needs a “warmer or colder” hint, that is the older way, g_targets.
  • Fixes with accuracy worse than 30 metres don't count at all. A position from wifi or from an address arrives with hundreds of metres of error, and its centre lands in any zone by chance — the code would be entered for someone who never went anywhere.
  • Jumps don't count: a fix by which the player moved faster than 150 km/h is dropped.
  • Faking geolocation in a browser is still possible, and there is no way to close that. The point is elsewhere: the target is not in the page, and getting round a zone takes deliberate effort, not one look at the page source.

A plain code is entered by a zone once. A field condition in square brackets lets it enter again — and this is where it matters most:

{! on gps /* north yard */ 51.5221 -0.1072 radius 30: code [c1 !=] north !}
{! on gps /* south yard */ 51.5201 -0.1072 radius 30: code [c1 !=] south !}

Two zones on one sector make a switch: the first sector holds the code of the zone the team was in last. A {% match code_1 %} template shows from it where the team is now. The zones of a switch must not overlap: standing in both at once, the player gets both codes one after another.

{! on gps /* far */      51.5190 -0.1072 radius 30: code [c2 <] 1far !}
{! on gps /* at door */  51.5215 -0.1072 radius 30: code [c2 <] 4door !}

A set of zones with < makes a priority: a code with a bigger leading number replaces a smaller one, but not the other way round. So the sector keeps the best spot reached.

More on field conditions — on the Server Actions page.

Sometimes a zone doesn't need to enter anything — you only want to show the player something while they stand by the gazebo. That is what the zone action with a name is for:

{! on gps /* gazebo */ 51.5241 -0.1072 radius 30: zone gazebo !}

Such a zone has no code. The page tells the task script that the player entered or left the zone — once per change, not on every fix:

QEng.Gps.onZone = function (name, inside) {
  if (name === 'gazebo') $('#gazebo').toggle(inside);
};
  • The zone name is everything after the word zone; spaces are allowed. It arrives only to someone already standing in the zone — the page still doesn't know where the zone is.
  • When the task is redrawn (a code entered, a hint arrived), the engine reports “entered” again for the zones the player is still in: the redraw wipes out what the script drew in the text.
  • A zone that both enters a code and names itself to the script is one marker: {! on gps 51.5299 -0.1275: code newton: zone yard !}.

A marker works as long as the text it stands in is visible. So a zone in a bonus description exists while the bonus is unsolved, and a zone in a hint appears together with the hint. That way the set of zones changes over the level without a single line of script.

A zone doesn't finish the game by itself. It takes two steps: the zone enters a code, and finish_game stands in a template branch on that code:

{! on gps 51.5299 -0.1275: code finish !}
{% match code_1 }:{ {%= finish}:{ {! finish_game !} %} %}

Example in game: a switch, a polygon, a priority, a zone without a code, zones in a bonus and in a hint

The tester has a “GPS” tab in the testing panel at the bottom of the page — it appears on a task whose script calls gps_start().

The "GPS" tab of the testing panel: the level's zones on a map and the player's position

  • The map shows every zone of the level, with numbers and labels from the comments. Zones from template branches that aren't open right now are shown too: that way you can see whether zones overlap. The zones working right now are marked.
  • You can fake your position: click the map, drag the player marker, or type latitude and longitude and press “Apply”. The engine checks the zones against the new spot right away. The speed check doesn't get in the tester's way — jump around the map as much as you like.
  • “Drop the fake” returns to the real sensor, “Show all” fits the map to the player and every zone.
  • The zone list is refreshed together with the task: once a bonus is solved, its zone leaves the map too.

What the markers entered on this page load is shown by the neighbouring “Actions” tab — see Server Actions.

The older way, and it works as it always did. The coordinates of the points sit in the page source, so it won't do for a “find the place” riddle — the player opens the source and sees the target. You need it when the browser itself has to know where the target is: to show the distance or draw the point on your own map.

Points are put into the g_targets array. Each point is an object with coordinates, a radius and actions:

gps_start();
 
window.target_lat1 = 51.5211367;   // Latitude of the point
window.target_lon1 = -0.10723867;  // Longitude of the point
 
g_targets.push({
  'lat': window.target_lat1,
  'lon': window.target_lon1,
  'r': 30,  // Radius in METRES
  'action_inside': "enter_silent('finish_code_123')",
  'action_outside': "$('#distance_to').text(gps_distance(window.target_lat1, window.target_lon1).toFixed(0))"
});
  • lat, lon — the coordinates of the point.
  • r — the trigger radius in metres.
  • action_inside — a string of JavaScript, executed while the player is inside the radius.
  • action_outside — a string of JavaScript, executed while the player is outside the radius.

There can be as many points as you like — just push several objects into g_targets.

The actions run on every coordinate update, not once when the player enters the zone. As long as the player stands inside the radius, action_inside will fire again and again. For submitting a code that is harmless: enter() and enter_silent() send a code only once. Showing a message or playing a sound, however, is better guarded with a flag of your own:
'action_inside': "if (!window.was_inside) { window.was_inside = 1; play_sound('limit'); }"

The function gps_distance(latitude, longitude) returns the distance from the player's current position to the given point in metres. It is usually shown to the player so they can tell whether they are getting warmer or colder:

<p>Distance to the point: <span id="distance_to">???</span> m.</p>

A level with a point in the script: the distance to it is computed by the player's browser

The function can only be called once the first coordinates have arrived — before that the player's position is unknown. The easiest way is to call it from action_outside, as in the example above: the engine will run it itself as soon as the coordinates appear.

If you need more than a radius check — drawing a map or your own indicator, say — override the on_update_position() function: the engine calls it every time new coordinates arrive. The current position is in g_position (the lat and lon fields, as well as accuracy in metres, timestamp, altitude, speed, heading):

gps_start();
 
on_update_position = function () {
  $('#my_coords').text(g_position.lat.toFixed(5) + ', ' + g_position.lon.toFixed(5) + ' (±' + Math.round(g_position.accuracy) + 'm)');
};

You can also monitor GPS sensor states via on_gps_status(status, details):

  • requesting — requesting browser permission;
  • searching — waiting for GPS satellite acquisition;
  • locked — coordinates acquired and actively updating;
  • timeout — satellite acquisition is taking longer (engine automatically soft-retries);
  • denied — user denied geolocation permission;
  • unavailable — geolocation is unsupported or disabled.

To stop tracking (e.g. between stages to save battery), use gps_stop(). Calling gps_start() again reuses the active watcher and immediately evaluates new targets without waiting for the next satellite tick.

The same functions are also available under the common name QEng.Gps: QEng.Gps.start() and stop(), distance(lat, lon), position, targets, addTarget({…}), onUpdatePosition, onStatus. The old names work and will keep working.

The service function init_debug_map(latitude, longitude, zoom) draws a map with your points and radii on it and puts down a draggable player marker. Wherever you drag the marker is where the engine will believe the player to be. The function expects a <div id=“debug_map”> on the page and works on Leaflet + OpenStreetMap, no API key needed. Server triggers don't need it — the tester has the “GPS” tab for that.

The engine does not load any mapping libraries — the author adds the map to the task text personally. The example below uses the free Leaflet with OpenStreetMap tiles: the library's CSS and JS are included, a map container is created, and a marker moves along with the player's coordinates.

A Leaflet example: the level's map with the player's marker on it

Keep in mind that the task is redrawn on every auto-refresh, so it is convenient to keep the state of the map (centre, zoom, the map object) in window, so that it is not created anew and does not jump around under the player.

If the plot only needs to send the player to a spot rather than check that they arrived, an ordinary link is enough. Insert it in “Source” mode:

<a href="geo:?q=59.249,37.465">59.249, 37.465</a>

The geo: scheme opens the coordinates in whatever navigation app is installed on the phone. For desktop browsers, which have no such scheme, a link to a map service will do:

<a href="https://www.google.com/maps/search/?api=1&query=49.940333,36.39305" target="_blank">49.940333, 36.39305</a>

Example of server triggers | Example of a point in the script | Example with a map | Example of links to coordinates

  • en/authors_main/task_editor/advanced/gps.txt
  • Last modified: 2026/09/23 08:33
  • (external edit)