Friday, April 7, 2023

Coding Challenge #44 2-Dimensional Inventory

 /*
Briefcase Inventory
Logic demo of 2d spatial inventory (vector-based).
I think. might be abusing those words.
Same kind of inventory famously used in Resident Evil 4 and Cabal Online.
The user must manage the inventory slots efficiently by rotating and moving the items.
The inventory is defined as an X-by-Y box with X*Y slots (asserting that X>1&&Y>1).
The user can pick up an item from the case, hold an item above the case, and place the held item down (which would swap it with a single item below it).
There are some assumptions for this script's use case; they are noted in comments throughout the script. (Search "assum")
*/

// rough draft & pseudocode:
/*inv manager re4
    basically the item works with dots
    on A press:
        if each dot this item takes in this rotation == null
            place item
        else if all dots belong to 1 item
            swap item for the 1 item under it
        else (dots belong to more than 1 object)
            error sound, can't place held item
board []
board[x].itemIndex
if board[ heldItem.dots[xint].x + heldItem.offsetX + ((heldItem.dots[xint].y + heldItem.offsetY) * briefcaseWidth) ].itemIndex == 0
    # where itemIndex of 0 signifies a vacant slot in the briefcase
    changed my mind; -1 is empty, max is 127 (s8)
*/

// we make the assumption that an item that can be held and attempted to be placed in the briefcase is actually small enough to fit into an empty briefcase (example if our game briefcase is 4x4, we don't have any 5x4 items, which would be too long to fit into the briefcase even when empty)

function item(dots){
    this.rotation = 0; // current rotation. 0=upright.1=90*clockwise.2=180*(upside down).3=270*.
    this.dots = []; // array of indices from coords (x,y)
    this.width = 0;
    this.height = 0;
    this.stringBlueprint = "";
    this.fromString = function(s){
        this.stringBlueprint = s;
        var d = [];
        var line = 0; // which row (y-axis)
        var width = s.indexOf('\n'); // we assume the string is properly structured
        this.width = width;
        var xprog = 0; // which column (x-axis)
        if(width == -1) width = s.length;
        for(var xint = 0;xint < s.length;xint++){
            if(s.charAt(xint) == 'o'){
                d.push(xprog + (line * width));
                xprog++;
            }else if(s.charAt(xint) == '\n'){
                line++;
                xprog = 0;
            }
        }
        this.height = line + 1;
        this.dots = d;
    }
    this.toString = function(){ // dots to string, really just returned stored string
        if(this.rotation == 0) return this.stringBlueprint;
        var block = [];
        for(var xint = 0, len = this.width * this.height;xint < len;xint++) block[xint] = "x";
        for(var xint = 0;xint < this.dots.length;xint++) block[this.dots[xint]] = "o";
        var out = "";
        for(var yint = 0;yint < this.height;yint++){
            for(var xint = 0;xint < this.width;xint++) out += block[xint + (this.width * yint)];
            out += "\n";
        }
        return out;
    }
    this.rotateClockwise = function(){
        this.rotation++;
        if(this.rotation >= 4) this.rotation = 0;
        var oldWidth = this.width;
        var oldHeight = this.height;
        var newWidth = oldHeight;
        var newHeight = oldWidth;
        var res = [];
        for(var xint = 0;xint < this.dots.length;xint++){
            //console.log("roclo[" + xint + "]: " + this.dots[xint]);
            var oldx = this.dots[xint] % oldWidth;
            var oldy = Math.floor(this.dots[xint] / oldWidth);
            var newy = oldx;
            var newx = (newHeight-1) - oldy;
            res.push(newx + (newy * newHeight)); // save translated dot as index
            //console.log("old x/y ",oldx,",",oldy," -> new x/y ",newx,",",newy,". logged idx: " + res[res.length-1]);
        }
        this.dots = res;
        this.width = newWidth;
        this.height = newHeight;
    }
    this.rotateCounterClockwise = function(){
        this.rotation--;
        if(this.rotation < 0) this.rotation = 3;
        var oldWidth = this.width;
        var oldHeight = this.height;
        var newWidth = oldHeight;
        var newHeight = oldWidth;
        var res = [];
        for(var xint = 0;xint < this.dots.length;xint++){
            console.log("roclo[" + xint + "]: " + this.dots[xint]);
            var oldx = this.dots[xint] % oldWidth;
            var oldy = Math.floor(this.dots[xint] / oldWidth);
            var newx = oldy;
            var newy = (newWidth-1) - oldx;
            res.push(newx + (newy * newHeight)); // save translated dot as index
            //console.log("old x/y ",oldx,",",oldy," -> new x/y ",newx,",",newy,". logged idx: " + res[res.length-1]);
        }
        this.dots = res;
        this.width = newWidth;
        this.height = newHeight;
    }
    if(typeof dots == "string"){ // construct from string
        this.fromString(dots);
    }
}
function makeBriefcase(width, height){
    briefcase = [];
    briefcaseWidth = width;
    briefcaseHeight = height;
    for(var xint = 0;xint < width;xint++){
        for(var yint=0;yint<height;yint++) briefcase.push(-1);
    }
    briefcaseLen = (width * height) - 1; // assumes width & height are not both 1
}
function makeItem(name, dots){ // add an item to the shop
    shopNames.push(name);
    shop.push(new item(dots));
    console.log("\n=====\nshop got new stock: " + name + "\n" + dots + "\n=====");
}
function ctoi(x,y){ // coordinates to index
    return x + (y * briefcaseHeight);
}
function cloneItem(original){
    var copy = new item();
    copy.rotation = original.rotation;
    copy.width = original.width;
    copy.height = original.height;
    copy.dots = [];
    for(var xint = 0, len = original.dots.length;xint < len;xint++) copy.dots.push(original.dots[xint]);
    copy.stringBlueprint = original.stringBlueprint;
    return copy;
    // i guess coulda also just used below instead of all above (but above is probably a little faster than additionally parsing the string):
    // return new item(original.stringBlueprint);
}

var briefcase = []; // where our owned items are stored. manage this space. array of s8's. represents indices of the inventory.
var briefcaseWidth = 4;
var briefcaseHeight = 4;
var briefcaseLen = 15; // max index + 1
var shop = []; // items to select to add to inv. also acts as a database for item definitions here
var shopNames = []; // item names
var inventory = []; for(var xint=0;xint<8;xint++)inventory[xint]=-1;// a list of our owned items. indices of shop; list of item IDs. -1 = empty inventory slot. needs to be static length for briefcase values.
var holdingItem = false;
var itemInHands = null;
function tryToPlace(heldItem, x, y){
    // we assume that the item will not be out of bounds over the briefcase
    // for example, an item of width 3 cannot be placed at index 1 in a briefcase of width 3, because the right-most dot would be outside the suitcase, causing an exception here
    var matchedAny = false;
    var matchedID = -1;
    for(var xint = 0, len = heldItem.dots.length;xint < len;xint++){
        var slot = briefcase[ctoi(x,y) + heldItem.dots[xint]]; // an underlying slot(dot) in the briefcase
        if(slot != -1){ // overlapped non-empty slot
            if(matchedAny){
                if(matchedID == slot) continue; // it's ok, same item we already matched
                else{
                    // 2nd item that our held item is overlapping
                    // we can't place the item because we can't be holding both briefcase items - we can only hold 1 item at a time
                    // can play an ERRRRH sound here :)
                    return;
                }
            }else{
                // first tim overlapping, note the ID of the item in the briefcase that is overlapping (we can only carry 1 item in our hands)
                matchedAny = true;
                matchedID = slot;
            }
        }
    }
    
    // check there is space in the inventory to store the item we are holding
    // this may be unnecessary, as in most cases there would be a number of inventory slots equal to the 'dots'
    // e.g. a 4x4 case has 4*4 slots (16), so 16 1x1 items could be fit into it
    // therefore this check is a formality
    var intoInvSlot = -1;
    if(!matchedAny){ // only need to check if we're not swapping for an item in inv
        var found = false;
        for(var xint = 0;xint < inventory.length;xint++){
            if(inventory[xint] == -1){
                found = true;
                intoInvSlot = xint;
                break;
            }
        }
        if(!found){
            // can play error sound here and show message "Max number of items in briefcase reached!"
            // This is the case where there is an arbitrary limit of inv items regardless of case slots
            // e.g. 4x4 case size (16 slots) but player can only have 3 items in the case at any time, regardless of item size
            return;
        }
    }
    
    // save the inventory item from briefcase in case we need to swap it
    var itemFromBriefcase;
    if(matchedAny){
        // is there space in the inventory to hold the briefcase item?
        //itemFromBriefcase = inventory.splice(id,1)[0]; // move item from inventory (briefcase) to temporary spot
        itemFromBriefcase = inventory[id];
        intoInvSlot = id;
    }
    
    // let's now set the held item down
    inventory[intoInvSlot] = itemInHands;
    // update briefcase dots where the previously held item was set down so that they point to this inventory slot
    for(var xint = 0;xint < itemInHands.dots.length;xint++){
        // we have to change the dot value: y for the dot (with width of the item) is not necessarily the same as y for the briefcase, due to a difference in width.
        // example without change: pistol into 4x4 briefcase. briefcase is then [0,0,0,-1,.....]
        // example WITH change: pistol into 4x4 briefcase. briefcase becomes [0,0,-1,-1,0,-1,-1,-1....]
        // might help to make illustration
        /*
        no change, using raw value from heldItem.dots[xint]
        ooox
        xxxx
        (the width of the item is 2)
        (the briefcase is 4x2)
        See the issue? The item doesn't wrap because the width doesn't match the briefcase
        
        fixed width to match briefcase
        ooxx
        oxxx
        (width of item: 2)
        (briefcase is 4x2)
        Notice the difference? The handle of the pistol (bottom 'o') is now properly wrapped to the below line like it's supposed to be, because it used the offset of the briefcase's width for each line.
        */
        // so here are the
        var ix = heldItem.dots[xint] % heldItem.width;
        var iy = Math.floor(heldItem.dots[xint] / heldItem.width);
        briefcase[ctoi(x,y) + ctoi(ix,iy)] = intoInvSlot; // payload being equivalent to ctoi(x+ix,y+iy)
        // briefcase[ctoi(x,y) + heldItem.dots[xint]] = intoInvSlot; // original, erroneous line that didn't respect briefcase width
    }
    
    if(matchedAny){
        // we have to replace the held item with the item we overlapped
        itemInhands = itemFromBriefcase; // now we're holding the item we replaced
    }
    else holdingItem = false; // we set it down into empty slots. we're not holding anything now. (don't need to nullify itemInHands)
}
function pickUp(x, y){ // not holding item, pick up item in briefcase at coords
    var id = briefcase[ctoi(x,y)];
    // -1 means empty briefcase slot
    if(id >= 0){
        holdingItem = true;
        itemInHands = shop[inventory[id]];
        // remove item from briefcase, we are now holding it
        for(var xint = 0;xint < briefcaseLen;xint++){
            if(briefcase[xint] == id) briefcase[xint] = -1;
        }
    }
}
function trashHeldItem(){
    holdingItem = false;
}
function buyItem(shopIndex){ // convenient function to copy a shop item into itemInHands
    if(holdingItem) trashHeldItem();
    itemInHands = cloneItem(shop[shopIndex]);
    holdingItem = true;
}
function pressSpace(){ // function that acts as the button to pick up / place items in briefcase.
    if(holdingItem) tryToPlace(itemInHands);
    else pickUp(x,y);
}
function pressX(){ // used to trash a held item
    if(holdingItem) trashHeldItem();
}

makeBriefcase(4,4); // briefcase has 16 slots: width of 4, height of 4
makeItem("pistol", "oo\nox"); // L shape
makeItem("RPG", "ooooo\nxoxxx"); // vertical bar with a handle on 2nd column of 2nd row

shop[0].toString();
shop[0].rotateClockwise();
shop[0].rotateClockwise();
shop[0].rotateCounterClockwise();
console.log("now 90* clockwise");
shop[0].toString();

itemInHands = cloneItem(shop[0]);
console.log("in hands: ");
itemInHands.toString();

console.log('placing... @ 0,0 in briefcase');
tryToPlace(cloneItem(itemInHands), 0,0);
console.log("Briefcase now: ", briefcase);

No comments:

Post a Comment

Coding Challenge #54 C++ int to std::string (no stringstream or to_string())

Gets a string from an integer (ejemplo gratis: 123 -> "123") Wanted to come up with my own function for this like 10 years ago ...