Merge branch 'master' into darkmode

master
Josué Martinez 4 years ago committed by GitHub
commit da7060229b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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 {

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

@ -8,7 +8,10 @@
<title>Animal Crossing - Turnip Prophet</title>
<meta name="description" content="Animal Crossing - Turnip Prophet">
<meta name="author" content="Mike Bryant">
<meta name="theme-color" content="#def2d9">
<link rel="manifest" href="/manifest.json">
<link rel="apple-touch-icon" href="/img/192.png">
<link rel="stylesheet" href="css/styles.css">
<!-- Global site tag (gtag.js) - Google Analytics -->
@ -98,13 +101,12 @@
<div class="form__row">
<h6>Sunday</h6>
<div class="input__group">
<label>What was the price of turnips this week? <i>(If this is your first time buying turnips, this field
<label>What was the price of turnips this week on your island? <i>(If this is your first time buying turnips, this field
will be disabled)</i></label>
<input type="number" id="buy" placeholder="..." />
</div>
</div>
<i>AM - 8:00 am to 11:59 am</i>
<i>PM - 12:00 pm to 10:00 pm</i>
<div class="form__flex-wrap">
@ -189,6 +191,10 @@
<h2>Output</h2>
<div class="chart-wrapper">
<canvas id="chart" width="100%" height="100"></canvas>
</div>
<div class="table-wrapper">
<table id="turnipTable">
<thead>
@ -263,6 +269,7 @@
</g>
</svg>
</div>
</div>
</div>
@ -284,13 +291,42 @@
Support, comments and contributions are available through <a
href="https://github.com/mikebryant/ac-nh-turnip-prices/issues">Github</a>
</p>
<p>Oh! And let's not forget to thank those who have contributed so far!</p>
<p id="contributors">
Contributors:
</p>
</div>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js"></script>
<script src="js/chart.js"></script>
<script src="js/predictions.js"></script>
<script src="js/scripts.js"></script>
<script src="https://cdn.jsdelivr.net/npm/darkmode-js@1.5.5/lib/darkmode-js.min.js"></script>
<script> new Darkmode({label: '🌓', buttonColorDark: '#fff00'}).showWidget();</script>
<script src="js/contributors.js"></script>
<script>
// Check compatibility for the browser we're running this in
if ("serviceWorker" in navigator) {
if (navigator.serviceWorker.controller) {
console.log(
"[PWA] Service Worker already found, skipping register"
);
} else {
// Register the service worker
navigator.serviceWorker
.register("/service-worker.js", {
scope: "./",
})
.then(function (reg) {
console.log(
"[PWA] Service worker has been registered for scope: " +
reg.scope
);
});
}
}
</script>
</body>
</html>

@ -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",
});
}
}

@ -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(`<a href="${contributor.html_url}">${contributor.login}</a>`);
if (idx < data.length - 1) {
container.append(', ');
}
});
});
}
}
$(document).ready(getContributors);

@ -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;

@ -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 = "<tr><td class='table-pattern'>" + poss.pattern_description + "</td>"
out_line += `<td>${Number.isFinite(poss.probability) ? ((poss.probability * 100).toPrecision(3) + '%') : '—'}</td>`;
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);
}

@ -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"
}
]
}

@ -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);
});
}
Loading…
Cancel
Save