0

I'm trying to create a browser game. The game state is stored in an object with multiple layers.

let State = {
    points: 0,
    health: 50,
    currentLocation: {x: 5, y: 55},
    inventory: { bread: 8, water: 33, money: 20 }
    abilities:
        charisma:5,
        perseverance: 3,
        speed: 8
    }
    buildings {
        bakery: { location: {x: 23, y: 41}, unlocked: 1, visited: 1},
        homestead: { location: {x: 3, y: 59}, unlocked: 0, visited: 0},
        forge: { location: {x: 56, y: 11}, unlocked: 1, visited: 0}
    }
    
}

I want to be able to control the game logic based on the current values of State.

Some cases are very simple if(State.health == 0) { Game.die(); }

Most cases are much more complex

if(State.buildings.bakery.unlocked == 1) {

    // calculate player's distance to bakery
    let dX = Math.abs(State.buildings.bakery.location.x - State.currentLocation.x)
    let dY = Math.abs(State.buildings.bakery.location.y - State.currentLocation.y)
    let d = DX + dY;
    
    if(State.inventory.bread == 0 && d < State.inventory.abilities.speed) {
        Game.die();
    }
    
}

What is the best way to achieve something like this? Looping over all the conditions seems to be a needless use of resources. I've looked into getters, setters and Proxy but don't really know what I'm doing! Ideally I'd only want to only check the logic when a relevant part of State changes.

Byron
  • 1
  • 1
  • Both getters/setters or Proxy would be the way to go. If you have a problem with such implementation, please reduce your example to just the one property you have a problem with, and include your implementation, and where it goes wrong. – trincot Jul 19 '20 at 18:18

2 Answers2

0

I'd only want to only check the logic when a relevant part of State changes.

Then use a getter and setter like this:

let State = {
  buildings: {
    bakery: {
      _unlocked: false,
      get unlocked() {
        return this._unlocked;
      },
      set unlocked(value) {
        this._unlocked = value;
        if (value == true) {
          console.log("Calculating player's distance to bakery ...");
        }
      }
    }
  }
}

State.buildings.bakery.unlocked = true;
// Calculating player's distance to bakery ...

The use of the _unlocked variable is because if you didn't, the first time unlocked is accessed, it will trigger its own getter recursively until you get a stack overflow error.

GirkovArpa
  • 4,427
  • 4
  • 14
  • 43
  • This doesn't solve the problem. To know `if(State.buildings.bakery.value1 + State.buildings.forge.value2 > 50)` I'd have to call it from both setters. I want to put my conditions in a single location in the code `let State = { buildings: { bakery: { _value1: 10, get value1() { return this._value1; }, set value1(value) { this._value1 = value; } }, forge: { _value2: 20, get value2() { return this._value2; }, set value2(value) { this._value2 = value; } } } }` – Byron Jul 19 '20 at 19:07
  • Yes, you will need to check the condition inside the getters of both `value1` and `value2`. Write the check as a single function, then call the check function inside both getters, so that you only have to define the condition check once. – GirkovArpa Jul 19 '20 at 19:15
0

Here's what I've ended up doing...

const state = {

    _data:{}, // object in which the data is actually stored

    get:(path) => { // safely get the value or return undefined 
        return path.split('.').reduce((o,i)=>(o||{})[i], state._data)
    },
    
    set:(fullpath,value,silent=0) => { // safely set the value
        
        function cycle(obj,path,value) {
            let i = path[0]
            
            if(path.length==1) { // if this is the end of the path
                obj[i] = value; // change the value
                if(!silent) { Pubsub.publish('state.'+fullpath, value) ; } // and publish the event
            } else { // if this is not the end of the the path
                if(typeof obj[i] !== 'object') { obj[i] = {} } // create this part of the path if it doesn't exist
                cycle(obj[i],path.slice(1), value) // move on to the next part of the path
            }
        }
        
        cycle(state._data,fullpath.split('.'),value)
            

    }

Step 1: Get and set

I created two custom functions to get and set the state.

get() takes a dot notation path as a string, e.g. state.get('buildings.bakery.unlocked').

set() also takes a dot notation path as a string as well as the value, e.g. state.set('buildings.bakery.unlocked', 1).

I used some code from this thread. Using these functions means it's easy to manipulate nested properties without worrying about the dreaded TypeError.

Step 2: PubSub

The dot notation path also feeds a publish/subscribe model. I'm using PubSubJS. Calling set() also publishes an event that matches the path, e.g. state.set('abilities.strength', 5) also publishes 'state.abilities.strength'.

To monitor state changes, you simply subscribe to the relevant events:

PubSub.subscribe('state.abilities.strength', () => console.log('strength value changed'))

The published event also passes back the new value, so you can do what you want with it:

PubSub.subscribe('state.inventory.bread', (path, value) => console.log('bread value changed, you now have ' + value))

Step 3: Implement to compare

PubSub has the benefit of being a topic based system. This means you can subscribe to non-final nodes:

PubSub.subscribe('state.inventory', () => console.log('something in the inventory changed'))
PubSub.subscribe('state.inventory.water', () => console.log('water changed'))

Changing inventory.bread will trigger the first, changing inventory.water will trigger both.

This allows me to separates the game logic from the state. There's two ways to compare values...

    // function to compare two values
    let comparison = function () {

        console.log('running comparison');

        if(state.get('test.valueA') > state.get('test.valueB')) {
            console.log('A is bigger than B');
        }
    }

    // you either subscribe to both the values, and compare whenever either one changes
    PubSub.subscribe('state.test.valueA', comparison)
    PubSub.subscribe('state.test.valueB', comparison)
    
    // or alternatively, if they have a shared topic, then subscribe to that
    PubSub.subscribe('state.test', comparison)
Byron
  • 1
  • 1