diff --git a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/db/CachedMerchantDAO.java b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/db/CachedMerchantDAO.java index a5bb96388..317e513f3 100644 --- a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/db/CachedMerchantDAO.java +++ b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/db/CachedMerchantDAO.java @@ -28,7 +28,7 @@ public class CachedMerchantDAO implements MerchantDAO, Cached { @Override public void addTransaction(String blueprintId, float price, Date date, TransactionType transactionType) { _delegate.addTransaction(blueprintId, price, date, transactionType); - _blueprintIdLastTransaction.put(blueprintId, new Transaction(date, price, transactionType)); + _blueprintIdLastTransaction.remove(blueprintId); } @Override diff --git a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/db/DbMerchantDAO.java b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/db/DbMerchantDAO.java index 3bd67059c..3327a0368 100644 --- a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/db/DbMerchantDAO.java +++ b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/db/DbMerchantDAO.java @@ -81,7 +81,7 @@ public class DbMerchantDAO implements MerchantDAO { try { Connection connection = _dbAccess.getDataSource().getConnection(); try { - PreparedStatement statement = connection.prepareStatement("select blueprint_id, transaction_price, transaction_date, transaction_type from merchant_data where blueprint_id=?"); + PreparedStatement statement = connection.prepareStatement("select blueprint_id, transaction_price, transaction_date, transaction_type, buy_count-sell_count from merchant_data where blueprint_id=?"); try { statement.setString(1, blueprintId); ResultSet rs = statement.executeQuery(); @@ -90,8 +90,9 @@ public class DbMerchantDAO implements MerchantDAO { float price = rs.getFloat(2); Date date = rs.getTimestamp(3); String type = rs.getString(4); + int stock = rs.getInt(5); - return new Transaction(date, price, TransactionType.valueOf(type)); + return new Transaction(date, price, TransactionType.valueOf(type), stock); } else { return null; } diff --git a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/db/MerchantDAO.java b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/db/MerchantDAO.java index 36d23b482..d38a90bfc 100644 --- a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/db/MerchantDAO.java +++ b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/db/MerchantDAO.java @@ -15,11 +15,13 @@ public interface MerchantDAO { private float _price; private Date _date; private TransactionType _transactionType; + private int _stock; - public Transaction(Date date, float price, TransactionType transactionType) { + public Transaction(Date date, float price, TransactionType transactionType, int stock) { _date = date; _price = price; _transactionType = transactionType; + _stock = stock; } public Date getDate() { @@ -33,5 +35,9 @@ public interface MerchantDAO { public TransactionType getTransactionType() { return _transactionType; } + + public int getStock() { + return _stock; + } } } diff --git a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/merchant/ParametrizedMerchant.java b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/merchant/ParametrizedMerchant.java index 911364d2d..f6f0b2886 100644 --- a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/merchant/ParametrizedMerchant.java +++ b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/merchant/ParametrizedMerchant.java @@ -11,7 +11,7 @@ import java.util.Map; public class ParametrizedMerchant implements Merchant { private static final int BOOSTER_PRICE = 1000; - private static final long DAY = BOOSTER_PRICE * 60 * 60 * 24; + private static final long DAY = 1000 * 60 * 60 * 24; private Date _merchantSetupDate; @@ -82,7 +82,7 @@ public class ParametrizedMerchant implements Merchant { Integer basePrice = getBasePrice(blueprintId); if (basePrice == null) return null; - lastTrans = new MerchantDAO.Transaction(_merchantSetupDate, basePrice, MerchantDAO.TransactionType.SELL); + lastTrans = new MerchantDAO.Transaction(_merchantSetupDate, basePrice, MerchantDAO.TransactionType.SELL, 0); } if (lastTrans.getDate().getTime() + _priceRevertTimeMs > currentTime.getTime()) { if (lastTrans.getTransactionType() == MerchantDAO.TransactionType.SELL) { diff --git a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/merchant/StorageBasedMerchant.java b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/merchant/StorageBasedMerchant.java new file mode 100644 index 000000000..18d10675f --- /dev/null +++ b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/merchant/StorageBasedMerchant.java @@ -0,0 +1,135 @@ +package com.gempukku.lotro.merchant; + +import com.gempukku.lotro.cards.packs.RarityReader; +import com.gempukku.lotro.cards.packs.SetRarity; +import com.gempukku.lotro.db.MerchantDAO; +import com.gempukku.lotro.game.LotroCardBlueprintLibrary; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +public class StorageBasedMerchant implements Merchant { + private static final int BOOSTER_PRICE = 1000; + private static final long DAY = 1000 * 60 * 60 * 24; + private static final float _profitMargin = 0.7f; + private static final int DOUBLE_AFTER_DAYS = 60; + private static final int MAX_STOCK_BUY = 100; + private static final int FOIL_PRICE_MULTIPLIER = 2; + private static final int MIN_SELL_PRICE = 2; + private static final int MIN_BUY_PRICE = 1; + private static final double PRICE_SHIFT_AFTER_TRANSACTION = 1.05; + + private LotroCardBlueprintLibrary _library; + private MerchantDAO _merchantDao; + private Map _rarity = new HashMap(); + private Date _merchantSetupDate; + + public StorageBasedMerchant(LotroCardBlueprintLibrary library, MerchantDAO merchantDao, Date merchantSetupDate) { + _library = library; + _merchantDao = merchantDao; + _merchantSetupDate = merchantSetupDate; + RarityReader rarityReader = new RarityReader(); + for (int i = 0; i <= 19; i++) + _rarity.put(i, rarityReader.getSetRarity(String.valueOf(i))); + } + + @Override + public void cardBought(String blueprintId, Date currentTime, int price) { + if (blueprintId.endsWith("*")) + price = price / FOIL_PRICE_MULTIPLIER; + blueprintId = _library.getBaseBlueprintId(blueprintId); + _merchantDao.addTransaction(blueprintId, (price / _profitMargin), currentTime, MerchantDAO.TransactionType.BUY); + } + + @Override + public void cardSold(String blueprintId, Date currentTime, int price) { + _merchantDao.addTransaction(blueprintId, price, currentTime, MerchantDAO.TransactionType.SELL); + } + + @Override + public Integer getCardSellPrice(String blueprintId, Date currentTime) { + blueprintId = _library.getBaseBlueprintId(blueprintId); + + MerchantDAO.Transaction lastTransaction = _merchantDao.getLastTransaction(blueprintId); + + if (lastTransaction == null || lastTransaction.getStock() < 0) + return null; + + Double normalPrice = getNormalPrice(lastTransaction, blueprintId, currentTime); + if (normalPrice == null) + return null; + + return Math.max(MIN_SELL_PRICE, (int) Math.ceil(normalPrice)); + } + + @Override + public Integer getCardBuyPrice(String blueprintId, Date currentTime) { + boolean foil = false; + if (blueprintId.endsWith("*")) + foil = true; + + blueprintId = _library.getBaseBlueprintId(blueprintId); + + MerchantDAO.Transaction lastTransaction = _merchantDao.getLastTransaction(blueprintId); + + if (lastTransaction != null && lastTransaction.getStock() >= MAX_STOCK_BUY && !foil) + return null; + + Double normalPrice = getNormalPrice(lastTransaction, blueprintId, currentTime); + if (normalPrice == null) + return null; + + int price = Math.max(MIN_BUY_PRICE, (int) Math.floor(_profitMargin * normalPrice)); + + if (foil) + return FOIL_PRICE_MULTIPLIER * price; + return price; + } + + public Double getNormalPrice(MerchantDAO.Transaction lastTransaction, String blueprintId, Date currentTime) { + float basePrice; + float daysSinceLastTransaction; + int stock; + if (lastTransaction == null) { + Integer priceBasedOnRarity = getBasePrice(blueprintId); + if (priceBasedOnRarity == null) + return null; + basePrice = priceBasedOnRarity.floatValue(); + daysSinceLastTransaction = (currentTime.getTime() - _merchantSetupDate.getTime()) / (DAY * 1f); + stock = 0; + } else { + basePrice = lastTransaction.getPrice(); + if (lastTransaction.getTransactionType() == MerchantDAO.TransactionType.BUY) + basePrice/= PRICE_SHIFT_AFTER_TRANSACTION; + else + basePrice*= PRICE_SHIFT_AFTER_TRANSACTION; + daysSinceLastTransaction = (currentTime.getTime() - lastTransaction.getDate().getTime()) / (DAY * 1f); + stock = lastTransaction.getStock(); + } + + if (stock > 0) + return basePrice / (1+ Math.pow(daysSinceLastTransaction/DOUBLE_AFTER_DAYS, 1.5)); + else + return basePrice * (1+ Math.pow(daysSinceLastTransaction/DOUBLE_AFTER_DAYS, 1.5)); + + } + + private Integer getBasePrice(String blueprintId) { + int underscoreIndex = blueprintId.indexOf("_"); + if (underscoreIndex == -1) + return null; + SetRarity rarity = _rarity.get(Integer.parseInt(blueprintId.substring(0, blueprintId.indexOf("_")))); + String cardRarity = rarity.getCardRarity(blueprintId); + if (cardRarity.equals("X")) + return 3 * BOOSTER_PRICE; + if (cardRarity.equals("R") || cardRarity.equals("P")) + return BOOSTER_PRICE; + if (cardRarity.equals("U") || cardRarity.equals("S")) + return BOOSTER_PRICE / 3; + if (cardRarity.equals("C")) + return BOOSTER_PRICE / 7; + throw new RuntimeException("Unknown rarity for priced card: " + cardRarity); + } +} +; \ No newline at end of file diff --git a/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/merchant/MockMerchantDAO.java b/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/merchant/MockMerchantDAO.java index 46bd00765..2a8f09737 100644 --- a/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/merchant/MockMerchantDAO.java +++ b/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/merchant/MockMerchantDAO.java @@ -10,18 +10,23 @@ public class MockMerchantDAO implements MerchantDAO { private Map _prices = new HashMap(); private Map _dates = new HashMap(); private Map _transactionTypes = new HashMap(); + private Map _stock = new HashMap(); @Override public void addTransaction(String blueprintId, float price, Date date, TransactionType transactionType) { _prices.put(blueprintId, price); _dates.put(blueprintId, date); _transactionTypes.put(blueprintId, transactionType); + Integer previousStock = _stock.get(blueprintId); + if (previousStock == null) + previousStock = 0; + _stock.put(blueprintId, previousStock + ((transactionType == TransactionType.SELL) ? -1 : 1)); } @Override public Transaction getLastTransaction(String blueprintId) { if (_dates.containsKey(blueprintId)) - return new Transaction(_dates.get(blueprintId), _prices.get(blueprintId), _transactionTypes.get(blueprintId)); + return new Transaction(_dates.get(blueprintId), _prices.get(blueprintId), _transactionTypes.get(blueprintId), _stock.get(blueprintId)); return null; } } diff --git a/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/merchant/StorageBasedMerchantTest.java b/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/merchant/StorageBasedMerchantTest.java new file mode 100644 index 000000000..7062ecc57 --- /dev/null +++ b/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/merchant/StorageBasedMerchantTest.java @@ -0,0 +1,116 @@ +package com.gempukku.lotro.merchant; + +import com.gempukku.lotro.db.MerchantDAO; +import com.gempukku.lotro.game.LotroCardBlueprintLibrary; +import org.junit.Before; +import org.junit.Test; + +import java.util.Date; + +import static org.junit.Assert.*; + +public class StorageBasedMerchantTest { + private StorageBasedMerchant _merchant; + private static final long DAY = 1000 * 60 * 60 * 24; + + @Before + public void setup() { + Date setupDate = new Date(0); + MerchantDAO merchantDao = new MockMerchantDAO(); + + _merchant = new StorageBasedMerchant(new LotroCardBlueprintLibrary(), merchantDao, setupDate); + } + + @Test + public void cardTradingWithNoStock() { + assertNull(_merchant.getCardSellPrice("1_1", new Date(0))); + assertNotNull(_merchant.getCardBuyPrice("1_1", new Date(0))); + } + + @Test + public void cardTradingWithSomeStock() { + _merchant.cardBought("1_1", new Date(0), 1000); + assertNotNull(_merchant.getCardSellPrice("1_1", new Date(0))); + assertNotNull(_merchant.getCardBuyPrice("1_1", new Date(0))); + } + + @Test + public void cardTradingWithFullStock() { + for (int i=0; i<100; i++) + _merchant.cardBought("1_1", new Date(0), 1000); + assertNotNull(_merchant.getCardSellPrice("1_1", new Date(0))); + assertNull(_merchant.getCardBuyPrice("1_1", new Date(0))); + } + + @Test + public void cardPriceLowersAfterMerchantBuys() { + _merchant.cardBought("1_1", new Date(0), 700); + int cardInitialPrice = _merchant.getCardSellPrice("1_1", new Date(0)); + assertEqualsMoreOrLess((int) (1000 / 1.05f), cardInitialPrice); + } + + @Test + public void cardPriceRisesAfterMerchantSells() { + // Setup some stock + _merchant.cardBought("1_1", new Date(0), 700); + _merchant.cardBought("1_1", new Date(0), 700); + + _merchant.cardSold("1_1", new Date(0), 1000); + int cardInitialPrice = _merchant.getCardSellPrice("1_1", new Date(0)); + assertEqualsMoreOrLess((int) (1000*1.05f), cardInitialPrice); + } + + @Test + public void cardPriceRisesOverTimeWhenNoStock() { + int initialPrice = _merchant.getCardBuyPrice("1_1", new Date(0)); + assertTrue(initialPrice < _merchant.getCardBuyPrice("1_1", new Date(DAY))); + } + + @Test + public void cardPriceLowersOverTimeWhenInStock() { + _merchant.cardBought("1_1", new Date(0), 700); + int initialPrice = _merchant.getCardBuyPrice("1_1", new Date(0)); + assertTrue(initialPrice > _merchant.getCardBuyPrice("1_1", new Date(DAY))); + } + + // @Test + public void plotPricesAfterTransactions() { + Date setupDate = new Date(-1000 * 60 * 60 * 24 * 50L); + Date firstTrans = new Date(0); + + MerchantDAO merchantDao = new MockMerchantDAO(); + + StorageBasedMerchant merchant = new StorageBasedMerchant(new LotroCardBlueprintLibrary(), merchantDao, setupDate); + + merchant.cardSold("1_1", firstTrans, 1000); + merchant.cardBought("1_2", firstTrans, 700); + + long hour = 1000 * 60 * 60; + + System.out.println("-2,1000,700,1000,700"); + for (long time = 0; time < hour * 24 * 35L; time += hour * 2) { + System.out.println(time / hour + "," + merchant.getCardSellPrice("1_1", new Date(time)) + "," + merchant.getCardBuyPrice("1_1", new Date(time)) + + "," + merchant.getCardSellPrice("1_2", new Date(time)) + "," + merchant.getCardBuyPrice("1_2", new Date(time))); + } + } + + + // @Test + public void plotPriceEasingFromSetup() { + Date setupDate = new Date(0); + + MerchantDAO merchantDao = new MockMerchantDAO(); + + StorageBasedMerchant merchant = new StorageBasedMerchant(new LotroCardBlueprintLibrary(), merchantDao, setupDate); + + long hour = 1000 * 60 * 60; + + for (long time = 0; time < hour * 24 * 35L; time += hour * 2) + System.out.println(time / hour + "," + merchant.getCardSellPrice("1_1", new Date(time)) + "," + merchant.getCardBuyPrice("1_1", new Date(time))); + } + + private void assertEqualsMoreOrLess(int expected, int given) { + assertTrue("Expected " + expected + " more or less, but received " + given, given <= expected + 1 && given >= expected - 1); + } + +}