Tuning

Required

tuning.json holds the shared numbers of your game: costs, speeds, sizes, timings. The builder, the person, team or AI that turns your package into a game, reads them from here, and your prose points at them by name. values is required; ranges and rules are optional, and so is open, for numbers you have not decided yet.

tuning.json, the numbers file

{
  "values": {
    "dash.speed_tiles_per_second": 12,
    "dash.duration_seconds": 0.15,
    "dash.cooldown_seconds": 0.6,
    "gap.width_tiles": 1,
    "level.width_tiles": 24,
    "room.width_tiles": 8
  },
  "ranges": {
    "dash.speed_tiles_per_second": [8, 20],
    "dash.duration_seconds": [0.1, 0.3],
    "dash.cooldown_seconds": [0.3, 1.5]
  },
  "rules": {
    "no-dash-while-dashing": "dash.cooldown_seconds >= dash.duration_seconds",
    "dash-covers-the-gap": "dash.speed_tiles_per_second * dash.duration_seconds >= gap.width_tiles"
  }
}

values gives each number one name and one value. A name has two or more parts joined by dots. A part uses letters, digits, underscores or hyphens, and may not begin or end with a hyphen; lowercase with underscores is the usual choice. A name made only of digits and dots, such as 1.2, is not allowed, because prose could not cite it.

ranges says how far a value may change. You may change it when you balance the design. The builder may choose any value inside the range while balancing the build. The exception is a value set by a personalization answer, the answer to a question each build answers for itself. Both end numbers are allowed values. A value without a range stays as written.

rules are one-line checks on your numbers. Each rule must stay true. Each rule has a name, in lowercase words joined by hyphens, and one comparison. no-dash-while-dashing requires the cooldown to be at least as long as the dash, so one dash ends before the next can start. dash-covers-the-gap requires one dash to be long enough to cross the gap. The validator (the checking tool) checks each rule over the values in the file and reports the ones that are false. A rule never changes a number.

Suppose that during balancing someone sets the speed to 8 and the duration to 0.1. Both are inside their ranges, but dash-covers-the-gap is now false:

rule "dash-covers-the-gap" does not hold
  dash.speed_tiles_per_second * dash.duration_seconds >= gap.width_tiles
  8 * 0.1 = 0.8, which is not >= gap.width_tiles = 1

The report names the rule, shows it as you wrote it, and shows the numbers that make it false. A false rule is an error: the package does not validate until the rule holds.

A rule is one comparison, using ==, !=, <, <=, > or >=, between two calculations. A calculation uses plain numbers and names from values and open, joined by +, -, * and /, with parentheses and the functions min, max and floor. floor rounds down, which is useful for a whole-number count:

tuning.json, a whole-number rule using values declared above

{
  "two-rooms-fit-across": "floor((level.width_tiles - 2) / room.width_tiles) >= 2"
}

Multiplication and division happen before addition and subtraction. Otherwise the calculation goes from left to right, so 8 / 2 / 2 is 2. The parentheses above subtract the two wall tiles first. Use parentheses when you are not sure.

The validator compares numbers exactly. A computer stores most decimal numbers with a very small error, so 0.1 + 0.2 == 0.3 is false.

A rule that divides by zero cannot be calculated. When every number in the rule comes from values, that is an error. When a guess causes it, the validator gives a warning about the guess. The section "Numbers you have not decided" below explains guesses.

A hyphen inside a dotted name is part of the name, so put a space before a minus sign that subtracts. A rule can also read a number on one item of a collection; that chapter shows how. A runtime value, which changes during play, cannot appear in a rule.

Rules are about the numbers written in the design. "The player never has more than three dashes stored" is about what happens during play, so it is an acceptance test, not a rule.

Numbers you have not decided#

Sometimes you know what a number is for, but not its value. "I think the stamina bar should refill in around 3 seconds, but I'm not sure." In values, 3 would be a decision: "it is 3; build it that way." Write it as an open number instead: your guess, and what the number must achieve. The builder finds the value that works.

tuning.json, the stamina numbers

{
  "values": {
    "stamina.max": 100,
    "stamina.dash_cost": 25,
    "dash.cooldown_seconds": 0.6
  },
  "open": {
    "stamina.refill_seconds": 3,
    "stamina.refill_delay_seconds": null
  },
  "rules": {
    "stamina-limits-dashing": "stamina.refill_seconds * stamina.dash_cost / stamina.max > dash.cooldown_seconds"
  }
}

stamina.refill_seconds holds a guess: 3. stamina.refill_delay_seconds, the pause before the bar starts to refill, is null: you have no guess yet. A name is in values or in open, not in both. Prose cites an open number by its name, like any other.

So each number in tuning.json is one of three kinds:

  • decided, in values: "it is 0.6; build it that way";
  • ranged, in values with a range: the builder may choose any value inside the range;
  • open, in open: "my guess is 3; find what works".

An open number may have a range too. The value the builder chooses must then be inside that range. Your guess must be inside it too. A guess outside the range is an error, not a warning.

An open number needs a target: what the number must achieve. Usually the target is a sentence next to its citation. With the target, the decision stays with you: the builder searches for a value, and that value must meet your target.

02-mechanics.md, the target beside the citation

The bar refills from empty in stamina.refill_seconds. A player who stops behind cover for a moment can dash again. A player who dashes every time the cooldown allows runs out of stamina.

A person judges that sentence; the validator does not. For a strict limit, where a wrong value would break the game, add a rule the validator can check. A rule or an acceptance test is a target too. If the bar refilled one dash's cost before the cooldown ended, a player could dash forever and the bar would mean nothing. stamina-limits-dashing forbids that.

The validator also checks your rules with your guesses. A guess that breaks a rule gives a warning, not an error, because a guess is only a starting point. With a guess of 2 seconds:

guesses break rule "stamina-limits-dashing"
  stamina.refill_seconds * stamina.dash_cost / stamina.max > dash.cooldown_seconds
  2 * 25 / 100 = 0.5, which is not > dash.cooldown_seconds = 0.6

A guess can also make a rule impossible to calculate, for example a guess of 0 for a number you divide by. That also gives a warning about the guess.

A rule over numbers you have decided is still an error when it is false. While you write, the validator skips a rule in one case only: one of its numbers has no decided value and no guess, so there is no number to use. A default answer to a personalization question can decide an open number. The validator then checks the rule with that answer.

Every rule is checked against the build. The builder's report of a build records the value used for every open number, and every rule must hold over the numbers the build used. A rule that is false over those numbers, or that cannot be calculated, makes the report invalid.

Which numbers to name#

Before you name a number, ask: would the prose need to talk about this number? Name it when a rule, a test or a sentence needs to point at it. If no player could tell two values apart, such as a menu fade of 0.30 or 0.31 seconds, leave the number out. The builder chooses it.

A shared gameplay number belongs here. A number that belongs to one item in a list stays with that item. A number used only by one test stays with the test.

A count is a number that only says how many of something there are. A count can stay in your prose when nothing else in the package needs it, as in "a run lasts three rounds". When something else needs that count, such as a rule, a test or a personalization answer, store it under a name, such as run.round_count in tuning.json, and cite the name in the sentence.

Do not start a key with a word OpenGDD already uses, such as runtime, clocks, palette or collections, and do not name any part of a key json or md. The validator reports a reserved word if you use one. The OpenGDD specification lists them all.

Full rules: OpenGDD specification, §4.

← Names and addresses · All chapters · Acceptance tests →