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!
This commit is contained in:
parent
5472ccebef
commit
ed5b62e161
6 changed files with 128 additions and 23 deletions
|
@ -311,7 +311,8 @@ class Item < ApplicationRecord
|
||||||
elsif compatible_body_ids.size == 0
|
elsif compatible_body_ids.size == 0
|
||||||
# If somehow we have this item, but not any modeling data for it (weird!),
|
# 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.
|
# 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
|
else
|
||||||
# First, find our compatible pet types, then pair each body ID with its
|
# First, find our compatible pet types, then pair each body ID with its
|
||||||
# color. (As an optimization, we omit standard colors, other than the
|
# 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.
|
compatible_color_ids_by_body_id.values.
|
||||||
any? { |v| v.include?("basic") && (v & modelable_color_ids).empty? }
|
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 =
|
predicted_pet_types =
|
||||||
(basic_is_modelable ? PetType.basic : PetType.none).
|
(basic_is_modelable ? PetType.basic : PetType.none).
|
||||||
or(PetType.where(color_id: modelable_color_ids))
|
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
|
predicted_pet_types.distinct.pluck(:body_id).sort
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -409,6 +417,12 @@ class Item < ApplicationRecord
|
||||||
compatible_body_ids.size.to_f / predicted_body_ids.size
|
compatible_body_ids.size.to_f / predicted_body_ids.size
|
||||||
end
|
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={})
|
def as_json(options={})
|
||||||
super({
|
super({
|
||||||
only: [:id, :name, :description, :thumbnail_url, :rarity_index],
|
only: [:id, :name, :description, :thumbnail_url, :rarity_index],
|
||||||
|
|
|
@ -26,6 +26,10 @@ class PetType < ApplicationRecord
|
||||||
merge(Species.order(name: :asc)).
|
merge(Species.order(name: :asc)).
|
||||||
merge(Color.order(basic: :desc, standard: :desc, 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)
|
def self.random_basic_per_species(species_ids)
|
||||||
random_pet_types = []
|
random_pet_types = []
|
||||||
|
|
12
lib/tasks/items.rake
Normal file
12
lib/tasks/items.rake
Normal file
|
@ -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
|
18
spec/fixtures/items.yml
vendored
18
spec/fixtures/items.yml
vendored
|
@ -10,5 +10,21 @@ straw_hat:
|
||||||
rarity_index: 90
|
rarity_index: 90
|
||||||
price: 376
|
price: 376
|
||||||
weight_lbs: 1
|
weight_lbs: 1
|
||||||
zones_restrict: 0000000000000000000000000001000000001010000000000000
|
zones_restrict: "0000000000000000000000000001000000001010000000000000"
|
||||||
species_support_ids: "35"
|
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"
|
||||||
|
|
9
spec/fixtures/species.yml
vendored
9
spec/fixtures/species.yml
vendored
|
@ -7,9 +7,12 @@ blumaroo:
|
||||||
chia:
|
chia:
|
||||||
id: 7
|
id: 7
|
||||||
name: chia
|
name: chia
|
||||||
mynci:
|
|
||||||
id: 35
|
|
||||||
name: mynci
|
|
||||||
jetsam:
|
jetsam:
|
||||||
id: 20
|
id: 20
|
||||||
name: jetsam
|
name: jetsam
|
||||||
|
mynci:
|
||||||
|
id: 35
|
||||||
|
name: mynci
|
||||||
|
vandagyre:
|
||||||
|
id: 55
|
||||||
|
name: vandagyre
|
||||||
|
|
|
@ -10,14 +10,21 @@ RSpec.describe Item do
|
||||||
#
|
#
|
||||||
# We create some basic color pet types, and some Maraquan pet types—and,
|
# 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.
|
# 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
|
before do
|
||||||
PetType.destroy_all # Make sure no leftovers from e.g. PetType's spec!
|
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(:blue), species(:acara), body_id: 1).save!
|
||||||
build_pt(colors(:red), 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(:blue), species(:blumaroo), body_id: 2).save!
|
||||||
build_pt(colors(:green), species(:chia), body_id: 3).save!
|
build_pt(colors(:green), species(:chia), body_id: 3).save!
|
||||||
build_pt(colors(:red), species(:mynci), body_id: 4).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(:acara), body_id: 11).save!
|
||||||
build_pt(colors(:maraquan), species(:blumaroo), body_id: 12).save!
|
build_pt(colors(:maraquan), species(:blumaroo), body_id: 12).save!
|
||||||
|
@ -26,7 +33,7 @@ RSpec.describe Item do
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_pt(color, species, body_id:)
|
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
|
end
|
||||||
|
|
||||||
def build_item_asset(zone, body_id:)
|
def build_item_asset(zone, body_id:)
|
||||||
|
@ -60,19 +67,20 @@ RSpec.describe Item do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "an item without any modeling data" do
|
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_behaves_like "a not-fully-modeled item"
|
||||||
it("has no compatible body IDs") do
|
it("has no compatible body IDs") do
|
||||||
expect(item.compatible_body_ids).to be_empty
|
expect(item.compatible_body_ids).to be_empty
|
||||||
end
|
end
|
||||||
it("predicts all standard bodies are compatible") do
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "an item with one species modeled" do
|
describe "an item with one species modeled" do
|
||||||
subject(:item) { items(:straw_hat) }
|
subject(:item) { items(:birthday_bg) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 1)
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 1)
|
||||||
|
@ -85,7 +93,7 @@ RSpec.describe Item do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "an item with two species modeled" do
|
describe "an item with two species modeled" do
|
||||||
subject(:item) { items(:straw_hat) }
|
subject(:item) { items(:birthday_bg) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 1)
|
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)
|
expect(item.compatible_body_ids).to contain_exactly(1, 2)
|
||||||
end
|
end
|
||||||
it("predicts remaining standard bodies are compatible") do
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "an item with all standard species modeled" do
|
describe "an item with all standard species modeled" do
|
||||||
subject(:item) { items(:straw_hat) }
|
subject(:item) { items(:birthday_bg) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 1)
|
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: 2)
|
||||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 3)
|
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: 4)
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 5)
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like "a fully-modeled item"
|
it_behaves_like "a fully-modeled item"
|
||||||
it("is compatible with all standard body IDs") do
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "an item that fits all pets the same" do
|
describe "an item that fits all pets the same" do
|
||||||
subject(:item) { items(:straw_hat) }
|
subject(:item) { items(:birthday_bg) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
item.swf_assets << build_item_asset(zones(:background), body_id: 0)
|
item.swf_assets << build_item_asset(zones(:background), body_id: 0)
|
||||||
|
@ -131,7 +140,7 @@ RSpec.describe Item do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "an item with one Maraquan pet modeled" do
|
describe "an item with one Maraquan pet modeled" do
|
||||||
subject(:item) { items(:straw_hat) }
|
subject(:item) { items(:birthday_bg) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 11)
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 11)
|
||||||
|
@ -144,7 +153,7 @@ RSpec.describe Item do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "an item with two Maraquan pets modeled" do
|
describe "an item with two Maraquan pets modeled" do
|
||||||
subject(:item) { items(:straw_hat) }
|
subject(:item) { items(:birthday_bg) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 11)
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 11)
|
||||||
|
@ -161,7 +170,7 @@ RSpec.describe Item do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "an item with all Maraquan species modeled" do
|
describe "an item with all Maraquan species modeled" do
|
||||||
subject(:item) { items(:straw_hat) }
|
subject(:item) { items(:birthday_bg) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 11)
|
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)
|
expect(item.compatible_body_ids).to contain_exactly(11, 12, 13, 4)
|
||||||
end
|
end
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue