diff --git a/README.md b/README.md index 9477740..e602b0f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,16 @@ # ac-nh-turnip-prices Price calculator/predictor for Turnip prices + +## Scope + +This tool is: +- A predictor for future prices that week +- Able to calculate probabilities for different futures +- Able to show data from a query string +- A single page web-based app + +This tool is not: +- A calculator for how much money you'll make +- A way to count your turnips +- A way to store multiple people's islands +- Something with a backend diff --git a/css/styles.css b/css/styles.css index ff3e08d..ee7d60c 100644 --- a/css/styles.css +++ b/css/styles.css @@ -368,7 +368,15 @@ input[type=number] { white-space: nowrap; } - +.chart-wrapper { + margin-top: 8px; + display: flex; + flex-wrap: wrap; + height: 400px; + width: 100%; + max-width: 1080px; + justify-content: center; +} .waves { diff --git a/img/192.png b/img/192.png new file mode 100644 index 0000000..68b4ae5 Binary files /dev/null and b/img/192.png differ diff --git a/img/512.png b/img/512.png new file mode 100644 index 0000000..a59c3ec Binary files /dev/null and b/img/512.png differ diff --git a/index.html b/index.html index 0a106be..e1adac5 100644 --- a/index.html +++ b/index.html @@ -8,7 +8,10 @@ Animal Crossing - Turnip Prophet + + + @@ -98,13 +101,12 @@
Sunday
-
- AM - 8:00 am to 11:59 am PM - 12:00 pm to 10:00 pm
@@ -189,6 +191,10 @@

Output

+
+ +
+
@@ -263,6 +269,7 @@ + @@ -284,13 +291,42 @@ Support, comments and contributions are available through Github

+

Oh! And let's not forget to thank those who have contributed so far!

+

+ Contributors: +

+ + + + diff --git a/js/chart.js b/js/chart.js new file mode 100644 index 0000000..9ea48c5 --- /dev/null +++ b/js/chart.js @@ -0,0 +1,54 @@ +let chart_instance = null; + +Chart.defaults.global.defaultFontFamily = "'Varela Round', sans-serif"; + +const chart_options = { + elements: { + line: { + backgroundColor: "#DEF2D9", + backgroundColor: "#DEF2D9", + cubicInterpolationMode: "monotone", + }, + }, + maintainAspectRatio: false, + tooltips: { + intersect: false, + mode: "index", + }, +}; + +function update_chart(input_data, possibilities) { + var ctx = $("#chart"); + + datasets = [ + { + label: "Input Price", + data: input_data.slice(1), + fill: false, + }, + { + label: "Minimum", + data: possibilities[0].prices.slice(1).map(day => day.min), + fill: false, + }, + { + label: "Maximum", + data: possibilities[0].prices.slice(1).map(day => day.max), + fill: "-1", + }, + ]; + + if (chart_instance) { + chart_instance.data.datasets = datasets; + chart_instance.update(); + } else { + chart_instance = new Chart(ctx, { + data: { + datasets: datasets, + labels: ["Sunday", "Mon AM", "Mon PM", "Tue AM", "Tue PM", "Wed AM", "Wed PM", "Thu AM", "Thu PM", "Fri AM", "Fri PM", "Sat AM", "Sat PM"], + }, + options: chart_options, + type: "line", + }); + } +} diff --git a/js/contributors.js b/js/contributors.js new file mode 100644 index 0000000..e3cf7eb --- /dev/null +++ b/js/contributors.js @@ -0,0 +1,16 @@ +function getContributors() { + if (window.jQuery) { + const container = $('#contributors'); + jQuery.ajax('https://api.github.com/repos/mikebryant/ac-nh-turnip-prices/contributors', {}) + .done(function (data) { + data.forEach((contributor, idx) => { + container.append(`${contributor.login}`); + if (idx < data.length - 1) { + container.append(', '); + } + }); + }); + } +} + +$(document).ready(getContributors); diff --git a/js/predictions.js b/js/predictions.js index 96fe462..f1410a6 100644 --- a/js/predictions.js +++ b/js/predictions.js @@ -1,3 +1,7 @@ +// The reverse-engineered code is not perfectly accurate, especially as it's not +// 32-bit ARM floating point. So, be tolerant of slightly unexpected inputs +const FUDGE_FACTOR = 5; + const PATTERN = { FLUCTUATING: 0, LARGE_SPIKE: 1, @@ -10,7 +14,7 @@ const PATTERN_COUNTS = { [PATTERN.LARGE_SPIKE]: 7, [PATTERN.DECREASING]: 1, [PATTERN.SMALL_SPIKE]: 8, -} +}; const PROBABILITY_MATRIX = { [PATTERN.FLUCTUATING]: { @@ -39,15 +43,187 @@ const PROBABILITY_MATRIX = { }, }; +const RATE_MULTIPLIER = 10000; + +function intceil(val) { + return Math.trunc(val + 0.99999); +} + function minimum_rate_from_given_and_base(given_price, buy_price) { - return 10000 * (given_price - 1) / buy_price; + return RATE_MULTIPLIER * (given_price - 0.99999) / buy_price; } function maximum_rate_from_given_and_base(given_price, buy_price) { - return 10000 * given_price / buy_price; + return RATE_MULTIPLIER * (given_price + 0.00001) / buy_price; +} + +function get_price(rate, basePrice) { + return intceil(rate * basePrice / RATE_MULTIPLIER); +} + +/* + * This corresponds to the code: + * for (int i = start; i < start + length; i++) + * { + * sellPrices[work++] = + * intceil(randfloat(rate_min / RATE_MULTIPLIER, rate_max / RATE_MULTIPLIER) * basePrice); + * } + * + * Would modify the predicted_prices array. + * If the given_prices won't match, returns false, otherwise returns true + */ +function generate_individual_random_price( + given_prices, predicted_prices, start, length, rate_min, rate_max) { + rate_min *= RATE_MULTIPLIER; + rate_max *= RATE_MULTIPLIER; + + const buy_price = given_prices[0]; + + for (let i = start; i < start + length; i++) { + let min_pred = get_price(rate_min, buy_price); + let max_pred = get_price(rate_max, buy_price); + if (!isNaN(given_prices[i])) { + if (given_prices[i] < min_pred - FUDGE_FACTOR || given_prices[i] > max_pred + FUDGE_FACTOR) { + // Given price is out of predicted range, so this is the wrong pattern + return false; + } + min_pred = given_prices[i]; + max_pred = given_prices[i]; + } + + predicted_prices.push({ + min: min_pred, + max: max_pred, + }); + } + return true; +} + +/* + * This corresponds to the code: + * rate = randfloat(start_rate_min, start_rate_max); + * for (int i = start; i < start + length; i++) + * { + * sellPrices[work++] = intceil(rate * basePrice); + * rate -= randfloat(rate_decay_min, rate_decay_max); + * } + * + * Would modify the predicted_prices array. + * If the given_prices won't match, returns false, otherwise returns true + */ +function generate_decreasing_random_price( + given_prices, predicted_prices, start, length, rate_min, + rate_max, rate_decay_min, rate_decay_max) { + rate_min *= RATE_MULTIPLIER; + rate_max *= RATE_MULTIPLIER; + rate_decay_min *= RATE_MULTIPLIER; + rate_decay_max *= RATE_MULTIPLIER; + + const buy_price = given_prices[0]; + + for (let i = start; i < start + length; i++) { + let min_pred = get_price(rate_min, buy_price); + let max_pred = get_price(rate_max, buy_price); + if (!isNaN(given_prices[i])) { + if (given_prices[i] < min_pred - FUDGE_FACTOR || given_prices[i] > max_pred + FUDGE_FACTOR) { + // Given price is out of predicted range, so this is the wrong pattern + return false; + } + if (given_prices[i] >= min_pred || given_prices[i] <= max_pred) { + // The value in the FUDGE_FACTOR range is ignored so the rate range would not be empty. + const real_rate_min = minimum_rate_from_given_and_base(given_prices[i], buy_price); + const real_rate_max = maximum_rate_from_given_and_base(given_prices[i], buy_price); + rate_min = Math.max(rate_min, real_rate_min); + rate_max = Math.min(rate_max, real_rate_max); + } + min_pred = given_prices[i]; + max_pred = given_prices[i]; + } + + predicted_prices.push({ + min: min_pred, + max: max_pred, + }); + + rate_min -= rate_decay_max; + rate_max -= rate_decay_min; + } + return true; +} + + +/* + * This corresponds to the code: + * rate = randfloat(rate_min, rate_max); + * sellPrices[work++] = intceil(randfloat(rate_min, rate) * basePrice) - 1; + * sellPrices[work++] = intceil(rate * basePrice); + * sellPrices[work++] = intceil(randfloat(rate_min, rate) * basePrice) - 1; + * + * Would modify the predicted_prices array. + * If the given_prices won't match, returns false, otherwise returns true + */ +function generate_peak_price( + given_prices, predicted_prices, start, rate_min, rate_max) { + rate_min *= RATE_MULTIPLIER; + rate_max *= RATE_MULTIPLIER; + + const buy_price = given_prices[0]; + + // Main spike 1 + min_pred = get_price(rate_min, buy_price) - 1; + max_pred = get_price(rate_max, buy_price) - 1; + if (!isNaN(given_prices[start])) { + if (given_prices[start] < min_pred - FUDGE_FACTOR || given_prices[start] > max_pred + FUDGE_FACTOR) { + // Given price is out of predicted range, so this is the wrong pattern + return false; + } + min_pred = given_prices[start]; + max_pred = given_prices[start]; + } + predicted_prices.push({ + min: min_pred, + max: max_pred, + }); + + // Main spike 2 + min_pred = predicted_prices[start].min; + max_pred = intceil(2.0 * buy_price); + if (!isNaN(given_prices[start + 1])) { + if (given_prices[start + 1] < min_pred - FUDGE_FACTOR || given_prices[start + 1] > max_pred + FUDGE_FACTOR) { + // Given price is out of predicted range, so this is the wrong pattern + return false; + } + min_pred = given_prices[start + 1]; + max_pred = given_prices[start + 1]; + } + predicted_prices.push({ + min: min_pred, + max: max_pred, + }); + + // Main spike 3 + min_pred = intceil(1.4 * buy_price) - 1; + max_pred = predicted_prices[start + 1].max - 1; + if (!isNaN(given_prices[start + 2])) { + if (given_prices[start + 2] < min_pred - FUDGE_FACTOR || given_prices[start + 2] > max_pred + FUDGE_FACTOR) { + // Given price is out of predicted range, so this is the wrong pattern + return false; + } + min_pred = given_prices[start + 2]; + max_pred = given_prices[start + 2]; + } + predicted_prices.push({ + min: min_pred, + max: max_pred, + }); + + return true; } -function* generate_pattern_0_with_lengths(given_prices, high_phase_1_len, dec_phase_1_len, high_phase_2_len, dec_phase_2_len, high_phase_3_len) { +function* + generate_pattern_0_with_lengths( + given_prices, high_phase_1_len, dec_phase_1_len, high_phase_2_len, + dec_phase_2_len, high_phase_3_len) { /* // PATTERN 0: high, decreasing, high, decreasing, high work = 2; @@ -84,8 +260,8 @@ function* generate_pattern_0_with_lengths(given_prices, high_phase_1_len, dec_ph } */ - buy_price = given_prices[0]; - var predicted_prices = [ + const buy_price = given_prices[0]; + const predicted_prices = [ { min: buy_price, max: buy_price, @@ -97,120 +273,45 @@ function* generate_pattern_0_with_lengths(given_prices, high_phase_1_len, dec_ph ]; // High Phase 1 - for (var i = 2; i < 2 + high_phase_1_len; i++) { - min_pred = Math.floor(0.9 * buy_price); - max_pred = Math.ceil(1.4 * buy_price); - if (!isNaN(given_prices[i])) { - if (given_prices[i] < min_pred || given_prices[i] > max_pred) { - // Given price is out of predicted range, so this is the wrong pattern - return; - } - min_pred = given_prices[i]; - max_pred = given_prices[i]; - } - - predicted_prices.push({ - min: min_pred, - max: max_pred, - }); + if (!generate_individual_random_price( + given_prices, predicted_prices, 2, high_phase_1_len, 0.9, 1.4)) { + return; } // Dec Phase 1 - var min_rate = 6000; - var max_rate = 8000; - for (var i = 2 + high_phase_1_len; i < 2 + high_phase_1_len + dec_phase_1_len; i++) { - min_pred = Math.floor(min_rate * buy_price / 10000); - max_pred = Math.ceil(max_rate * buy_price / 10000); - - - if (!isNaN(given_prices[i])) { - if (given_prices[i] < min_pred || given_prices[i] > max_pred) { - // Given price is out of predicted range, so this is the wrong pattern - return; - } - min_pred = given_prices[i]; - max_pred = given_prices[i]; - min_rate = minimum_rate_from_given_and_base(given_prices[i], buy_price); - max_rate = maximum_rate_from_given_and_base(given_prices[i], buy_price); - } - - predicted_prices.push({ - min: min_pred, - max: max_pred, - }); - - min_rate -= 1000; - max_rate -= 400; + if (!generate_decreasing_random_price( + given_prices, predicted_prices, 2 + high_phase_1_len, dec_phase_1_len, + 0.6, 0.8, 0.04, 0.1)) { + return; } // High Phase 2 - for (var i = 2 + high_phase_1_len + dec_phase_1_len; i < 2 + high_phase_1_len + dec_phase_1_len + high_phase_2_len; i++) { - min_pred = Math.floor(0.9 * buy_price); - max_pred = Math.ceil(1.4 * buy_price); - if (!isNaN(given_prices[i])) { - if (given_prices[i] < min_pred || given_prices[i] > max_pred) { - // Given price is out of predicted range, so this is the wrong pattern - return; - } - min_pred = given_prices[i]; - max_pred = given_prices[i]; - } - - predicted_prices.push({ - min: min_pred, - max: max_pred, - }); + if (!generate_individual_random_price(given_prices, predicted_prices, + 2 + high_phase_1_len + dec_phase_1_len, high_phase_2_len, 0.9, 1.4)) { + return; } // Dec Phase 2 - var min_rate = 6000; - var max_rate = 8000; - for (var i = 2 + high_phase_1_len + dec_phase_1_len + high_phase_2_len; i < 2 + high_phase_1_len + dec_phase_1_len + high_phase_2_len + dec_phase_2_len; i++) { - min_pred = Math.floor(min_rate * buy_price / 10000); - max_pred = Math.ceil(max_rate * buy_price / 10000); - - - if (!isNaN(given_prices[i])) { - if (given_prices[i] < min_pred || given_prices[i] > max_pred) { - // Given price is out of predicted range, so this is the wrong pattern - return; - } - min_pred = given_prices[i]; - max_pred = given_prices[i]; - min_rate = minimum_rate_from_given_and_base(given_prices[i], buy_price); - max_rate = maximum_rate_from_given_and_base(given_prices[i], buy_price); - } - - predicted_prices.push({ - min: min_pred, - max: max_pred, - }); - - min_rate -= 1000; - max_rate -= 400; + if (!generate_decreasing_random_price( + given_prices, predicted_prices, + 2 + high_phase_1_len + dec_phase_1_len + high_phase_2_len, + dec_phase_2_len, 0.6, 0.8, 0.04, 0.1)) { + return; } // High Phase 3 if (2 + high_phase_1_len + dec_phase_1_len + high_phase_2_len + dec_phase_2_len + high_phase_3_len != 14) { throw new Error("Phase lengths don't add up"); } - for (var i = 2 + high_phase_1_len + dec_phase_1_len + high_phase_2_len + dec_phase_2_len; i < 14; i++) { - min_pred = Math.floor(0.9 * buy_price); - max_pred = Math.ceil(1.4 * buy_price); - if (!isNaN(given_prices[i])) { - if (given_prices[i] < min_pred || given_prices[i] > max_pred) { - // Given price is out of predicted range, so this is the wrong pattern - return; - } - min_pred = given_prices[i]; - max_pred = given_prices[i]; - } - predicted_prices.push({ - min: min_pred, - max: max_pred, - }); + const prev_length = 2 + high_phase_1_len + dec_phase_1_len + + high_phase_2_len + dec_phase_2_len; + if (!generate_individual_random_price( + given_prices, predicted_prices, prev_length, 14 - prev_length, 0.9, + 1.4)) { + return; } + yield { pattern_description: "Fluctuating", pattern_number: 0, @@ -257,8 +358,8 @@ function* generate_pattern_1_with_peak(given_prices, peak_start) { } */ - buy_price = given_prices[0]; - var predicted_prices = [ + const buy_price = given_prices[0]; + const predicted_prices = [ { min: buy_price, max: buy_price, @@ -269,54 +370,21 @@ function* generate_pattern_1_with_peak(given_prices, peak_start) { }, ]; - var min_rate = 8500; - var max_rate = 9000; - - for (var i = 2; i < peak_start; i++) { - min_pred = Math.floor(min_rate * buy_price / 10000); - max_pred = Math.ceil(max_rate * buy_price / 10000); - - - if (!isNaN(given_prices[i])) { - if (given_prices[i] < min_pred || given_prices[i] > max_pred) { - // Given price is out of predicted range, so this is the wrong pattern - return; - } - min_pred = given_prices[i]; - max_pred = given_prices[i]; - min_rate = minimum_rate_from_given_and_base(given_prices[i], buy_price); - max_rate = maximum_rate_from_given_and_base(given_prices[i], buy_price); - } - - predicted_prices.push({ - min: min_pred, - max: max_pred, - }); - - min_rate -= 500; - max_rate -= 300; + if (!generate_decreasing_random_price( + given_prices, predicted_prices, 2, peak_start - 2, 0.85, 0.9, 0.03, + 0.05)) { + return; } // Now each day is independent of next min_randoms = [0.9, 1.4, 2.0, 1.4, 0.9, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4] max_randoms = [1.4, 2.0, 6.0, 2.0, 1.4, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9] - for (var i = peak_start; i < 14; i++) { - min_pred = Math.floor(min_randoms[i - peak_start] * buy_price); - max_pred = Math.ceil(max_randoms[i - peak_start] * buy_price); - - if (!isNaN(given_prices[i])) { - if (given_prices[i] < min_pred || given_prices[i] > max_pred) { - // Given price is out of predicted range, so this is the wrong pattern - return; - } - min_pred = given_prices[i]; - max_pred = given_prices[i]; + for (let i = peak_start; i < 14; i++) { + if (!generate_individual_random_price( + given_prices, predicted_prices, i, 1, min_randoms[i - peak_start], + max_randoms[i - peak_start])) { + return; } - - predicted_prices.push({ - min: min_pred, - max: max_pred, - }); } yield { pattern_description: "Large spike", @@ -345,9 +413,8 @@ function* generate_pattern_2(given_prices) { break; */ - - buy_price = given_prices[0]; - var predicted_prices = [ + const buy_price = given_prices[0]; + const predicted_prices = [ { min: buy_price, max: buy_price, @@ -358,32 +425,11 @@ function* generate_pattern_2(given_prices) { }, ]; - var min_rate = 8500; - var max_rate = 9000; - for (var i = 2; i < 14; i++) { - min_pred = Math.floor(min_rate * buy_price / 10000); - max_pred = Math.ceil(max_rate * buy_price / 10000); - - - if (!isNaN(given_prices[i])) { - if (given_prices[i] < min_pred || given_prices[i] > max_pred) { - // Given price is out of predicted range, so this is the wrong pattern - return; - } - min_pred = given_prices[i]; - max_pred = given_prices[i]; - min_rate = minimum_rate_from_given_and_base(given_prices[i], buy_price); - max_rate = maximum_rate_from_given_and_base(given_prices[i], buy_price); - } - - predicted_prices.push({ - min: min_pred, - max: max_pred, - }); - - min_rate -= 500; - max_rate -= 300; + if (!generate_decreasing_random_price( + given_prices, predicted_prices, 2, 14 - 2, 0.85, 0.9, 0.03, 0.05)) { + return; } + yield { pattern_description: "Decreasing", pattern_number: 2, @@ -423,8 +469,8 @@ function* generate_pattern_3_with_peak(given_prices, peak_start) { } */ - buy_price = given_prices[0]; - var predicted_prices = [ + const buy_price = given_prices[0]; + const predicted_prices = [ { min: buy_price, max: buy_price, @@ -434,130 +480,30 @@ function* generate_pattern_3_with_peak(given_prices, peak_start) { max: buy_price, }, ]; + let probability = 1; - var min_rate = 4000; - var max_rate = 9000; - - for (var i = 2; i < peak_start; i++) { - min_pred = Math.floor(min_rate * buy_price / 10000); - max_pred = Math.ceil(max_rate * buy_price / 10000); - - - if (!isNaN(given_prices[i])) { - if (given_prices[i] < min_pred || given_prices[i] > max_pred) { - // Given price is out of predicted range, so this is the wrong pattern - return; - } - min_pred = given_prices[i]; - max_pred = given_prices[i]; - min_rate = minimum_rate_from_given_and_base(given_prices[i], buy_price); - max_rate = maximum_rate_from_given_and_base(given_prices[i], buy_price); - } - - predicted_prices.push({ - min: min_pred, - max: max_pred, - }); - - min_rate -= 500; - max_rate -= 300; + if (!generate_decreasing_random_price( + given_prices, predicted_prices, 2, peak_start - 2, 0.4, 0.9, 0.03, + 0.05)) { + return; } // The peak - - for (var i = peak_start; i < peak_start + 2; i++) { - min_pred = Math.floor(0.9 * buy_price); - max_pred = Math.ceil(1.4 * buy_price); - if (!isNaN(given_prices[i])) { - if (given_prices[i] < min_pred || given_prices[i] > max_pred) { - // Given price is out of predicted range, so this is the wrong pattern - return; - } - min_pred = given_prices[i]; - max_pred = given_prices[i]; - } - - predicted_prices.push({ - min: min_pred, - max: max_pred, - }); + if (!generate_individual_random_price( + given_prices, predicted_prices, peak_start, 2, 0.9, 1.4)) { + return; } - // Main spike 1 - min_pred = Math.floor(1.4 * buy_price) - 1; - max_pred = Math.ceil(2.0 * buy_price) - 1; - if (!isNaN(given_prices[peak_start + 2])) { - if (given_prices[peak_start + 2] < min_pred || given_prices[peak_start + 2] > max_pred) { - // Given price is out of predicted range, so this is the wrong pattern - return; - } - min_pred = given_prices[peak_start + 2]; - max_pred = given_prices[peak_start + 2]; + if (!generate_peak_price( + given_prices, predicted_prices, peak_start + 2, 1.4, 2.0)) { + return; } - predicted_prices.push({ - min: min_pred, - max: max_pred, - }); - - // Main spike 2 - min_pred = predicted_prices[peak_start + 2].min; - max_pred = Math.ceil(2.0 * buy_price); - if (!isNaN(given_prices[peak_start + 3])) { - if (given_prices[peak_start + 3] < min_pred || given_prices[peak_start + 3] > max_pred) { - // Given price is out of predicted range, so this is the wrong pattern - return; - } - min_pred = given_prices[peak_start + 3]; - max_pred = given_prices[peak_start + 3]; - } - predicted_prices.push({ - min: min_pred, - max: max_pred, - }); - - // Main spike 3 - min_pred = Math.floor(1.4 * buy_price) - 1; - max_pred = predicted_prices[peak_start + 3].max - 1; - if (!isNaN(given_prices[peak_start + 4])) { - if (given_prices[peak_start + 4] < min_pred || given_prices[peak_start + 4] > max_pred) { - // Given price is out of predicted range, so this is the wrong pattern - return; - } - min_pred = given_prices[peak_start + 4]; - max_pred = given_prices[peak_start + 4]; - } - predicted_prices.push({ - min: min_pred, - max: max_pred, - }); if (peak_start + 5 < 14) { - var min_rate = 4000; - var max_rate = 9000; - - for (var i = peak_start + 5; i < 14; i++) { - min_pred = Math.floor(min_rate * buy_price / 10000); - max_pred = Math.ceil(max_rate * buy_price / 10000); - - - if (!isNaN(given_prices[i])) { - if (given_prices[i] < min_pred || given_prices[i] > max_pred) { - // Given price is out of predicted range, so this is the wrong pattern - return; - } - min_pred = given_prices[i]; - max_pred = given_prices[i]; - min_rate = minimum_rate_from_given_and_base(given_prices[i], buy_price); - max_rate = maximum_rate_from_given_and_base(given_prices[i], buy_price); - } - - predicted_prices.push({ - min: min_pred, - max: max_pred, - }); - - min_rate -= 500; - max_rate -= 300; + if (!generate_decreasing_random_price( + given_prices, predicted_prices, peak_start + 5, + 14 - (peak_start + 5), 0.4, 0.9, 0.03, 0.05)) { + return; } } @@ -620,6 +566,27 @@ function analyze_possibilities(sell_prices, first_buy, previous_pattern) { generated_possibilities = Array.from(generate_possibilities(sell_prices, first_buy)); generated_possibilities = get_probabilities(generated_possibilities, previous_pattern); + for (let poss of generated_possibilities) { + var weekMins = []; + var weekMaxes = []; + for (let day of poss.prices.slice(2)) { + weekMins.push(day.min); + weekMaxes.push(day.max); + } + poss.weekGuaranteedMinimum = Math.max(...weekMins); + poss.weekMax = Math.max(...weekMaxes); + } + + generated_possibilities.sort((a, b) => { + if (a.weekMax < b.weekMax) { + return 1; + } else if (a.weekMax > b.weekMax) { + return -1; + } else { + return 0; + } + }); + global_min_max = []; for (var day = 0; day < 14; day++) { prices = { @@ -637,31 +604,12 @@ function analyze_possibilities(sell_prices, first_buy, previous_pattern) { global_min_max.push(prices); } - generated_possibilities.push({ + generated_possibilities.unshift({ pattern_description: "All patterns", pattern_number: 4, prices: global_min_max, - }); - - for (let poss of generated_possibilities) { - var weekMins = []; - var weekMaxes = []; - for (let day of poss.prices.slice(2)) { - weekMins.push(day.min); - weekMaxes.push(day.max); - } - poss.weekGuaranteedMinimum = Math.max(...weekMins); - poss.weekMax = Math.max(...weekMaxes); - } - - generated_possibilities.sort((a, b) => { - if (a.weekMax < b.weekMax) { - return 1; - } else if (a.weekMax > b.weekMax) { - return -1; - } else { - return 0; - } + weekGuaranteedMinimum: Math.min(...generated_possibilities.map(poss => poss.weekGuaranteedMinimum)), + weekMax: Math.max(...generated_possibilities.map(poss => poss.weekMax)), }); return generated_possibilities; diff --git a/js/scripts.js b/js/scripts.js index 24f1376..65cf904 100644 --- a/js/scripts.js +++ b/js/scripts.js @@ -65,9 +65,10 @@ const fillFields = function (prices, first_buy, previous_pattern) { const initialize = function () { try { - const prices = getPrices() - const first_buy = getFirstBuyState(); - const previous_pattern = getPreviousPatternState(); + const previous = getPrevious(); + const first_buy = previous[0]; + const previous_pattern = previous[1]; + const prices = previous[2]; if (prices === null) { fillFields([], first_buy, previous_pattern) } else { @@ -101,14 +102,65 @@ const isEmpty = function (arr) { return filtered.length == 0 } -const getFirstBuyState = function () { +const getFirstBuyStateFromQuery = function (param) { + try { + const params = new URLSearchParams(window.location.search.substr(1)); + const firstbuy_str = params.get(param); + + if (firstbuy_str == null) { + return null; + } + + firstbuy = null; + if (firstbuy_str == "1" || firstbuy_str == "yes" || firstbuy_str == "true") { + firstbuy = true; + } else if (firstbuy_str == "0" || firstbuy_str == "no" || firstbuy_str == "false") { + firstbuy = false; + } + + return firstbuy; + + } catch (e) { + return null; + } +} + +const getFirstBuyStateFromLocalstorage = function () { return JSON.parse(localStorage.getItem('first_buy')) } -const getPreviousPatternState = function () { +const getPreviousPatternStateFromLocalstorage = function () { return JSON.parse(localStorage.getItem('previous_pattern')) } +const getPreviousPatternStateFromQuery = function (param) { + try { + const params = new URLSearchParams(window.location.search.substr(1)); + const pattern_str = params.get(param); + + if (pattern_str == null) { + return null; + } + + if (pattern_str == "0" || pattern_str == "fluctuating") { + pattern = 0; + } else if (pattern_str == "1" || pattern_str == "large-spike") { + pattern = 1; + } else if (pattern_str == "2" || pattern_str == "decreasing") { + pattern = 2; + } else if (pattern_str == "3" || pattern_str == "small-spike") { + pattern = 3; + } else { + pattern = -1; + } + + return pattern; + + } catch (e) { + return null; + } +} + const getPricesFromLocalstorage = function () { try { const sell_prices = JSON.parse(localStorage.getItem("sell_prices")); @@ -123,10 +175,10 @@ const getPricesFromLocalstorage = function () { } }; -const getPricesFromQuery = function () { +const getPricesFromQuery = function (param) { try { const params = new URLSearchParams(window.location.search.substr(1)); - const sell_prices = params.get("prices").split(".").map((x) => parseInt(x, 10)); + const sell_prices = params.get(param).split(".").map((x) => parseInt(x, 10)); if (!Array.isArray(sell_prices)) { return null; @@ -141,15 +193,45 @@ const getPricesFromQuery = function () { sell_prices.push(0); } - window.price_from_query = true; return sell_prices; } catch (e) { return null; } }; -const getPrices = function () { - return getPricesFromQuery() || getPricesFromLocalstorage(); +const getPreviousFromQuery = function () { + + /* Check if valid prices are entered. Exit immediately if not. */ + prices = getPricesFromQuery("prices"); + if (prices == null) { + return null; + } + + console.log("Using data from query."); + window.populated_from_query = true; + return [ + getFirstBuyStateFromQuery("first"), + getPreviousPatternStateFromQuery("pattern"), + prices + ]; +}; + +const getPreviousFromLocalstorage = function () { + return [ + getFirstBuyStateFromLocalstorage(), + getPreviousPatternStateFromLocalstorage(), + getPricesFromLocalstorage() + ]; +}; + + +/** + * Gets previous values. First tries to parse parameters, + * if none of them match then it looks in local storage. + * @return {[first time, previous pattern, prices]} + */ +const getPrevious = function () { + return getPreviousFromQuery() || getPreviousFromLocalstorage(); }; const getSellPrices = function () { @@ -165,7 +247,8 @@ const calculateOutput = function (data, first_buy, previous_pattern) { return; } let output_possibilities = ""; - for (let poss of analyze_possibilities(data, first_buy, previous_pattern)) { + let analyzed_possibilities = analyze_possibilities(data, first_buy, previous_pattern); + for (let poss of analyzed_possibilities) { var out_line = "" out_line += ``; for (let day of poss.prices.slice(1)) { @@ -180,6 +263,8 @@ const calculateOutput = function (data, first_buy, previous_pattern) { } $("#output").html(output_possibilities) + + update_chart(data, analyzed_possibilities); } const update = function () { @@ -193,7 +278,7 @@ const update = function () { const prices = [buy_price, buy_price, ...sell_prices]; - if (!window.price_from_query) { + if (!window.populated_from_query) { updateLocalStorage(prices, first_buy, previous_pattern); } diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..94f06a7 --- /dev/null +++ b/manifest.json @@ -0,0 +1,19 @@ +{ + "name": "Turnip Prophet - ACNH Turnip Tracker", + "short_name": "Turnip Prophet", + "description": "An app to track your Animal Crossing: New Horizons turnip prices daily!", + "start_url": "index.html", + "display": "standalone", + "background_color": "#def2d9", + "theme_color": "#def2d9", + "icons": [ + { + "src": "/img/192.png", + "sizes": "192x192" + }, + { + "src": "/img/512.png", + "sizes": "512x512" + } + ] +} diff --git a/service-worker.js b/service-worker.js new file mode 100644 index 0000000..e68b22d --- /dev/null +++ b/service-worker.js @@ -0,0 +1,90 @@ +// PWA Code adapted from https://github.com/pwa-builder/PWABuilder +const CACHE = "pwa-precache"; +const precacheFiles = [ + "/index.html", + "/js/predictions.js", + "/js/scripts.js", + "/css/styles.css", + "https://code.jquery.com/jquery-3.4.1.min.js", +]; + +self.addEventListener("install", function (event) { + console.log("[PWA] Install Event processing"); + + console.log("[PWA] Skip waiting on install"); + self.skipWaiting(); + + event.waitUntil( + caches.open(CACHE).then(function (cache) { + console.log("[PWA] Caching pages during install"); + return cache.addAll(precacheFiles); + }) + ); +}); + +// Allow sw to control of current page +self.addEventListener("activate", function (event) { + console.log("[PWA] Claiming clients for current page"); + event.waitUntil(self.clients.claim()); +}); + +// If any fetch fails, it will look for the request in the cache and serve it from there first +self.addEventListener("fetch", function (event) { + if (event.request.method !== "GET") return; + + event.respondWith( + fromCache(event.request).then( + (response) => { + // The response was found in the cache so we responde with it and update the entry + + // This is where we call the server to get the newest version of the + // file to use the next time we show view + event.waitUntil( + fetch(event.request).then(function (response) { + return updateCache(event.request, response); + }) + ); + + return response; + }, + () => { + // The response was not found in the cache so we look for it on the server + return fetch(event.request) + .then(function (response) { + // If request was success, add or update it in the cache + event.waitUntil( + updateCache(event.request, response.clone()) + ); + + return response; + }) + .catch(function (error) { + console.log( + "[PWA] Network request failed and no cache." + error + ); + }); + } + ) + ); +}); + +function fromCache(request) { + // Check to see if you have it in the cache + // Return response + // If not in the cache, then return + return caches.open(CACHE).then(function (cache) { + return cache.match(request).then(function (matching) { + if (!matching || matching.status === 404) { + return Promise.reject("no-match"); + } + + return matching; + }); + }); +} + +function updateCache(request, response) { + return caches.open(CACHE).then(function (cache) { + return cache.put(request, response); + }); +}
" + poss.pattern_description + "${Number.isFinite(poss.probability) ? ((poss.probability * 100).toPrecision(3) + '%') : '—'}