Ask A Question

Notifications

You’re not receiving notifications from this thread.

Deck Builder App. How do I query only decks you can make with the cards in your inventory?

Matias Fernandez asked in Rails

I'm making a deck building site similar to hearthpwn.com. It would be nice if users could filter decks based off the cards they have in their inventory.

In other words, I'm trying to make a query that gets the following:

  • All Decks where DeckCard.card_ids are all in UserCard.card_ids AND where deck_card.quantity is less than or equal to the matching user_card.quanitity.

I want to have a scope in the Deck Model that does this. You can see my current implementation below. See scope :with_all_deck_cards_in_user_cards. It doesn't filter the quantities correctly and I need some help debugging it.

Models

class Card
    has_many :user_cards
    has_many :users, through: :user_cards
    has_many :deck_cards
    has_many :decks, through: :deck_cards
end

class User
    has_many :decks
    has_many :user_cards
    has_many :cards, through: :user_cards
end

class Deck
    belongs_to :user
    has_many :deck_cards
    has_many :cards, through: :deck_cards

    user_id

    scope :with_all_deck_cards_in_user_cards, lambda { |user_id|
        user_cards = User.find(user_id).user_cards
        deck_cards = DeckCard.arel_table
        decks      = Deck.arel_table

        user_cards.reduce(self) do |deck, user_card|
            deck.where(
                DeckCard \
                .where(deck_cards[:deck_id].eq(decks[:id])) \
                .where(deck_cards[:card_id].eq(user_card.card_id)) \
                .where(deck_cards[:quantity].lteq(user_card.quantity)) \
                .exists
            )
        end
    }
end

class UserCard
    belongs_to :user
    belongs_to :card

    card_id
    user_id
    quantity
end

class DeckCard
    belongs_to :deck
    belongs_to :card

    card_id
    deck_id
    quantity
end

Let me know if you need any more information.

Reply

Hey Matias,

Is there any chance you could put together a quick example you could share on github? It's kind of hard to debug without being able to play with the data itself. When you say it's not filtering the quantities correctly, what is the result you're getting and what is the expected result?

Have you tried using some of the other enumerable methods to restructure the query? I remember having some weird results when I started chaining where statements, so I think (as memory serves) I had to switch to using select in that particular scenario... so something like:

DeckCard.select{ |k| decks[:id].includes?(k.id) && deck_cards[:card_id] == user_card.card_id && deck_cards[:quantity] <= user_card.quantity }

But my memory is pretty foggy on that...

Reply

I've prepared an example on github with seed data and expected results: https://github.com/MatiasFMolinari/example_deck_builder

Requirements and expected results are in the index page.

To summarize, I need to create a scope in the Deck model that only returns the decks that that user can build. It cannot return an enumerable since I need to be able to chain it with other scopes.

Reply

Alright, well after a little tinkering I've got a working solution. It could be refactored quite a few times, but I like to first get a solution, then work on refactoring.

scope :that_user_can_create, -> (user_id) {
  user_cards = User.find(user_id).user_cards
  # no need to proceed if user doesn't have any cards
  return nil if user_cards.nil?

  deck_ids = []
  Deck.all.each do |deck|
    deck_card_status = []
    deck.deck_cards.each do |card|
      # check to see if the user has this card
      has_card = user_cards.where(card_id: card.card_id).present?
      deck_card_status << has_card
      # no need to proceed if the user doesn't have this card
      next unless has_card
      has_qty  = user_cards.where(card_id: card.card_id).first.quantity >= card.quantity
      deck_card_status << has_qty
    end

    deck_id = deck_card_status.include?(false) ? '' : deck.id
    deck_ids << deck_id
  end

  Deck.where(id: deck_ids.flatten.uniq)
}

The approach I'm taking is to compare each card in a deck to see if the user meets that cards requirements, if so, that card is flagged true in deck_card_status. deck_card_status then gets checked to see if it includes false, as long as it doesn't then you can include it in the deck_ids. Then, do the final Deck.where(id: deck_ids.flatten.uniq) to get the decks that user can make.

Reply

This works Jacob, thank you for taking the time to help me out. If you want to see it in production, take a look at YugiDecks in the coming weeks.

Reply

Just checking if this a typo when you past it.

  has_many :deck_cards, through: :deck_cards

did you mean?

`has_many :decks, through: :deck_cards`
Reply

Awesome, I'm glad it will work for you! I'll keep an eye out, look forward to seeing the completed site!

Yes, that was a typo Francisco - the repository he shared had the correct association in it. Good catch though, I never even noticed haha :)

Reply

Thanks Francisco! I corrected the typo.

Reply
Join the discussion
Create an account Log in

Want to stay up-to-date with Ruby on Rails?

Join 87,563+ developers who get early access to new tutorials, screencasts, articles, and more.

    We care about the protection of your data. Read our Privacy Policy.