Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions lib/game.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ class Runa {
this.dungeonReturn = null
this.player = new Player()
this.field = null
this.excursion = 0
this.shop = null
this.cursor = 0
this.title = true
Expand Down Expand Up @@ -774,7 +775,10 @@ class Runa {
this.pending = null

if (location.kind === 'field') {
this.field = new Field({ script: this.scriptSource, player: this.player })
this.excursion++
const seed =
(this.excursion * 2654435761 + this.player.xp * 40503 + this.player.gold) >>> 0
this.field = new Field({ script: this.scriptSource, player: this.player, seed })
Comment on lines +778 to +781

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Quality: Seed derivation formula duplicated in two places

The identical (this.excursion * 2654435761 + this.player.xp * 40503 + this.player.gold) >>> 0 expression appears in both loadSlot() and enter(). Duplicating the magic constants risks the two paths drifting apart. Extract a small helper (e.g. nextFieldSeed() that also does this.excursion++) and call it from both sites.

Centralize the seed derivation so both field entry points stay in sync.:

nextFieldSeed() {
  this.excursion++
  return (this.excursion * 2654435761 + this.player.xp * 40503 + this.player.gold) >>> 0
}

// both call sites:
const seed = this.nextFieldSeed()
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

this.field.player.x = clampNumber(location.x, 0, this.field.width - 1)
this.field.player.y = clampNumber(location.y, 0, this.field.height - 1)
} else {
Expand Down Expand Up @@ -1177,7 +1181,10 @@ class Runa {
switch (action.kind) {
case 'travel':
if (action.to === 'field') {
this.field = new Field({ player: this.player })
this.excursion++
const seed =
(this.excursion * 2654435761 + this.player.xp * 40503 + this.player.gold) >>> 0
this.field = new Field({ player: this.player, seed })
Comment on lines +1184 to +1187

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Bug: excursion counter isn't persisted or reset on load

The excursion field is initialized to 0 in the constructor, incremented on every field entry, but never written to saveState() nor reset in loadSlot(). As a result the derived seed after loading a save depends on how many excursions happened earlier in the current session: reloading the same save twice yields different fields, and two players loading an identical save file (same xp/gold) get different maps. This contradicts the PR's stated goal that "una misma partida sigue siendo reproducible." Persist excursion in saveState() and restore it in loadSlot() (and reset it in the constructor path used for new games) so the seed is a pure function of saved state.

Persist and restore the excursion counter so the field seed is reproducible from saved state.:

// in saveState() return object:
    return {
      name: this.name,
      player,
      location,
      excursion: this.excursion,
      ...
    }

// in loadSlot(), before deriving the seed:
    this.excursion = Number(saved.excursion) || 0
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

this.field.setScript(this.scriptSource)
this.say('salis de la ciudad. pulsa t para volver cuando no estes peleando')
} else if (action.to === 'dungeon') {
Expand Down
34 changes: 34 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1098,6 +1098,40 @@ test('hitbox combat stays on the field and advances on attack input', (t) => {
)
})

test('two excursions are never the same field (#3)', (t) => {
const game = new Runa({ presence: false })
startGame(game)

const openField = () => {
const gateTile = { x: 0, y: 0 }
// find the '>' exit and step on it via placeAt + enter
for (let y = 0; y < MAPS.city.rows.length; y++) {
const x = MAPS.city.rows[y].indexOf('>')
if (x !== -1) {
gateTile.x = x
gateTile.y = y
break
}
}
game.walker.placeAt('city', gateTile.x, gateTile.y)
press(game, 'e')
}

openField()
t.ok(game.field, 'first excursion opens the field')
const first = game.field.seed

press(game, 't')
t.absent(game.field, 't closes the excursion')
game.player.gold += 7

openField()
t.ok(game.field, 'second excursion opens a fresh field')
const second = game.field.seed

t.not(first, second, 'the seed differs between excursions')
})

test('t returns from the field to the city outside combat', (t) => {
const game = new Runa({ presence: false })
startGame(game)
Expand Down
Loading