Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×

How to Check if a Deck has Discarded Cards via API

I'm attempting to make an API for a custom deck-based game system. I have a bunch of various functions, but I'm trying to improve them to fail more elegantly. However, I've run into an issue with my getCard() function, which takes the ID value for a deck and returns either an ID value for the card drawn or the value false if no card is drawn (essentially an improved version of the base drawCard() function). However, before returning false, it will first check the discard pile of the deck, and if cards are in the discard, it will shuffle the deck and attempt to draw again. The function is included below: const getCard = (deckId) => { //Attempt to draw a card let cardId = drawCard(deckId); if(cardId !== false){ //A card was drawn return cardId; } else { //No card is in the deck let deck = getObj("deck", deckId); let deckName = deck.get("name"); //Check that we can shuffle for more cards if(deck.get("_discardPile")===""){ //The discard is empty sendChat(deckName+" Deck", 'The deck and discard are both empty.'); return false; } else { sendChat(deckName+" Deck", 'The deck is empty. Shuffling the discard.'); shuffleDeck(deckId); //shuffle the discard without recalling let cardId = drawCard(deckId); return cardId; }; }; }; The issue I'm running into is that when using the drawCard and giveCardToPlayer functions (even the base functions without my additions), sometimes card IDs are being added to the deck's _discardPile even when the cards are still in players' hands or on the board. Thus, the script thinks there's a discard to shuffle, even though there isn't. I thought about grabbing all the IDs for cards in players' hands and all the IDs for cards on the board, then comparing them to the _discardPile list to verify whether any of the _discardPile cards are actually in the discard, but that seems like a lot of overhead. Before spending time working on that, I wanted to ping the community to see if there were any more elegant ways to handle this.
1611278335
The Aaron
Roll20 Production Team
API Scripter
So, first off, that's definitely a bug.  I'll try to make a repro script and pass it on to the devs. (or if you can whip up a quick one that that creates the issue, so much the better!) Second, your approach is exactly what I would probably suggest.  You could write a little system that observes change:hand and change:deck and keeps things up to date for later querying.
Thanks for the reply, and it's great to get a reply from you in particular, Aaron, as your posts have been a huge help in my own work with APIs! In case you haven't already done this, here's a quick demo script. Steps to reproduce: Create a new game. Load the below script into the API. Launch the game. Show the default Playing Cards deck. Shuffle the Playing Cards deck.  Issue the following chat command: !draw Playing Cards Issue the command again. Observe that the card drawn in Step 6 is now in the _discardPile. on("chat:message", function(msg) { if (msg.type !== "api") { return; } let args = msg.content.split(/\s(.+)/); switch(args[0]) { case '!draw':{ if(args.length <= 1){ log('You must include a deck argument (e.g. !draw Playing Cards) to use this command.'); } else { let deckName = args[1]; //For the given deck name, find matching decks const decks = findObjs({_type: "deck", name: deckName}, {caseInsensitive: true}); if(decks.length === 0){ //No matches for deck name log('The '+deckName+' deck was not found.'); } else { //Draw a card from each deck matching name for(const deck of decks){ //Draw a card from the deck let cardId = drawCard(deck.id); if(cardId !== false){ //Card found, give to player giveCardToPlayer(cardId, msg.playerid); log(msg.who+' drew '+getObj("card",cardId).get("name")+' from '+deckName+'.'); } else { //No cards found log(msg.who+' failed to draw a card from the '+deckName+'deck.'); }; //Troubleshooting Discard Issues let discardCardOutput = deckName+" Discard: "; let discardIds = deck.get("_discardPile"); log(discardIds); if(discardIds != ""){ let discardIdArray = discardIds.split(','); for(discardId of discardIdArray){ discardCardOutput += getObj("card",discardId).get("name")+", "; }; }; log(discardCardOutput); }; }; }; break; } } }); Here's the output from running this a few times (each "Tester (GM) drew" line is from a new chat event): "Tester (GM) drew Five of Diamonds from Playing Cards." "Playing Cards Discard: " "Tester (GM) drew Nine of Diamonds from Playing Cards." "Playing Cards Discard: Five of Diamonds, " "Tester (GM) drew Eight of Hearts from Playing Cards." "Playing Cards Discard: Five of Diamonds, Nine of Diamonds, " "Tester (GM) drew Jack of Hearts from Playing Cards." "Playing Cards Discard: Five of Diamonds, Nine of Diamonds, Eight of Hearts, " "Tester (GM) drew Nine of Clubs from Playing Cards." "Playing Cards Discard: Five of Diamonds, Nine of Diamonds, Eight of Hearts, Jack of Hearts, " On the question I originally asked, I hadn't thought about using the change:hand  and change:deck  events to monitor and update the cards that have been drawn. That definitely seems more efficient than parsing through every card in a hand or on the table whenever a new card is drawn. I'll definitely look into that. Thanks for the suggestion!
1611322883
The Aaron
Roll20 Production Team
API Scripter
Ah, that's great, thanks for the reproduction script, I'll pass that onward! Glad I could be of help, both in the past and now. =D
1611325933

Edited 1611342609
The Aaron
Roll20 Production Team
API Scripter
I modified your repro script a bit into this: on('ready',()=>{ const getCardName = (()=>{ let lookup = {}; return (cardid) => { if(!lookup.hasOwnProperty(cardid)){ let c = getObj('card',cardid); if(c){ lookup[cardid] = c.get('name'); } else { lookup[cardid] = '[UNKNOWN]'; } } return lookup[cardid]; }; })(); on("chat:message", (msg) => { if (msg.type === "api" && /^!draw(\b\s|$)/i.test(msg.content)) { let who = (getObj('player',msg.playerid)||{get:()=>'API'}).get('_displayname'); const deck = findObjs({_type: "deck", name: "Playing Cards"}, {caseInsensitive: true})[0]; let args = msg.content.split(/\s+/); let num = parseInt(args[1])||1; let drawn = []; let drawfail = 0; let discardedBefore = deck.get("_discardPile").split(/,/).filter(s=>s.length); let handBefore = (findObjs({type:'hand', parentid: msg.playerid})[0]||{get:()=>''}).get('currentHand').split(/,/).filter(s=>s.length); //Draw a card from the deck for(let i = 0; i<num; ++i){ let cardId =drawCard(deck.id); if(cardId !== false){ //Card found, give to player giveCardToPlayer(cardId, msg.playerid); drawn.push(cardId); } else { ++drawfail; } } let hand = (findObjs({type:'hand', parentid: msg.playerid})[0]||{get:()=>''}).get('currentHand').split(/,/).filter(s=>s.length); let discarded = deck.get("_discardPile").split(/,/).filter(s=>s.length); const clear=()=>`<div style="clear:both;">`; const cardRows = (ids) => ids.map(getCardName).map(n=>`<li>${n}</li>`).join(''); const showDrawn = ()=>`<div><h3>Drawn Cards</h3><ul>${cardRows(drawn)}${ (drawfail >0) ? `<li><b>Failed Attempts: </b><code>${drawfail}</code></li>` : ''}</ul></div>`; const cardColumn = (title,ids) => `<div style="float:left;margin-right:2em;"><h5>${title}</h5><ul>${cardRows(ids)}</ul></div>`; const beforeAfter = (title,before,after,later) => `<div><h3>${title}</h3><div>${cardColumn('Before',before)}${cardColumn('After',after)}${(later) ? cardColumn('Later (+100ms)', later):''}${clear()}</div>`; setTimeout(()=>{ let discardedAfter = deck.get("_discardPile").split(/,/).filter(s=>s.length); sendChat('Deck Discard Repro',`/w "${who}" <div>${showDrawn()}${beforeAfter('Hand',handBefore,hand)}${beforeAfter('Discard',discardedBefore,discarded,discardedAfter)}`); },100); } }); }); I changed it to always draw from the playing cards deck, since that's always available, and I changed the argument to the !draw command to specify how many cards to draw.  It looks like however many you draw always get added to the discard, but at some weird later point: I'm sure this will really help with tracking down the issue, thanks again for doing the legwork! Edit: I adjusted it a bit further, it seems that the discard happens about 100ms after the script does it's work.
I think you and I have different definitions of "a bit." That looks considerably nicer! I know you enjoy talking about this stuff, so I was wondering if you could verify that I'm properly understanding the getCardName declaration at the top and how it's used (I'm still pretty new to both IFFE's and maps).  const getCardName = (()=>{ let lookup = {}; return (cardid) => { if(!lookup.hasOwnProperty(cardid)){ let c = getObj('card',cardid); if(c){ lookup[cardid] = c.get('name'); } else { lookup[cardid] = c.get('<UNKNOWN>'); } } return lookup[cardid]; }; })(); I understand that this is an IFFE, and that essentially you're declaring a variable lookup , which is an empty JSON object ( let lookup = {}; ). Then you return a function that will interact with this lookup object ( return (cardid) => {   function  }; ), and since the lookup object is declared within the closure, it will persist beyond interaction with the function (presumably to store the key-value pairs). The function that interacts with the object takes a cardId input, and then it checks to see if that id is already a key within the lookup  object ( if(!lookup.hasOwnProperty(cardid)) ). If it is not already in the object, the function proceeds to grab the card object with that id ( let c = getObj('card',cardid); ), and if that card object exists ( if(c){ ), creates a key-value mapping within the lookup  object between the card's id and the card's name ( lookup[cardid] = c.get('name'); ). If the card object does not exist (that is if we're in the false section of the if statement)...well, actually this is where I'm a little lost, since you're calling the .get method on something that is undefined, which should cause an error, right? Then, regardless of whether the id was already in the lookup  object or was put there during this call of the function, you return the card name associated with the id. This is later used in one place within the script:  const cardRows = (ids) => ids.map(getCardName).map(n=>`<li>${n}</li>`).join(''); . Here you're declaring a function cardRows that takes an array of IDs as input. It then puts each id through the getCardName function to get the card name associated with that id, and then it puts those names through another map to insert them into the html for a list item. It then concatenates the values in the resultant array, giving you a list of card names corresponding to the original array. So I think I have most of that correct, other than the confusion on that one part regarding the if statement's false block. However, if I'm off at any point, please let me know, and thanks for taking a look into this!
1611342362

Edited 1611342597
The Aaron
Roll20 Production Team
API Scripter
Ah yes, c.get('<UNKNOWN>') is actually a bug.  It should be just '<UNKNOWN>'.  =D  And really, '[UNKNOWN]' is probably better so it doesn't have to be escaped to output. The rest of your evaluation is completely correct. =D Were I using this in a full fledged script, I might add in some on('change:card') event handlers in the closure to update the lookup on name changes.  Basically, I'm just caching the name so I don't have to keep looking it up. const getCardName = (()=>{ let lookup = {}; on('change:card:name',(card)=>lookup[card.id]=card.get('name')); return (cardid) => { if(!lookup.hasOwnProperty(cardid)){ let c = getObj('card',cardid); if(c){ lookup[cardid] = c.get('name'); } else { lookup[cardid] = '[UNKNOWN]'; } } return lookup[cardid]; }; })();
1612472200

Edited 1612473286
I noticed more anomalous behavior when trying to do more work on this project. Specifically, I was using change, add, and destroy events to try to track when cards entered and left play, so that I could compare a list of IDs in play to the deck's discard pile to determine whether there are actually any cards in the discard. However, I observed that the giveCardToPlayer(), recallCards(), and shuffleDeck() calls do not seem to trigger  change:hand  or  change:deck  events. I'm pretty new to change events, so maybe this is expected behavior, but in case not, here's a script that can be used for reproduction: on('ready',()=>{ const getCardName = (()=>{ let lookup = {}; return (cardid) => { if(!lookup.hasOwnProperty(cardid)){ let c = getObj('card',cardid); if(c){ lookup[cardid] = c.get('name'); } else { lookup[cardid] = '[UNKNOWN]'; } } return lookup[cardid]; }; })(); on("chat:message", (msg) => { const [cmd, args] = msg.content.split(/\s(.+)/); log({cmd}); log({args}); const deck = findObjs({_type: "deck", name: "Playing Cards"}, {caseInsensitive: true})[0]; switch (cmd.toLowerCase()) { case '!draw': { let num = parseInt(args)||1; let drawn = []; let drawfail = 0; for(let i = 0; i<num; ++i){ let cardId =drawCard(deck.id); if(cardId !== false){ //Card found, give to player giveCardToPlayer(cardId, msg.playerid); drawn.push(cardId); } else { ++drawfail; } } const cardRows = (ids) => ids.map(getCardName).join(', '); if(drawn.length > 0) { log(`${msg.who} drew ${cardRows(drawn)} and failed to draw ${drawfail} cards.`); } else { log(`${msg.who} failed to draw ${drawfail} cards.`); } break; } case '!shuffle': { recallCards(deck.id); shuffleDeck(deck.id); log(`${msg.who} recalled and shuffled the cards for ${deck.get("name")}.`); break; } } }); on("change:hand", (hand) => { let player = getObj("player", hand.get("_parentid")); log(`A change event occurred for ${player.get("_displayname")}'s hand.`); }); on("change:deck", (deck) => { log(`A change event occurred for the ${deck.get("name")} deck.`); }); on("change:card", (card) => { let cardId = card.get("_cardid"); log(`A change event occurred for the ${getCardName(cardId)} card.`); }); on("add:card", (card) => { let cardId = card.get("_cardid"); log(`${getCardName(cardId)} was added to the tabletop.`); }); on("destroy:card", (card) => { let cardId = card.get("_cardid"); log(`${getCardName(cardId)} was removed from the tabletop.`); }); }); I followed the below steps to test this. I'm having issues getting the console log to post as a screenshot, so I'm just listing the steps along with the line number from the console log that should appear in the reproduction: Create a new game. Add the script to the game. Open the game. Go to Collections and click "Show" for the Playing Cards deck. This triggered a  change:deck  event [line 4]. Click the "Shuffle" button on the newly displayed deck. This triggered a  change:deck  event [line 5]. Drag a card from the shown deck to the player icon to add it to the player's hand. This triggered a  change:deck  event [line 6] but no  change:hand  event, presumably because the hand didn't exist at this point. Drag another card from the shown deck to the player icon. This time a  change:hand  event [line 7] and a  change:deck  event [line 8] are triggered. Issue the  !draw 2  command. This draws two cards [lines 9-11] but does not trigger any events. I dragged a card from the deck to the tabletop. This triggered a  change:deck  event [line 12], an  add:card  event [line 13], and another  change:deck  event [line 14]. I haven't looked into why two  change:deck  events are triggered by this behavior, though I suspect it may have to do with interacting with the deck after an API call was made against it. I then moused over the shown deck, selected the "Recall" option, checked the "Shuffle after recalling?" checkbox, and then clicked the "Recall All" button. This triggered four  change:hand  events (one per card) [lines 15-18], a  destroy:card  event [line 19], and a  change:deck  event [line 20]. I dragged a card from the deck to the tabletop. This triggered only two events (unlike in Step 1 above): a  change:deck  event [line 21] and  add:card  event [line 22]. I issued the  !draw 4  command. This drew the cards but did not trigger any events, as expected from the previous test case [lines 23-25]. I issued the  !shuffle  command. While I expect to see the same events as in Step 2, instead I see the logged command lines [lines 26-27, 29] and a single  destroy:card  event [line 28]. Given that Steps 6 and 7 triggered change:deck  events while Step 8 did not, and given that Step 7 triggered a change:hand  event while Step 8 did not, it seems that giveCardToPlayer() isn't triggering events correctly. Similarly, given that Step 10 triggered change:hand  and change:deck  events while Step 13 did not, it seems that recallCards() and shuffleDeck() are not triggering events correctly. This may be intended behavior to avoid infinite loops in the API, but I wasn't sure and figured I'd check with the community.
1612473445
The Aaron
Roll20 Production Team
API Scripter
I passed this along to the devs.
Thanks, Aaron! I presume that means that normally API functions trigger events when changing things? I think the infinite loop scenario may be good justification to avoid it, but when using a mixture of manual and scripted actions to interact with decks, it makes it a little harder to track when something happens.
1612474436
The Aaron
Roll20 Production Team
API Scripter
"it depends" is my best answer. =D  sendChat() certainly triggers events.  There are very few functions that modify the state of the game, rather than directly modifying an object.  I would expect these functions to cause the events, but you may be right about the circular notification (though that's a handleable situation).