Lesson 73 of 170

Make a tunable rule declarative

Martinez AI Studios Academy

Build and validate a configuration shape that separates tunable values from runtime behavior.

1062. Lesson identity

Module
3.4 — Declarative data
Lesson
2 — Make a tunable rule declarative
Academic type
Guided Build
Schema type
practical
Order
2 in the module
Estimated time
45–60 minutes

This lesson turns one adjustable gameplay rule into a small, reviewable data contract. You will define its shape, assign safe defaults, reject invalid input, reject unknown fields, distinguish omission from an explicitly supplied undefined, and identify which code remains responsible for behavior.

1063. Learning objective

After this lesson, you can define and validate one declarative data set so that valid configuration reaches the behavior system, invalid configuration is rejected with a useful reason, and omitted fields are handled differently from supplied values that have the wrong type.

1064. Why this matters

A tunable value is useful only when the game can load it consistently and a reviewer can determine what it means. Separating configuration from behavior lets you adjust a rule without rewriting its execution path. Validation protects the runtime from incomplete, mistyped, out-of-range, or misspelled data.

A precise absence policy is part of that contract. If a field is optional, its default should normally apply when the field is omitted. An explicitly supplied value still needs validation. Without an own-property check, { enabled: undefined } can be mistaken for omission and silently replaced by the default.

1065. Prior knowledge

You should be able to:

  • distinguish declarative data, behavior, and runtime state from 3.4 L1 — Data is not behavior;
  • describe the rule you want to tune in plain language;
  • read basic object and type syntax;
  • identify where the existing behavior obtains its current value.

If the existing code is unclear, trace the value from its source to the behavior that consumes it. Do not begin by moving executable code into a data file.

1066. Core concept

A declarative data set is a contract, not merely a bag of numbers. The contract answers six questions:

  1. Shape: Which fields exist, and what type does each field use?
  2. Defaults: What safe value applies when an optional field is omitted?
  3. Presence: Does an explicitly supplied undefined count as omission or as a supplied invalid value?
  4. Validity: Which values must be rejected, and why?
  5. Unknown fields: What happens when input contains a key outside the approved schema?
  6. Ownership: Which behavior reads the validated data and enforces the game rule?

For this lesson, choose one small tunable rule. A generic example is a recovery rule:

const recoveryConfig = {
  delaySeconds: 3,
  amount: 10,
  enabled: true
};

The object describes configuration. It does not perform recovery, wait for three seconds, or modify an entity. Those actions remain behavior.

A useful contract is:

type RecoveryConfig = {
  delaySeconds: number;
  amount: number;
  enabled: boolean;
};

The type describes the validated result. It does not prove that external or edited data is valid. A validation boundary must inspect the actual values before returning a RecoveryConfig.

This lesson uses two explicit policies:

  • Unknown fields are rejected.
  • A default applies only when the corresponding own property is absent. If a field is present with undefined, it is treated as a supplied value and rejected by type validation.

1067. Mental model

Use the DATA → VALIDATE → CONSUME model:

Stage Responsibility Example question
DATA Declare tunable values and their intended shape What may be adjusted?
VALIDATE Detect field presence, apply defaults for omissions, validate supplied values, and reject unknown keys Is this input complete, typed, permitted, and unambiguous?
CONSUME Execute behavior using validated values What does the running game do with this configuration?

The direction is one-way. The behavior system may read validated configuration, but configuration should not contain callbacks, control flow, or hidden runtime state.

1068. Concrete example

Suppose the behavior currently contains hard-coded values:

const delaySeconds = 3;
const amount = 10;

Move only the adjustable values into a configuration contract:

type RecoveryConfig = {
  delaySeconds: number;
  amount: number;
  enabled: boolean;
};

const defaultRecoveryConfig: RecoveryConfig = {
  delaySeconds: 3,
  amount: 10,
  enabled: true
};

The validator can reject unknown fields, distinguish omission from explicit undefined, and validate every resulting value:

function validateRecoveryConfig(
  input: Record<string, unknown>
): RecoveryConfig {
  const allowedKeys = new Set([
    "delaySeconds",
    "amount",
    "enabled"
  ]);

  for (const key of Object.keys(input)) {
    if (!allowedKeys.has(key)) {
      throw new Error(
        `Unknown recovery configuration field: ${key}`
      );
    }
  }

  const delaySeconds = Object.hasOwn(input, "delaySeconds")
    ? input.delaySeconds
    : defaultRecoveryConfig.delaySeconds;

  const amount = Object.hasOwn(input, "amount")
    ? input.amount
    : defaultRecoveryConfig.amount;

  const enabled = Object.hasOwn(input, "enabled")
    ? input.enabled
    : defaultRecoveryConfig.enabled;

  if (
    typeof delaySeconds !== "number" ||
    !Number.isFinite(delaySeconds) ||
    delaySeconds < 0
  ) {
    throw new Error(
      "delaySeconds must be a finite number greater than or equal to zero"
    );
  }

  if (
    typeof amount !== "number" ||
    !Number.isFinite(amount) ||
    amount <= 0
  ) {
    throw new Error(
      "amount must be a finite number greater than zero"
    );
  }

  if (typeof enabled !== "boolean") {
    throw new Error("enabled must be a boolean");
  }

  return { delaySeconds, amount, enabled };
}

This produces distinct outcomes:

  • { amount: 12 } is accepted. The absent fields receive their defaults.
  • { enabled: undefined } is rejected. The field is present, so its supplied value must be a boolean.
  • { amount: -5 } is rejected because the supplied value is outside the permitted range.
  • { delaySecond: 4 } is rejected because the key is not part of the schema.

Object.hasOwn is important here. A check such as input.enabled === undefined would treat both omission and an explicitly supplied undefined as the same case. That alternative is valid only if the contract deliberately defines undefined as omission and tests that policy. This lesson does not use that policy.

The validator does not decide when recovery occurs. It establishes a usable configuration. Existing behavior still decides when to start recovery and how to apply the configured amount.

1069. AI-native workflow

Use AI as an implementation assistant and review target, not as the authority for the contract.

  1. Write the rule, fields, values used for omissions, invalid cases, presence policy, and unknown-field policy in your own words.
  2. Ask the AI to propose a type and validator from that specification.
  3. Require the proposal to distinguish an absent property from a property explicitly set to undefined.
  4. Compare the proposal against the DATA → VALIDATE → CONSUME model.
  5. Reject callbacks, runtime state, undocumented clamping, executable behavior inside the data shape, or silent handling of unknown fields.
  6. Inspect the resulting change and run the available checks.
  7. Ask the AI to explain each validation branch, then verify the explanation against the code.

A useful request is: “Propose a configuration type and validator for this rule. Reject unknown fields. Apply defaults only to absent own properties, and reject explicitly supplied undefined values. Do not implement gameplay behavior. List assumptions separately.”

1070. Common mistakes

Treating defaults as validation

Merging an object with defaults can fill omitted fields, but it does not prove that supplied values have the correct type or range. { amount: -5 } still looks complete after a merge.

Testing only for undefined

A condition such as input.enabled === undefined cannot distinguish an omitted field from { enabled: undefined }. If the contract says defaults apply only to omission, test presence with Object.hasOwn and then validate the supplied value.

Passing unknown fields through

A misspelled field such as delaySecond can appear to succeed while having no effect. This lesson rejects unknown fields before constructing the validated result.

Silently clamping invalid values

Clamping can conceal an authoring error. Reject the value unless the design explicitly defines clamping as the intended rule.

1071. Guided practice

Build one validated configuration set for a tunable rule in the project or in a small isolated example.

Step 1: State the rule

Choose one rule with at least two adjustable values. Write one sentence describing what the running game does and another describing what the data may control.

Step 2: Define the contract

Document:

  • at least two typed fields;
  • a meaning and unit for every numeric field;
  • values used when optional fields are omitted;
  • validity conditions;
  • rejection of unknown fields;
  • whether explicit undefined is omission or invalid input.

For this practice, apply values only to omitted own properties and reject explicitly supplied undefined.

Step 3: Implement validation

Create a validation boundary and test at least:

  • a complete valid configuration;
  • a partial configuration that receives a value for an omitted field;
  • a wrong type;
  • an out-of-range value;
  • an unknown or misspelled field;
  • a known field explicitly supplied as undefined.

The last case must fail under this lesson's policy. Do not silently replace it with a default.

Step 4: Connect the consumer

Change the selected behavior to consume the validated configuration instead of a hard-coded adjustable value. Do not move the behavior itself into the data.

Step 5: Review the boundary

Answer:

  • Which fields may be adjusted?
  • Which omissions receive values?
  • What happens when a known field is explicitly set to undefined?
  • Which invalid and unknown inputs are rejected?
  • Which function still owns the gameplay behavior?

1072. Validation and evidence

The associated practical assessment requires:

  • the declared configuration contract;
  • documented meanings, units, and values used for omissions;
  • the validation boundary;
  • evidence for valid, omitted, wrong-type, out-of-range, unknown-field, and explicit-undefined cases;
  • a consumer that uses only validated data;
  • an inspected change showing that runtime state and executable behavior remain outside the data set.

1073. Key takeaways

  • Declarative data describes adjustable values; it does not execute the rule.
  • A type describes the intended result, while validation checks actual input.
  • Defaults address omission, not correctness.
  • Property presence and value equality are different checks.
  • Unknown fields need an explicit policy; this lesson rejects them.
  • The behavior system remains responsible for applying validated configuration.

1074. Next lesson

Continue to 3.5 — Coupling (lesson-s3-3-5-01-coupling-has-a-change-cost).

1075. Knowledge check

Answer these items for yourself before reading the answers.

What is the primary responsibility of the validation step?

  • A. Execute the gameplay rule
  • B. Store current runtime state
  • C. Apply omission defaults and reject invalid configuration
  • D. Replace the behavior system
Show answer and feedback

Answer: Apply omission defaults and reject invalid configuration

Why: Validation establishes a complete, permitted configuration. It does not execute the rule or own runtime state.

Why is merging an input object with defaults not sufficient validation?

  • A. A merge cannot fill omitted optional fields
  • B. A merge may preserve wrong types or out-of-range values
  • C. Defaults always execute behavior
  • D. A type cannot contain numeric fields
Show answer and feedback

Answer: A merge may preserve wrong types or out-of-range values

Why: Defaults address omission, not correctness. Supplied values still require type and range checks.

Which item belongs in the behavior consumer rather than in the declarative data set?

  • A. A recovery delay in seconds
  • B. A boolean that enables the rule
  • C. The configured recovery amount
  • D. The operation that waits and applies recovery
Show answer and feedback

Answer: The operation that waits and applies recovery

Why: Waiting and changing an entity are executable behavior. The delay, enabled flag, and amount are configuration values.

What should you do when an AI proposal silently clamps an invalid value, but the design does not define clamping?

  • A. Accept it because any valid output is safe
  • B. Move the clamp into the data file
  • C. Reject or revise the proposal and define the intended policy
  • D. Remove all validation
Show answer and feedback

Answer: Reject or revise the proposal and define the intended policy

Why: Silent clamping can hide an authoring or design error. The policy must be chosen deliberately.

Under this lesson's policy, how should { enabled: undefined } be handled?

  • A. Treat the field as omitted and apply true
  • B. Reject it because the property is present with a non-boolean value
  • C. Remove the field and return the remaining object
  • D. Convert undefined to false
Show answer and feedback

Answer: Reject it because the property is present with a non-boolean value

Why: Object.hasOwn(input, "enabled") identifies the property as supplied. Its value must therefore pass boolean validation instead of receiving the omission default.

Support