Fix economy data not properly being used to price trades, resulting in default prices being applied at most towns.

Also add an exporter from econsim results to sqlite to aid in debugging the economy (which revealed this bug).
This commit is contained in:
Avi Weinstock 2021-05-28 14:42:29 -04:00
parent 2771c29d54
commit 3684cf0454
9 changed files with 215 additions and 20 deletions

View File

@ -141,6 +141,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Mindflayer AI now correctly summons husks at certain HP thresholds.
- Far away NPCs respond to being damaged by a projectile
- Fixed terrain clipping with glider
- Fixed an issue where prices weren't properly making their way from econsim to the actual trade values.
## [0.9.0] - 2021-03-20

3
Cargo.lock generated
View File

@ -5900,6 +5900,7 @@ dependencies = [
"bincode",
"bitvec",
"criterion",
"csv",
"deflate 0.9.1",
"enum-iterator",
"flate2",
@ -5919,8 +5920,10 @@ dependencies = [
"rand_chacha 0.3.0",
"rayon",
"ron",
"rusqlite",
"serde",
"structopt",
"strum",
"svg_fmt",
"tracing",
"tracing-subscriber",

View File

@ -41,6 +41,6 @@ good_scaling: [
(Food, 0.1), // common.items.food.mushroom
(Coin, 1.0), // common.items.utility.coins
(Armor, 0.05), // common.items.armor.misc.pants.worker_blue
(Tools, 0.05), // common.items.weapons.staff.starter_staff
(Ingredients, 0.25), // common.items.crafting_ing.leather_scraps
(Tools, 0.10), // common.items.weapons.staff.starter_staff
(Ingredients, 0.15), // common.items.crafting_ing.leather_scraps
])

View File

@ -551,10 +551,12 @@ impl LoadoutBuilder {
.expect("coins should be stackable");
*s = Some(coin_item);
coins = 0;
} else if let Some(item_id) =
TradePricing::random_item(Good::Armor, armor, true)
{
*s = Some(Item::new_from_asset_expect(&item_id));
} else if armor > 0.0 {
if let Some(item_id) =
TradePricing::random_item(Good::Armor, armor, true)
{
*s = Some(Item::new_from_asset_expect(&item_id));
}
}
}
let mut bag1 = Item::new_from_asset_expect(
@ -566,10 +568,13 @@ impl LoadoutBuilder {
.copied()
.unwrap_or_default()
/ 10.0;
for i in bag1.slots_mut() {
if let Some(item_id) = TradePricing::random_item(Good::Tools, weapon, true)
{
*i = Some(Item::new_from_asset_expect(&item_id));
if weapon > 0.0 {
for i in bag1.slots_mut() {
if let Some(item_id) =
TradePricing::random_item(Good::Tools, weapon, true)
{
*i = Some(Item::new_from_asset_expect(&item_id));
}
}
}
let mut rng = rand::thread_rng();

View File

@ -1,6 +1,7 @@
use serde::{Deserialize, Serialize};
use strum_macros::EnumIter;
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, EnumIter)]
pub enum BiomeKind {
Void,
Lake,
@ -13,3 +14,7 @@ pub enum BiomeKind {
Jungle,
Forest,
}
impl Default for BiomeKind {
fn default() -> BiomeKind { BiomeKind::Void }
}

View File

@ -5,6 +5,7 @@ use crate::{
};
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use strum_macros::EnumIter;
use tracing::{trace, warn};
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
@ -296,7 +297,7 @@ impl Default for Trades {
// we need this declaration in common for Merchant loadout creation, it is not
// directly related to trade between entities, but between sites (more abstract)
// economical information
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize, EnumIter)]
pub enum Good {
Territory(BiomeKind),
Flour,

View File

@ -7,7 +7,7 @@ edition = "2018"
[features]
tracy = ["common/tracy", "common-net/tracy"]
simd = ["vek/platform_intrinsics"]
bin_compression = ["lz-fear", "deflate", "flate2", "common-frontend", "image/jpeg", "num-traits"]
bin_compression = ["lz-fear", "deflate", "flate2", "image/jpeg", "num-traits"]
default = ["simd"]
@ -43,15 +43,18 @@ lz-fear = { version = "0.1.1", optional = true }
deflate = { version = "0.9.1", optional = true }
flate2 = { version = "1.0.20", optional = true }
num-traits = { version = "0.2", optional = true }
common-frontend = { package = "veloren-common-frontend", path = "../common/frontend", optional = true }
[dev-dependencies]
common-frontend = { package = "veloren-common-frontend", path = "../common/frontend" }
criterion = "0.3"
csv = "1.1.3"
tracing-subscriber = { version = "0.2.15", default-features = false, features = ["fmt", "chrono", "ansi", "smallvec", "env-filter"] }
minifb = "0.19.1"
rusqlite = { version = "0.24.2", features = ["array", "vtab", "bundled", "trace"] }
svg_fmt = "0.4"
structopt = "0.3"
strum = "0.20"
[[bench]]
harness = false

View File

@ -0,0 +1,177 @@
use common::{
terrain::BiomeKind,
trade::{Good, SitePrices},
};
use rayon::ThreadPoolBuilder;
use rusqlite::{Connection, ToSql};
use std::error::Error;
use strum::IntoEnumIterator;
use vek::Vec2;
use veloren_world::{
index::Index,
sim::{FileOpts, WorldOpts, DEFAULT_WORLD_MAP},
World,
};
fn good_pricing_csv(world: &World, index: &Index) -> Result<(), Box<dyn Error>> {
let mut csv = csv::Writer::from_path("good_pricing.csv")?;
csv.write_record(&[
"Site",
"XCoord",
"YCoord",
"Flour",
"Meat",
"Transportation",
"Food",
"Wood",
"Stone",
"Tools",
"Armor",
"Ingredients",
"Potions",
"Coin",
"RoadSecurity",
])?;
for civsite in world.civs().sites() {
if let Some(site_id) = civsite.site_tmp {
let site = index.sites.get(site_id);
if site.do_economic_simulation() {
let prices = site.economy.get_site_prices();
//println!("{:?}: {:?} {:?}", site.name(), civsite.center, prices);
csv.write_record(&[
site.name(),
&format!("{}", civsite.center.x),
&format!("{}", civsite.center.y),
&format!("{}", prices.values.get(&Good::Flour).unwrap_or(&0.0)),
&format!("{}", prices.values.get(&Good::Meat).unwrap_or(&0.0)),
&format!(
"{}",
prices.values.get(&Good::Transportation).unwrap_or(&0.0)
),
&format!("{}", prices.values.get(&Good::Food).unwrap_or(&0.0)),
&format!("{}", prices.values.get(&Good::Wood).unwrap_or(&0.0)),
&format!("{}", prices.values.get(&Good::Stone).unwrap_or(&0.0)),
&format!("{}", prices.values.get(&Good::Tools).unwrap_or(&0.0)),
&format!("{}", prices.values.get(&Good::Armor).unwrap_or(&0.0)),
&format!("{}", prices.values.get(&Good::Ingredients).unwrap_or(&0.0)),
&format!("{}", prices.values.get(&Good::Potions).unwrap_or(&0.0)),
&format!("{}", prices.values.get(&Good::Coin).unwrap_or(&0.0)),
&format!("{}", prices.values.get(&Good::RoadSecurity).unwrap_or(&0.0)),
])?;
}
}
}
Ok(())
}
fn economy_sqlite(world: &World, index: &Index) -> Result<(), Box<dyn Error>> {
let conn = Connection::open("economy.sqlite")?;
#[rustfmt::skip]
conn.execute_batch("
CREATE TABLE IF NOT EXISTS site (
xcoord INTEGER NOT NULL,
ycoord INTEGER NUT NULL,
name TEXT NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS site_position ON site(xcoord, ycoord);
CREATE TABLE IF NOT EXISTS site_price (
xcoord INTEGER NOT NULL,
ycoord INTEGER NOT NULL,
good TEXT NOT NULL,
price REAL NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS site_good on site_price(xcoord, ycoord, good);
")?;
let mut all_goods = Vec::new();
for good in Good::iter() {
match good {
Good::Territory(_) => {
for biome in BiomeKind::iter() {
all_goods.push(Good::Territory(biome));
}
},
Good::Terrain(_) => {
for biome in BiomeKind::iter() {
all_goods.push(Good::Terrain(biome));
}
},
_ => {
all_goods.push(good);
},
}
}
let mut good_columns = String::new();
let mut good_exprs = String::new();
for good in all_goods.iter() {
good_columns += &format!(", '{:?}'", good);
good_exprs += &format!(
", MAX(CASE WHEN site_price.good = '{:?}' THEN site_price.price END)",
good
);
}
#[rustfmt::skip]
let create_view = format!("
CREATE VIEW IF NOT EXISTS site_price_tr (xcoord, ycoord {})
AS SELECT xcoord, ycoord {}
FROM site NATURAL JOIN site_price
GROUP BY xcoord, ycoord
", good_columns, good_exprs);
conn.execute_batch(&create_view)?;
let mut insert_price_stmt = conn
.prepare("REPLACE INTO site_price (xcoord, ycoord, good, price) VALUES (?1, ?2, ?3, ?4)")?;
let mut insert_price = move |center: Vec2<i32>, good: Good, prices: &SitePrices| {
let price = prices.values.get(&good).unwrap_or(&0.0);
insert_price_stmt.execute(&[
&center.x as &dyn ToSql,
&center.y,
&format!("{:?}", good),
&(*price as f64),
])
};
for civsite in world.civs().sites() {
if let Some(site_id) = civsite.site_tmp {
let site = index.sites.get(site_id);
if site.do_economic_simulation() {
let prices = site.economy.get_site_prices();
conn.execute(
"REPLACE INTO site (xcoord, ycoord, name) VALUES (?1, ?2, ?3)",
&[
&civsite.center.x as &dyn ToSql,
&civsite.center.y,
&site.name(),
],
)?;
for good in all_goods.iter() {
insert_price(civsite.center, *good, &prices)?;
}
}
}
}
Ok(())
}
fn main() {
common_frontend::init_stdout(None);
println!("Loading world");
let pool = ThreadPoolBuilder::new().build().unwrap();
let (world, index) = World::generate(
59686,
WorldOpts {
seed_elements: true,
world_file: FileOpts::LoadAsset(DEFAULT_WORLD_MAP.into()),
},
&pool,
);
println!("Loaded world");
if let Err(e) = good_pricing_csv(&world, &index) {
println!("Error generating goodpricing csv: {:?}", e);
}
if let Err(e) = economy_sqlite(&world, &index) {
println!("Error generating economy sqlite db: {:?}", e);
}
}

View File

@ -339,12 +339,12 @@ impl Economy {
values: {
let labor_values = normalize(self.labor_values.clone());
// Use labor values as prices. Not correct (doesn't care about exchange value)
let prices = normalize(self.values.clone())
.map(|good, value| Some((labor_values[good]? + value?) * 0.5));
prices
.iter()
.map(|(g, v)| (g, v.unwrap_or(Economy::MINIMUM_PRICE)))
.collect()
let prices = normalize(self.values.clone()).map(|good, value| {
(labor_values[good].unwrap_or(Economy::MINIMUM_PRICE)
+ value.unwrap_or(Economy::MINIMUM_PRICE))
* 0.5
});
prices.iter().map(|(g, v)| (g, *v)).collect()
},
}
}