js-dojo

6 getPlayerById over a [[Prototype]] chain

GOAL

Capstone: write a roster lookup that resolves ids the way the language itself resolves properties — closest level first, follow the hidden link upward, stop at the first match, hand back null when there is nothing left to climb — by walking the chain by hand with Object.getPrototypeOf.

CONCEPT

A read like `scope.players` only ever lands on the CLOSEST `players` array (shadowing!). Arrays never merge across levels — to search every roster you have to iterate: current object → Object.getPrototypeOf → … → null, and at each stop inspect that level's OWN array (Object.hasOwn) so no roster is scanned twice.

HINT

for (let o = scope; o !== null; o = Object.getPrototypeOf(o)) { … }

MIRRORS

Registries that cascade in layers — a match's own roster, the party the match belongs to, the campaign that owns every party; plugin hooks that fall back to app-level then framework-level handlers; DI containers with parent scopes. Every one of them resolves an id with exactly the semantics of a property read.

Run

node 06-get-player-by-id-over-a-chain.cjs

Source

'use strict';
// ---------------------------------------------------------------------
// PROTO-DOJO · Module 1 / Exercise 6 — getPlayerById over a
// [[Prototype]] chain
//
// GOAL:    Capstone: write a roster lookup that resolves ids the way
//          the language itself resolves properties — closest level
//          first, follow the hidden link upward, stop at the first
//          match, hand back null when there is nothing left to climb —
//          by walking the chain by hand with Object.getPrototypeOf.
// CONCEPT: A read like `scope.players` only ever lands on the CLOSEST
//          `players` array (shadowing!). Arrays never merge across
//          levels — to search every roster you have to iterate: current
//          object → Object.getPrototypeOf → … → null, and at each stop
//          inspect that level's OWN array (Object.hasOwn) so no roster
//          is scanned twice.
// HINT: for (let o = scope; o !== null; o = Object.getPrototypeOf(o)) {
//       … }
// MIRRORS: Registries that cascade in layers — a match's own roster,
//          the party the match belongs to, the campaign that owns every
//          party; plugin hooks that fall back to app-level then
//          framework-level handlers; DI containers with parent scopes.
//          Every one of them resolves an id with exactly the semantics
//          of a property read.
//
// Run: node 06-get-player-by-id-over-a-chain.cjs
// ---------------------------------------------------------------------
const assert = require('node:assert');

const campaignScope = {
  scopeName: 'campaign',
  players: [
    { id: 'mage', name: 'Mage' },
    { id: 'knight', name: 'Knight — campaign default build' },
  ],
};

const partyScope = Object.create(campaignScope);
partyScope.scopeName = 'party';
partyScope.players = [
  { id: 'knight', name: 'Knight — party build' }, // shadows the campaign one
  { id: 'rogue', name: 'Rogue' },
];

const gameScope = Object.create(partyScope);
gameScope.scopeName = 'match-42';
gameScope.players = [{ id: 'knight-dummy', name: 'Knight training dummy' }];

// -- TODO 1 -----------------------------------------------------------
// How many objects does gameScope's chain hold — gameScope included —
// before
// the hidden link bottoms out at null? Remember the object that every plain
// object literal quietly chains to. Replace '???' with your count.
const objectsOnChain = '???';

// -- TODO 2 -----------------------------------------------------------
// resolvePlayer has to reproduce property lookup by hand. At the moment it
// consults ONE roster — whichever array the single `scope.players` READ
// lands
// on — so any player registered a level higher is invisible. Walk the
// chain:
// at every level search that level's OWN `players` array (Object.hasOwn
// — a
// bare `scope.players` at each step would keep landing on the same closest
// array), return the first match, and return null once the chain is used up.
function resolvePlayer(scope, id) {
  return scope.players.find((p) => p.id === id) ?? null;
}

// -- checks -----------------------------------------------------------
let walked = 0;
for (let o = gameScope; o !== null; o = Object.getPrototypeOf(o)) walked += 1;
assert.strictEqual(
  objectsOnChain,
  walked,
  'TODO 1: gameScope → partyScope → campaignScope → …one more object you never created… → null'
);

assert.strictEqual(resolvePlayer(gameScope, 'knight-dummy').name, 'Knight training dummy',
  'own scope: the closest roster is searched first');
assert.ok(
  resolvePlayer(gameScope, 'rogue'),
  "TODO 2: rogue is registered on the PARTY's own roster — one hidden link above the match. Walk up."
);
assert.strictEqual(resolvePlayer(gameScope, 'rogue').name, 'Rogue');
assert.strictEqual(
  resolvePlayer(gameScope, 'knight').name,
  'Knight — party build',
  'shadowing: both party and campaign register a knight — the CLOSEST one must win, exactly like property reads'
);
assert.strictEqual(resolvePlayer(gameScope, 'mage').name, 'Mage',
  'falls through two links to the campaign roster');
assert.strictEqual(resolvePlayer(gameScope, 'paladin'), null,
  'exhausted chain → null, no throw (mind the level that has no own players array at all)');
assert.strictEqual(resolvePlayer(campaignScope, 'rogue'), null,
  'lookup only ever walks UP the chain, never down to children');

console.log('PASS — 06-get-player-by-id-over-a-chain: you re-implemented property resolution');

Solution