Error accessing property 'novice' of undefined



  • Which shard is affected?
    It happened on S3 - not sure if it affects others.

    What happened?
    My claimer creep ran into an error. Its memory had an array of roomNames. It would go to the first in the list, .attackController if it was owned, .claimController otherwise, then shift it out of the array and proceed. After going through the array, it would try to go home and recycle. Full code is at the bottom.

    After numerous succesful .attackController lapses, the owner (same for all six rooms) respawned. Immediately afterwards the problems appeared. At least, based on the replay. I noticed only after it had been going on for a while.

    The creep would move up to the controller, but claimController gave the error: TypeError: Cannot read property 'novice' of undefined

    During troubleshooting I disabled the claimController line with a comment, which stopped the errors. I then manually tried claiming the controller with a console command:
    let a = Game.getObjectById("..."); Game.creeps.downgrader.claimController(a)

    The used id was copy-pasted and the creep was adjacent to the controller, as you can see on the screenshot below.

    The console command returned an error, again as seen on the screenshot: TypeError: Cannot read property 'novice' of undefined and continued with runtime details.

    What should have happened?
    The controller should have been claimed successfully.

    How can we reproduce this?
    Honestly I'm not sure. The same thing happened with the controller 1 room down south, E31S5, but I didn't look into it then because I assumed it was an error on my part and I decided against claiming that room instead of troubleshooting. It also happened in E32S4 judging by the replay, yet it claimed fine later with the same code.E33S4 claimed fine afterwards as well, again same code.

    There is about

    Replay link:
    E31S4 - from the screenshot The replay doesn't really show much, but the creep should have 'booped' the controller and then move on east. Interestingly this happens much later than the successful claims from below.

    E32S4 - broken
    E32S4 - successful claim intent at 12643142, roughly 1800 ticks after the broken attempt.

    E33S4 - successful claim intent at 12643192

    I did not check behavior along the rest of the route.

    Piece of code which shows the bug in action: Screenshot

    Claimer memory shoved into spawnCreep(body, "downgrader", { memory }):

        // const memory = { downgradeNames: ["E31S5", "E31S4", "E32S4", "E33S4", "E33S5", "E32S5"] }
    

    Full creep code: (please don't judge, this was shoved together on-demand).

    function runDowngrader(creep) {
        const visualizePathStyle = {
            fill: 'transparent',
            stroke: '#fff',
            lineStyle: 'dashed',
            strokeWidth: .15,
            opacity: .2
        }
        const opts = { range:1, reusePath: 50, ignoreRoads: true, visualizePathStyle };
        
        // Load array of target roomnames from memory
        let targetRoom = creep.memory.downgradeNames[0]; 
        let target;
        
        
        // Load controller object if vision on target room
        if (Game.rooms[targetRoom]) {
            target = Game.rooms[targetRoom].controller;
        }
        // Load controller position if not visible
        else {
            const flatPos = Memory.rooms[targetRoom].controller.pos;
            target = new RoomPosition(flatPos.x, flatPos.y, targetRoom);
        }
        
        
        // Proceed if target is truthy (Controller | RoomPosition)
        if(target) {
            creep.moveTo(target, opts)
            
            // Attack controller
            const result = creep.attackController(target)
            // console.log(Game.time + " - " + result)
            if (result === OK) {
                creep.memory.downgradeNames.shift() // Remove roomName from memory array
                console.log(creep + " succesfully attacked " + target + ", " + target.ticksToDowngrade + " remain before losing RCL " + target.level)
                
                // Move to new target
                targetRoom = creep.memory.downgradeNames[0];
                const flatPos = Memory.rooms[targetRoom].controller.pos;
                target = new RoomPosition(flatPos.x, flatPos.y, targetRoom)
            }
            // Claim if need be
            else if (creep.room.name = targetRoom && target.level === 0 && creep.pos.isNearTo(target)) {
                console.log(target) // These are all the `[structure (controller)]` logs from the screenshot. Commented this line eventually. 
                const res = creep.claimController(target); // Commented this out during troubleshooting
                // console.log(res)
            }
            
            // Activate observer for pathfinding
            const obs = Game.spawns.Spawn1.pos.findClosestByRange(FIND_MY_STRUCTURES, {filter: (s) => s.structureType === STRUCTURE_OBSERVER})
            
            if (obs && targetRoom) {
                obs.observeRoom(targetRoom)
            }
        }
        // Recycle if all targets are visited
        else if (!target) {
            target = Game.spawns.Spawn3; // HARDCODE. The horror!
            // creep.moveTo(target, opts)
            
            if(creep.pos.isNearTo(target)) {
                target.recycleCreep(creep)
            }
        }
    }


  • Can you repro the error and print out this.room.name for the claiming creep?


  • Dev Team

    @keenathar In your shoes, I'd check all game code, most probably you have an incorrect comparison somewhere (.room.name = ... instead of .room.name == ...)

    UPD: Right, here:

        // Claim if need be
        else if (creep.room.name = targetRoom &&


  • @o4kapuk Well that's embarrassing. The second I code something without a linter I make such a rookie mistake.

    However, this doesn't explain why the oneliner in the game's Console failed as well. At that point I had commented the line you point out, and there is no such mistake in the console command - it's literally just grabbing the object and setting the intent. The error messes with the conditions for the else if-statement, but in the cases of that replay it would also be true with proper comparison. While the sign is wrong, it shouldn't have actually changed anything in the offending replays (as the creep was in the proper room as held in the variable targetRoom).

    More specifically, the creep is moving to the proper room from from:

    // Proceed if target is truthy (Controller | RoomPosition)
    if(target) {
        creep.moveTo(target, opts)
    

    result will be -7 ERR_INVALID_TARGET if the creep isn't in the right room, or -9 ERR_NOT_IN_RANGE if too far away in the proper room:

    // Attack controller
    const result = creep.attackController(target)
    

    So this branch would not be entered

       // console.log(Game.time + " - " + result)
        if (result === OK) { ... }
    

    Bringing us to the offending line. Even with the = mistake, the error occurred while the creep was adjacent to the controller. That means the = is essentially doing A becomes A, as they're already the same.

     // Claim if need be
     else if (creep.room.name = targetRoom && target.level === 0 && creep.pos.isNearTo(target)) { ... }
    

    How would that cause the issue regarding property novice? The first condition is true from the single =, but target must still be the controller because otherwise target.level === 0 would already throw cannot access property 'level' of undefined - which did not happen.

    I'm trying to understand, but don't see how the single = could cause the observed error.

    Edit: I tried it, first with the incorrect = and then with the fixed ===, and it did solve the issue. However I'm still curious how it makes a difference, just for my own understanding. Especially because if the creep is adjacent to the controller, they should be the same 🤔


  • Dev Team

    However I'm still curious how it makes a difference, just for my own understanding.

    To understand this, you should output your targetRoom. Basically you're changing the value of creep.room.name in your code to the value of targetRoom so it doesn't match the real state of the world anymore. Most of our API code is not intended to work with objects modified like this which cause weird exceptions or unexpected behavior here and there which are extremely hard to debug.

    This specific case was because you changed creep.room.name to the name of a room that you don't have a vision to. This broke one of the checks in Creep.claimController.