1

I'm trying to display a blackjack table using NodeJS and Canvas. I'm drawing the images of all players and all the cards. The problem is that the image of the last card is not rendered, but the code does execute at that point and the coordinates of the card are correct.

The code does:

  1. Sets all the constants needed for it to position images.
  2. For loop on the array which contains all the players.
  3. Draw their image and their respective cards.

I can't seem to find the problem. The code is:

await int.deferReply();

    interface Player {
      pic: string;
      hand: string[];
      pos?: { x: number; y: number };
    }
    
    const joinTable = (user: User): Player => ({
      pic: user.displayAvatarURL({ extension: "png" }),
      hand: []
    });
    
    const joined = [
      joinTable(int.user),
      joinTable(int.user),
      joinTable(int.user),
      joinTable(int.user),
      joinTable(int.user)
    ];
    const [W, H] = [1920, 1080];
    const [DEALER_Y, PLAYER_Y] = [H * 0.1, H * 0.75];
    const PLAYER_Y_ODD = PLAYER_Y - 300;
    const [HALF_W, OFFSET_X] = [W * 0.5, 64];
    const [CARD_DIMS, CARD_OFFSET_X] = [[52, 82] as [number, number], 32];
    const PORTIONS = W / (joined.length + 1);
    const canvas = createCanvas(W, H);
    const ctx = canvas.getContext("2d");
    const { img, cards } = PATHS;
    
    const drawCard = async (
      p: Player,
      deck: string[],
      x: number,
      y: number,
      n: number = 1
    ) => {
      for (let i = 0; i < n; i++) {
        let c = client.draw(deck, p.hand);
        p.hand.push(c);
        let cSprite = await loadImage(cards + c + ".png");
        ctx.drawImage(cSprite, x, y, ...CARD_DIMS);
        //console.log(`${c}: (${x}, ${y})`);
      }
    };
    
    let deck = client.deck();
    let bg = await loadImage(img + "green.jpg");
    let dProPic = await loadImage(
      client.user.displayAvatarURL({ extension: "png" })
    );
    ctx.drawImage(bg, 0, 0);
    ctx.drawImage(dProPic, HALF_W - OFFSET_X, DEALER_Y);
    
    for (let i = 0; i < joined.length; i++) {
      let plPic = await loadImage(joined[i].pic);
      let [x, y] = [
        PORTIONS * (i + 1) - OFFSET_X,
        i % 2 === 0 && joined.length > 2 ? PLAYER_Y_ODD : PLAYER_Y
      ];
      joined[i].pos = { x, y };
      ctx.drawImage(plPic, x, y);
      drawCard(
        joined[i],
        deck,
        Math.floor(x - OFFSET_X - 32 * joined[i].hand.length),
        y - 96
      );
    }
    
    let atc = new AttachmentBuilder(canvas.createPNGStream());
    await int.editReply({ files: [atc] });

example output:

If I decrease / increase the amount of players the problem persists. If I set 1 player the card is not rendered.

EDIT
Here's some additional infos:

The PATHS:

const BASE_PATH = __dirname + "/../";
export const PATHS = {
  img: join(BASE_PATH, "../assets/img/"),
  cards: join(BASE_PATH, "../assets/cards/"),
};

The client is a discord.js client, and int is a a discord.js command interaction, which is sent after a user sends a slash command in a discord channel where the bot is present. The int.user is the user assigned to an interaction that they fired, example: if I use a slash command, the int.user is an object that has all the properties of my discord user, like the id, the profile pic, the username etc.
I added a few methods on the client such as deck():

public deck() {
    let n = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K"];
    let s = ["C", "H", "D", "S"];
    let [ln, ls] = [n.length, s.length];
    let deck: string[] = [];
    for (let i = 0; i < ls; i++) {
      for (let j = 0; j < ln; j++) {
        deck.push(n[j] + s[i]);
      }
    }
    return deck;
  }

Which creates a 52 card deck.

That aside, I tried to move the code which was indise the drawCard function directly inside the for-loop, and that works! But i don't know why and I am left with hard to read and repetitive code.

Saverio
  • 13
  • 3
  • Is it as simple as the missing card is the last thing to be drawn and `let atc = new Att...` runs before `ctx.drawImage(cSprite...` finishes? – Sam Dean Mar 03 '23 at 16:35
  • Nevermind https://stackoverflow.com/questions/51021972/does-canvasrenderingcontext2d-drawimage-run-asynchronously – Sam Dean Mar 03 '23 at 16:36
  • Might help to share more of the surrounding code. The 'img' and 'cards' paths. The `client.deck()` context. What client? `int.user`? Assuming a discord user? What is `int`? – Sam Dean Mar 03 '23 at 17:04
  • Done I edited my question, thank you for taking your time seeing this and don't hesitate to respond if you need additional informations. – Saverio Mar 05 '23 at 14:31

1 Answers1

0

let atc = new AttachmentBuilder(canvas.createPNGStream()); is running before the final card is drawn.

await int.deferReply(); The await means that no other code in this scope will run until it's finished.

The same applies to let cSprite = await loadImage(cards + c + ".png"); No other code within drawCard() will run until that image loads. However this doesn't stop code running outside of drawCard() because it's an async function.

So let atc = new AttachmentBuilder(canvas.createPNGStream()); runs before the final card is drawn and then no changes to the canvas affect the PNG attachment.

A quick fix would be to put an await on drawCard() when you call it. e.g.

await drawCard(
  joined[i],
  deck,
  Math.floor(x - OFFSET_X - 32 * joined[i].hand.length),
  y - 96
);

However this is very inefficient as it would load/draw all cards sequentially.

What we should do is use Promise.all().

When you call drawCard() it returns a promise. We can put all of these promises into an array and then once they're all complete we can continue.

Simplified example for your final for loop

const drawCardPromises = []
for (let i = 0; i < joined.length; i++) {
  // other code
  drawCardPromises.push(drawCard(
    joined[i],
    deck,
    x,
    y
  ));
}

Promise.all(drawCardPromises).then(() => {
  let atc = new AttachmentBuilder(canvas.createPNGStream());
  await int.editReply({ files: [atc] });
}

This will mean each players hand is drawn asynchronously.

You could also move the plPic drawing inside drawCard() (and then rename the function to drawHand() as it's drawing 2 cards and a player picture). Then you can also use Promise.all() within drawCard() to load the cards (and player pic) asynchronously.

Sam Dean
  • 433
  • 7
  • 22