From ed5b62e161868f4ccc76050c4b6fe8185d44dd9e Mon Sep 17 00:00:00 2001 From: Emi Matchu Date: Tue, 19 Nov 2024 16:41:50 -0800 Subject: [PATCH] Use PetType's created_at to predict who an item might be compatible with This is a basic attempt at the Vandagyre logic, but also things like "Maraquan items released before the Maraquan X was released"! I also added a new task, `rails items:update_cached_fields`, which needs to be run after this change, because it affects the value of `Item#predicted_fully_modeled?`. Eyeballing the updated search results for `-is:modeled`, this feels pretty close? I'm guessing it's not perfect (e.g. maybe a pet type we got modeled late into its existence, or some items that just never did fit a certain pet), but feels pretty good. I also know we had the "modeling hints" override in Impress 2020, which we aren't reading yet. We should probably take that into account here too! --- app/models/item.rb | 18 +++++++- app/models/pet_type.rb | 4 ++ lib/tasks/items.rake | 12 ++++++ spec/fixtures/items.yml | 18 +++++++- spec/fixtures/species.yml | 9 ++-- spec/models/item_spec.rb | 90 +++++++++++++++++++++++++++++++-------- 6 files changed, 128 insertions(+), 23 deletions(-) create mode 100644 lib/tasks/items.rake diff --git a/app/models/item.rb b/app/models/item.rb index e25bf6d8..49aec183 100644 --- a/app/models/item.rb +++ b/app/models/item.rb @@ -311,7 +311,8 @@ class Item < ApplicationRecord elsif compatible_body_ids.size == 0 # If somehow we have this item, but not any modeling data for it (weird!), # consider it to fit all standard pet types until shown otherwise. - PetType.basic.distinct.pluck(:body_id).sort + PetType.basic.released_before(released_at_estimate). + distinct.pluck(:body_id).sort else # First, find our compatible pet types, then pair each body ID with its # color. (As an optimization, we omit standard colors, other than the @@ -345,10 +346,17 @@ class Item < ApplicationRecord compatible_color_ids_by_body_id.values. any? { |v| v.include?("basic") && (v & modelable_color_ids).empty? } - # Get all body IDs for the colors we decided are modelable. + # Filter to pet types that match the colors that seem compatible. predicted_pet_types = (basic_is_modelable ? PetType.basic : PetType.none). or(PetType.where(color_id: modelable_color_ids)) + + # Only include species that were released when this item was. If we don't + # know our creation date (we don't have it for some old records), assume + # it's pretty old. + predicted_pet_types.merge! PetType.released_before(released_at_estimate) + + # Get all body IDs for the pet types we decided are modelable. predicted_pet_types.distinct.pluck(:body_id).sort end end @@ -409,6 +417,12 @@ class Item < ApplicationRecord compatible_body_ids.size.to_f / predicted_body_ids.size end + # We estimate the item's release time as either when we first saw it, or 2010 + # if it's so old that we don't have a record. + def released_at_estimate + created_at || Time.new(2010) + end + def as_json(options={}) super({ only: [:id, :name, :description, :thumbnail_url, :rarity_index], diff --git a/app/models/pet_type.rb b/app/models/pet_type.rb index 1ef5e884..29a5611c 100644 --- a/app/models/pet_type.rb +++ b/app/models/pet_type.rb @@ -26,6 +26,10 @@ class PetType < ApplicationRecord merge(Species.order(name: :asc)). merge(Color.order(basic: :desc, standard: :desc, name: :asc)) } + scope :released_before, ->(time) { + # We use DTI's creation timestamp as an estimate of when it was released. + where('created_at <= ?', time) + } def self.random_basic_per_species(species_ids) random_pet_types = [] diff --git a/lib/tasks/items.rake b/lib/tasks/items.rake new file mode 100644 index 00000000..6a100489 --- /dev/null +++ b/lib/tasks/items.rake @@ -0,0 +1,12 @@ +namespace :items do + desc "Update cached fields for all items (useful if logic changes)" + task :update_cached_fields => :environment do + puts "Updating cached item fields for all items…" + Item.includes(:swf_assets).find_in_batches.with_index do |items, batch| + puts "Updating item batch ##{batch+1}…" + Item.transaction do + items.each(&:update_cached_fields) + end + end + end +end diff --git a/spec/fixtures/items.yml b/spec/fixtures/items.yml index f2d3e93c..1139fe76 100644 --- a/spec/fixtures/items.yml +++ b/spec/fixtures/items.yml @@ -10,5 +10,21 @@ straw_hat: rarity_index: 90 price: 376 weight_lbs: 1 - zones_restrict: 0000000000000000000000000001000000001010000000000000 + zones_restrict: "0000000000000000000000000001000000001010000000000000" species_support_ids: "35" + created_at: "2011-03-28T14:33:36-07:00" + +birthday_bg: + id: 89876 + name: Birthday Bash Background + description: This place is all set for a brilliant birthday bash! + thumbnail_url: https://images.neopets.com/items/9a4gd6g6c0.gif + type: none + category: None + rarity: Special + rarity_index: 101 + price: 0 + weight_lbs: 1 + zones_restrict: "0000000000000000000000000000000000000000000000000000" + species_support_ids: "" + created_at: "2024-11-15T18:15:22-08:00" diff --git a/spec/fixtures/species.yml b/spec/fixtures/species.yml index 3509f3e9..36d4e9a9 100644 --- a/spec/fixtures/species.yml +++ b/spec/fixtures/species.yml @@ -7,9 +7,12 @@ blumaroo: chia: id: 7 name: chia -mynci: - id: 35 - name: mynci jetsam: id: 20 name: jetsam +mynci: + id: 35 + name: mynci +vandagyre: + id: 55 + name: vandagyre diff --git a/spec/models/item_spec.rb b/spec/models/item_spec.rb index 1eacb8b7..bc8e0b47 100644 --- a/spec/models/item_spec.rb +++ b/spec/models/item_spec.rb @@ -10,14 +10,21 @@ RSpec.describe Item do # # We create some basic color pet types, and some Maraquan pet types—and, # just like irl, the Maraquan Mynci has the same body as the basic Mynci. + # + # These pet types default to an early creation date of 2005, except the + # Vandagyre, which was released in 2014. before do PetType.destroy_all # Make sure no leftovers from e.g. PetType's spec! - build_pt(colors(:blue), species(:acara), body_id: 1).save! - build_pt(colors(:red), species(:acara), body_id: 1).save! - build_pt(colors(:blue), species(:blumaroo), body_id: 2).save! - build_pt(colors(:green), species(:chia), body_id: 3).save! - build_pt(colors(:red), species(:mynci), body_id: 4).save! + build_pt(colors(:blue), species(:acara), body_id: 1).save! + build_pt(colors(:red), species(:acara), body_id: 1).save! + build_pt(colors(:blue), species(:blumaroo), body_id: 2).save! + build_pt(colors(:green), species(:chia), body_id: 3).save! + build_pt(colors(:red), species(:mynci), body_id: 4).save! + build_pt(colors(:blue), species(:vandagyre), body_id: 5).tap do |pt| + pt.created_at = Date.new(2014, 11, 14) + pt.save! + end build_pt(colors(:maraquan), species(:acara), body_id: 11).save! build_pt(colors(:maraquan), species(:blumaroo), body_id: 12).save! @@ -26,7 +33,7 @@ RSpec.describe Item do end def build_pt(color, species, body_id:) - PetType.new(color:, species:, body_id:) + PetType.new(color:, species:, body_id:, created_at: Time.new(2005)) end def build_item_asset(zone, body_id:) @@ -60,19 +67,20 @@ RSpec.describe Item do end describe "an item without any modeling data" do - subject(:item) { items(:straw_hat) } + subject(:item) { items(:birthday_bg) } it_behaves_like "a not-fully-modeled item" it("has no compatible body IDs") do expect(item.compatible_body_ids).to be_empty end it("predicts all standard bodies are compatible") do - expect(item.predicted_missing_body_ids).to contain_exactly(1, 2, 3, 4) + expect(item.predicted_missing_body_ids).to contain_exactly( + 1, 2, 3, 4, 5) end end describe "an item with one species modeled" do - subject(:item) { items(:straw_hat) } + subject(:item) { items(:birthday_bg) } before do item.swf_assets << build_item_asset(zones(:wings), body_id: 1) @@ -85,7 +93,7 @@ RSpec.describe Item do end describe "an item with two species modeled" do - subject(:item) { items(:straw_hat) } + subject(:item) { items(:birthday_bg) } before do item.swf_assets << build_item_asset(zones(:wings), body_id: 1) @@ -97,28 +105,29 @@ RSpec.describe Item do expect(item.compatible_body_ids).to contain_exactly(1, 2) end it("predicts remaining standard bodies are compatible") do - expect(item.predicted_missing_body_ids).to contain_exactly(3, 4) + expect(item.predicted_missing_body_ids).to contain_exactly(3, 4, 5) end end describe "an item with all standard species modeled" do - subject(:item) { items(:straw_hat) } + subject(:item) { items(:birthday_bg) } before do item.swf_assets << build_item_asset(zones(:wings), body_id: 1) item.swf_assets << build_item_asset(zones(:wings), body_id: 2) item.swf_assets << build_item_asset(zones(:wings), body_id: 3) item.swf_assets << build_item_asset(zones(:wings), body_id: 4) + item.swf_assets << build_item_asset(zones(:wings), body_id: 5) end it_behaves_like "a fully-modeled item" it("is compatible with all standard body IDs") do - expect(item.compatible_body_ids).to contain_exactly(1, 2, 3, 4) + expect(item.compatible_body_ids).to contain_exactly(1, 2, 3, 4, 5) end end describe "an item that fits all pets the same" do - subject(:item) { items(:straw_hat) } + subject(:item) { items(:birthday_bg) } before do item.swf_assets << build_item_asset(zones(:background), body_id: 0) @@ -131,7 +140,7 @@ RSpec.describe Item do end describe "an item with one Maraquan pet modeled" do - subject(:item) { items(:straw_hat) } + subject(:item) { items(:birthday_bg) } before do item.swf_assets << build_item_asset(zones(:wings), body_id: 11) @@ -144,7 +153,7 @@ RSpec.describe Item do end describe "an item with two Maraquan pets modeled" do - subject(:item) { items(:straw_hat) } + subject(:item) { items(:birthday_bg) } before do item.swf_assets << build_item_asset(zones(:wings), body_id: 11) @@ -161,7 +170,7 @@ RSpec.describe Item do end describe "an item with all Maraquan species modeled" do - subject(:item) { items(:straw_hat) } + subject(:item) { items(:birthday_bg) } before do item.swf_assets << build_item_asset(zones(:wings), body_id: 11) @@ -175,5 +184,52 @@ RSpec.describe Item do expect(item.compatible_body_ids).to contain_exactly(11, 12, 13, 4) end end + + describe "a pre-Vandagyre item without any modeling data" do + subject(:item) { items(:straw_hat) } + + it_behaves_like "a not-fully-modeled item" + it("has no compatible body IDs") do + expect(item.compatible_body_ids).to be_empty + end + it("predicts all standard bodies except Vandagyre are compatible") do + expect(item.predicted_missing_body_ids).to contain_exactly(1, 2, 3, 4) + end + end + + # Skipping "pre-Vanda with one species modeled", because it's identical. + + describe "a pre-Vandagyre item with two species modeled" do + subject(:item) { items(:straw_hat) } + + before do + item.swf_assets << build_item_asset(zones(:wings), body_id: 1) + item.swf_assets << build_item_asset(zones(:wings), body_id: 2) + end + + it_behaves_like "a not-fully-modeled item" + it("has two compatible body IDs") do + expect(item.compatible_body_ids).to contain_exactly(1, 2) + end + it("predicts remaining standard bodies (sans Vandagyre) are compatible") do + expect(item.predicted_missing_body_ids).to contain_exactly(3, 4) + end + end + + describe "a pre-Vandagyre item with all other standard species modeled" do + subject(:item) { items(:straw_hat) } + + before do + item.swf_assets << build_item_asset(zones(:wings), body_id: 1) + item.swf_assets << build_item_asset(zones(:wings), body_id: 2) + item.swf_assets << build_item_asset(zones(:wings), body_id: 3) + item.swf_assets << build_item_asset(zones(:wings), body_id: 4) + end + + it_behaves_like "a fully-modeled item" + it("is compatible with all non-Vandagyre standard body IDs") do + expect(item.compatible_body_ids).to contain_exactly(1, 2, 3, 4) + end + end end end