merge from master

master
Lawrence Luk 4 years ago
commit 5bdf9668f9

@ -65,6 +65,36 @@ h2 {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.08);
}
.dialog-box-lng {
text-align: center;
}
.dialog-box-lng p,
.dialog-box-lng select {
display: inline;
}
.dialog-box-lng select {
font-size: 1rem;
padding: 4px;
font-weight: bold;
border-radius: 4px;
border-color: #DEF2D9;
color: #837865;
cursor: pointer;
transition: 0.2s all;
}
.dialog-box-lng select:hover {
background-color: #EBFEFD;
border-color: #5ECEDB;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.16);
}
.dialog-box-lng select:focus {
outline: none;
}
.dialog-box p {
font-family: 'Raleway', sans-serif;
font-weight: 800;
@ -267,10 +297,10 @@ input[type=number] {
color: #FFF;
}
.reset-button {
.button {
color: #686868;
font-family: inherit;
font-weight: bold;
color: #FFBABA;
padding: 8px 16px;
border-width: 0px;
border-radius: 40px;
@ -281,13 +311,16 @@ input[type=number] {
margin: 16px auto;
}
.reset-button:hover {
.button:hover {
transform: scale(1.1);
cursor: pointer;
opacity: 1;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.05), 0 3px 6px rgba(0, 0, 0, 0.09);
}
.button.button--reset {
color: #E45B5B;
}
.table-wrapper {
display: inline-block;
@ -348,7 +381,7 @@ input[type=number] {
#turnipTable td {
white-space: nowrap;
max-width: 100px;
max-width: 150px;
padding: 6px 4px;
text-align: center;
border-right: 1px solid rgba(0, 0, 0, 0.03);
@ -402,7 +435,6 @@ input[type=number] {
justify-content: center;
}
.waves {
position: relative;
width: 100%;
@ -412,6 +444,74 @@ input[type=number] {
max-height: 150px;
}
#permalink-input {
display: none;
position: fixed;
}
.permalink {
display: none;
white-space: nowrap;
font-size: 18px;
user-select: none;
cursor: pointer;
}
.permalink .fa-copy {
margin: 0 8px;
height: 20px;
color: #0AB5CD;
}
/* The snackbar - position it at the bottom and in the middle of the screen */
#snackbar {
visibility: hidden; /* Hidden by default. Visible on click */
min-width: 250px; /* Set a default minimum width */
background-color: #FFFAE5; /* Black background color */
font-family: 'Raleway', sans-serif;
font-weight: 800;
font-size: 1rem;
color: #837865;
letter-spacing: 0.2px;
line-height: 1.8rem;
text-align: center; /* Centered text */
border-radius: 40px; /* Rounded borders */
padding: 16px 24px; /* Padding */
position: fixed; /* Sit on top of the screen */
z-index: 1; /* Add a z-index if needed */
bottom: 30px; /* 30px from the bottom */
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.08);
}
/* Show the snackbar when clicking on a button (class added with JavaScript) */
#snackbar.show {
visibility: visible; /* Show the snackbar */
/* Add animation: Take 0.5 seconds to fade in and out the snackbar.
However, delay the fade out process for 2.5 seconds */
-webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s;
animation: fadein 0.5s, fadeout 0.5s 2.5s;
}
/* Animations to fade the snackbar in and out */
@-webkit-keyframes fadein {
from {bottom: 0; opacity: 0;}
to {bottom: 30px; opacity: 1;}
}
@keyframes fadein {
from {bottom: 0; opacity: 0;}
to {bottom: 30px; opacity: 1;}
}
@-webkit-keyframes fadeout {
from {bottom: 30px; opacity: 1;}
to {bottom: 0; opacity: 0;}
}
@keyframes fadeout {
from {bottom: 30px; opacity: 1;}
to {bottom: 0; opacity: 0;}
}
/* Animation */
.parallax>use {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

@ -10,8 +10,20 @@
<meta name="author" content="Mike Bryant">
<meta name="theme-color" content="#def2d9">
<!-- generics -->
<link rel="icon" href="img/favicon-32.png" sizes="32x32">
<link rel="icon" href="img/favicon-128.png" sizes="128x128">
<link rel="icon" href="img/favicon-192.png" sizes="192x192">
<!-- Android -->
<link rel="shortcut icon" href="img/favicon-196.png" sizes="196x196">
<!-- iOS -->
<link rel="apple-touch-icon" href="img/favicon-152.png" sizes="152x152">
<link rel="apple-touch-icon" href="img/favicon-167.png" sizes="167x167">
<link rel="apple-touch-icon" href="img/favicon-180.png" sizes="180x180">
<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 -->
@ -33,13 +45,10 @@
<body>
<div class="dialog-box">
<h2 class="dialog-box__name">Daisy Mae</h2>
<p>Hello, and welcome to the <b>Turnip Prophet</b> app on your Nook Phone.</p>
<p>This app lets you track your island's turnip prices daily, but you'll have to put the prices in yourself!
</p>
<p>After that, the Turnip Prophet app will <b>magically</b> predict the turnip prices you'll have for the rest of
the week.
</p>
<h2 class="dialog-box__name" data-i18n="general.daisy-mae"></h2>
<p data-i18n="[html]welcome.salutation"></p>
<p data-i18n="welcome.description"></p>
<p data-i18n="[html]welcome.conclusion"></p>
</div>
<div class="nook-phone">
@ -66,130 +75,133 @@
<form class="input__form">
<div class="form__row">
<h6>First-Time Buyer</h6>
<h6 data-i18n="first-time.title"></h6>
<div class="input__group">
<label>Is this your first time buying turnips from Daisy Mae on your island?<i>(This affects your
pattern)</i></label>
<label data-i18n="[html]first-time.description"></label>
<div class="input__radio-buttons">
<input type="radio" id="first-time-radio-no" name="first-time" value="false" checked>
<label for="first-time-radio-no">No</label>
<label for="first-time-radio-no" data-i18n="first-time.no"></label>
<input type="radio" id="first-time-radio-yes" name="first-time" value="true">
<label for="first-time-radio-yes">Yes</label>
<label for="first-time-radio-yes" data-i18n="first-time.yes"></label>
</div>
</div>
</div>
<div class="form__row">
<h6>Previous Pattern</h6>
<h6 data-i18n="patterns.title"></h6>
<div class="input__group">
<label for="">What was last week's turnip price pattern?<i>(This affects your pattern)</i></label>
<label for="" data-i18n="[html]patterns.description"></label>
<div class="input__radio-buttons">
<input type="radio" id="pattern-radio-unknown" name="pattern" value="-1" checked>
<label for="pattern-radio-unknown">I don't know</label>
<label data-i18n="patterns.unknown" for="pattern-radio-unknown"></label>
<input type="radio" id="pattern-radio-fluctuating" name="pattern" value="0">
<label for="pattern-radio-fluctuating">Fluctuating</label>
<label data-i18n="patterns.fluctuating" for="pattern-radio-fluctuating"></label>
<input type="radio" id="pattern-radio-small-spike" name="pattern" value="3">
<label for="pattern-radio-small-spike">Small Spike</label>
<label data-i18n="patterns.small-spike" for="pattern-radio-small-spike"></label>
<input type="radio" id="pattern-radio-large-spike" name="pattern" value="1">
<label for="pattern-radio-large-spike">Large Spike</label>
<label data-i18n="patterns.large-spike" for="pattern-radio-large-spike"></label>
<input type="radio" id="pattern-radio-decreasing" name="pattern" value="2">
<label for="pattern-radio-decreasing">Decreasing</label>
<label data-i18n="patterns.decreasing" for="pattern-radio-decreasing"></label>
</div>
</div>
</div>
<div class="form__row">
<h6>Sunday</h6>
<h6 data-i18n="weekdays.sunday"></h6>
<div class="input__group">
<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>
<label data-i18n="[html]prices.description"></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>
<i data-i18n="prices.open.am"></i>
<i data-i18n="prices.open.pm"></i>
<div class="form__flex-wrap">
<div class="form__row">
<h6>Monday</h6>
<h6 data-i18n="weekdays.monday"></h6>
<div class="input__group">
<label for="sell_2">AM</label>
<label data-i18n="times.morning" for="sell_2"></label>
<input type="number" id="sell_2" placeholder="..." />
</div>
<div class="input__group">
<label for="sell_3">PM</label>
<label data-i18n="times.afternoon" for="sell_3"></label>
<input type="number" id="sell_3" placeholder="..." />
</div>
</div>
<div class="form__row">
<h6>Tuesday</h6>
<h6 data-i18n="weekdays.tuesday"></h6>
<div class="input__group">
<label for="sell_4">AM</label>
<label data-i18n="times.morning" for="sell_4"></label>
<input type="number" id="sell_4" placeholder="..." />
</div>
<div class="input__group">
<label for="sell_5">PM</label>
<label data-i18n="times.afternoon" for="sell_5"></label>
<input type="number" id="sell_5" placeholder="..." />
</div>
</div>
<div class="form__row">
<h6>Wednesday</h6>
<h6 data-i18n="weekdays.wednesday"></h6>
<div class="input__group">
<label for="sell_6">AM</label>
<label data-i18n="times.morning" for="sell_6"></label>
<input type="number" id="sell_6" placeholder="..." />
</div>
<div class="input__group">
<label for="sell_7">PM</label>
<label data-i18n="times.afternoon" for="sell_7"></label>
<input type="number" id="sell_7" placeholder="..." />
</div>
</div>
<div class="form__row">
<h6>Thursday</h6>
<h6 data-i18n="weekdays.thursday"></h6>
<div class="input__group">
<label for="sell_8">AM</label>
<label data-i18n="times.morning" for="sell_8"></label>
<input type="number" id="sell_8" placeholder="..." />
</div>
<div class="input__group">
<label for="sell_9">PM</label>
<label data-i18n="times.afternoon" for="sell_9"></label>
<input type="number" id="sell_9" placeholder="..." />
</div>
</div>
<div class="form__row">
<h6>Friday</h6>
<h6 data-i18n="weekdays.friday"></h6>
<div class="input__group">
<label for="sell_10">AM</label>
<label data-i18n="times.morning" for="sell_10"></label>
<input type="number" id="sell_10" placeholder="..." />
</div>
<div class="input__group">
<label for="sell_11">PM</label>
<label data-i18n="times.afternoon" for="sell_11"></label>
<input type="number" id="sell_11" placeholder="..." />
</div>
</div>
<div class="form__row">
<h6>Saturday</h6>
<h6 data-i18n="weekdays.saturday"></h6>
<div class="input__group">
<label for="sell_12">AM</label>
<label data-i18n="times.morning" for="sell_12"></label>
<input type="number" id="sell_12" placeholder="..." />
</div>
<div class="input__group">
<label for="sell_13">PM</label>
<label data-i18n="times.afternoon" for="sell_13"></label>
<input type="number" id="sell_13" placeholder="..." />
</div>
</div>
</div>
<button type="button" id="reset" class="reset-button" name="action">
Reset Turnip Prophet
</button>
<input id="permalink-input" type="text" readOnly />
<div id="permalink-btn" class="button permalink">
<span data-i18n="prices.copy-permalink"></span>
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="copy" class="svg-inline--fa fa-copy fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path fill="currentColor" d="M320 448v40c0 13.255-10.745 24-24 24H24c-13.255 0-24-10.745-24-24V120c0-13.255 10.745-24 24-24h72v296c0 30.879 25.121 56 56 56h168zm0-344V0H152c-13.255 0-24 10.745-24 24v368c0 13.255 10.745 24 24 24h272c13.255 0 24-10.745 24-24V128H344c-13.2 0-24-10.8-24-24zm120.971-31.029L375.029 7.029A24 24 0 0 0 358.059 0H352v96h96v-6.059a24 24 0 0 0-7.029-16.97z"></path>
</svg>
</div>
<button type="button" id="reset" class="button button--reset" name="action" data-i18n="prices.reset"></button>
</form>
<h2>Output</h2>
<h2 data-i18n="output.title"></h2>
<div class="chart-wrapper">
<canvas id="chart" width="100%" height="100"></canvas>
@ -199,55 +211,55 @@
<table id="turnipTable">
<thead>
<tr>
<th valign="bottom">Pattern</th>
<th valign="bottom">% Chance</th>
<th valign="bottom">Sunday</th>
<th valign="bottom" data-i18n="patterns.pattern"></th>
<th colspan="2" valign="bottom" data-i18n="output.chance"></th>
<th valign="bottom" data-i18n="weekdays.sunday"></th>
<th colspan="2">
<div>Monday</div>
<div data-i18n="weekdays.monday"></div>
<div>
<span>AM</span>
<span>PM</span>
<span data-i18n="times.morning"></span>
<span data-i18n="times.afternoon"></span>
</div>
</th>
<th colspan="2">
<div>Tuesday</div>
<div data-i18n="weekdays.tuesday"></div>
<div>
<span>AM</span>
<span>PM</span>
<span data-i18n="times.morning"></span>
<span data-i18n="times.afternoon"></span>
</div>
</th>
<th colspan="2">
<div>
<div>Wednesday</div>
<div data-i18n="weekdays.wednesday"></div>
<div>
<span>AM</span>
<span>PM</span>
<span data-i18n="times.morning"></span>
<span data-i18n="times.afternoon"></span>
</div>
</div>
</th>
<th colspan="2">
<div>Thursday</div>
<div data-i18n="weekdays.thursday"></div>
<div>
<span>AM</span>
<span>PM</span>
<span data-i18n="times.morning"></span>
<span data-i18n="times.afternoon"></span>
</div>
</th>
<th colspan="2">
<div>Friday</div>
<div data-i18n="weekdays.friday"></div>
<div>
<span>AM</span>
<span>PM</span>
<span data-i18n="times.morning"></span>
<span data-i18n="times.afternoon"></span>
</div>
</th>
<th colspan="2">
<div>Saturday</div>
<div data-i18n="weekdays.saturday"></div>
<div>
<span>AM</span>
<span>PM</span>
<span data-i18n="times.morning"></span>
<span data-i18n="times.afternoon"></span>
</div>
</th>
<th valign="bottom">Guaranteed Minimum</th>
<th valign="bottom">Potential Maximum</th>
<th valign="bottom" data-i18n="output.minimum"></th>
<th valign="bottom" data-i18n="output.maximum"></th>
</tr>
</thead>
<tbody id="output"></tbody>
@ -274,34 +286,34 @@
</div>
<div class="dialog-box">
<h2 class="dialog-box__name">Daisy Mae</h2>
<p>After you've listed some turnip prices, the Turnip Prophet will run some numbers and display the different
possible patterns that your island may experience.</p>
<p>This app is still in development, but will improve over time!</p>
<h2 class="dialog-box__name" data-i18n="general.daisy-mae"></h2>
<p data-i18n="textbox.description"></p>
<p data-i18n="textbox.development"></p>
</div>
<div class="dialog-box">
<h2 class="dialog-box__name">Daisy Mae</h2>
<p>
None of this would have been possible without <a
href="https://twitter.com/_Ninji/status/1244818665851289602?s=20">Ninji's work</a> figuring out just how Timmy
and Tommy value their turnips.
</p>
<p>
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>
<h2 class="dialog-box__name" data-i18n="general.daisy-mae"></h2>
<p data-i18n="[html]textbox.thanks"></p>
<p data-i18n="[html]textbox.support"></p>
<p data-i18n="textbox.contributors-text"></p>
<p id="contributors"><span data-i18n="textbox.contributors"></span>: </p>
<div class="dialog-box-lng">
<p data-i18n="textbox.language"></p>: <select id="language"></select>
</div>
</div>
<div id="snackbar">Some text some message...</div>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/i18next/19.4.2/i18next.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/i18next-xhr-backend/3.2.2/i18nextXHRBackend.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/i18next-browser-languagedetector/4.0.2/i18nextBrowserLanguageDetector.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-i18next/1.2.1/jquery-i18next.min.js"></script>
<script src="js/chart.js"></script>
<script src="js/predictions.js"></script>
<script src="js/scripts.js"></script>
<script src="js/translations.js"></script>
<script src="js/contributors.js"></script>
<script>
// Check compatibility for the browser we're running this in
@ -326,5 +338,4 @@
}
</script>
</body>
</html>

@ -22,17 +22,17 @@ function update_chart(input_data, possibilities) {
datasets = [
{
label: "Input Price",
label: i18next.t("output.chart.input"),
data: input_data.slice(1),
fill: false,
},
{
label: "Minimum",
label: i18next.t("output.chart.minimum"),
data: possibilities[0].prices.slice(1).map(day => day.min),
fill: false,
},
{
label: "Maximum",
label: i18next.t("output.chart.maximum"),
data: possibilities[0].prices.slice(1).map(day => day.max),
fill: "-1",
},
@ -45,7 +45,7 @@ function update_chart(input_data, possibilities) {
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"],
labels: [i18next.t("weekdays.sunday"), i18next.t("weekdays.abr.monday") + " " + i18next.t("times.morning"), i18next.t("weekdays.abr.monday") + " " + i18next.t("times.afternoon"), i18next.t("weekdays.abr.tuesday") + " " + i18next.t("times.morning"), i18next.t("weekdays.abr.tuesday") + " " + i18next.t("times.afternoon"), i18next.t("weekdays.abr.wednesday") + " " + i18next.t("times.morning"), i18next.t("weekdays.abr.wednesday") + " " + i18next.t("times.afternoon"), i18next.t("weekdays.abr.thursday") + " " + i18next.t("times.morning"), i18next.t("weekdays.abr.thursday") + " " + i18next.t("times.afternoon"), i18next.t("weekdays.abr.friday") + " " + i18next.t("times.morning"), i18next.t("weekdays.abr.friday") + " " + i18next.t("times.afternoon"), i18next.t("weekdays.abr.saturday") + " " + i18next.t("times.morning"), i18next.t("weekdays.abr.saturday") + " " + i18next.t("times.afternoon")],
},
options: chart_options,
type: "line",

@ -9,13 +9,6 @@ const PATTERN = {
SMALL_SPIKE: 3,
};
const PATTERN_COUNTS = {
[PATTERN.FLUCTUATING]: 56,
[PATTERN.LARGE_SPIKE]: 7,
[PATTERN.DECREASING]: 1,
[PATTERN.SMALL_SPIKE]: 8,
};
const PROBABILITY_MATRIX = {
[PATTERN.FLUCTUATING]: {
[PATTERN.FLUCTUATING]: 0.20,
@ -57,10 +50,46 @@ function maximum_rate_from_given_and_base(given_price, buy_price) {
return RATE_MULTIPLIER * (given_price + 0.00001) / buy_price;
}
function rate_range_from_given_and_base(given_price, buy_price) {
return [
minimum_rate_from_given_and_base(given_price, buy_price),
maximum_rate_from_given_and_base(given_price, buy_price)
];
}
function get_price(rate, basePrice) {
return intceil(rate * basePrice / RATE_MULTIPLIER);
}
function* multiply_generator_probability(generator, probability) {
for (const it of generator) {
yield {...it, probability: it.probability * probability};
}
}
function clamp(x, min, max) {
return Math.min(Math.max(x, min), max);
}
function range_length(range) {
return range[1] - range[0];
}
function range_intersect(range1, range2) {
if (range1[0] > range2[1] || range1[1] < range2[0]) {
return null;
}
return [Math.max(range1[0], range2[0]), Math.min(range1[1], range2[1])];
}
function range_intersect_length(range1, range2) {
if (range1[0] > range2[1] || range1[1] < range2[0]) {
return 0;
}
return range_length(range_intersect(range1, range2));
}
/*
* This corresponds to the code:
* for (int i = start; i < start + length; i++)
@ -69,8 +98,9 @@ function get_price(rate, basePrice) {
* 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
* Would return the conditional probability given the given_prices, and modify
* the predicted_prices array.
* If the given_prices won't match, returns 0.
*/
function generate_individual_random_price(
given_prices, predicted_prices, start, length, rate_min, rate_max) {
@ -78,6 +108,8 @@ function generate_individual_random_price(
rate_max *= RATE_MULTIPLIER;
const buy_price = given_prices[0];
const rate_range = [rate_min, rate_max];
let prob = 1;
for (let i = start; i < start + length; i++) {
let min_pred = get_price(rate_min, buy_price);
@ -85,8 +117,14 @@ function generate_individual_random_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;
return 0;
}
// TODO: How to deal with probability when there's fudge factor?
// Clamp the value to be in range now so the probability won't be totally biased to fudged values.
const real_rate_range =
rate_range_from_given_and_base(clamp(given_prices[i], min_pred, max_pred), buy_price);
prob *= range_intersect_length(rate_range, real_rate_range) /
range_length(rate_range);
min_pred = given_prices[i];
max_pred = given_prices[i];
}
@ -96,7 +134,144 @@ function generate_individual_random_price(
max: max_pred,
});
}
return true;
return prob;
}
/*
* Probability Density Function of rates.
* Since the PDF is continuous*, we approximate it by a discrete probability function:
* the value in range [(x - 0.5), (x + 0.5)) has a uniform probability
* prob[x - value_start];
*
* Note that we operate all rate on the (* RATE_MULTIPLIER) scale.
*
* (*): Well not really since it only takes values that "float" can represent in some form, but the
* space is too large to compute directly in JS.
*/
class PDF {
/*
* Initialize a PDF in range [a, b], a and b can be non-integer.
* if uniform is true, then initialize the probability to be uniform, else initialize to a
* all-zero (invalid) PDF.
*/
constructor(a, b, uniform = true) {
this.value_start = Math.round(a);
this.value_end = Math.round(b);
const range = [a, b];
const total_length = range_length(range);
this.prob = Array(this.value_end - this.value_start + 1);
if (uniform) {
for (let i = 0; i < this.prob.length; i++) {
this.prob[i] =
range_intersect_length(this.range_of(i), range) / total_length;
}
}
}
range_of(idx) {
// TODO: consider doing the "exclusive end" properly.
return [this.value_start + idx - 0.5, this.value_start + idx + 0.5 - 1e-9];
}
min_value() {
return this.value_start - 0.5;
}
max_value() {
return this.value_end + 0.5 - 1e-9;
}
normalize() {
const total_probability = this.prob.reduce((acc, it) => acc + it, 0);
for (let i = 0; i < this.prob.length; i++) {
this.prob[i] /= total_probability;
}
}
/*
* Limit the values to be in the range, and return the probability that the value was in this
* range.
*/
range_limit(range) {
let [start, end] = range;
start = Math.max(start, this.min_value());
end = Math.min(end, this.max_value());
if (start >= end) {
// Set this to invalid values
this.value_start = this.value_end = 0;
this.prob = [];
return 0;
}
let prob = 0;
const start_idx = Math.round(start) - this.value_start;
const end_idx = Math.round(end) - this.value_start;
for (let i = start_idx; i <= end_idx; i++) {
const bucket_prob = this.prob[i] * range_intersect_length(this.range_of(i), range);
this.prob[i] = bucket_prob;
prob += bucket_prob;
}
this.prob = this.prob.slice(start_idx, end_idx + 1);
this.value_start = Math.round(start);
this.value_end = Math.round(end);
this.normalize();
return prob;
}
/*
* Subtract the PDF by a uniform distribution in [rate_decay_min, rate_decay_max]
*
* For simplicity, we assume that rate_decay_min and rate_decay_max are both integers.
*/
decay(rate_decay_min, rate_decay_max) {
const ret = new PDF(
this.min_value() - rate_decay_max, this.max_value() - rate_decay_min, false);
/*
// O(n^2) naive algorithm for reference, which would be too slow.
for (let i = this.value_start; i <= this.value_end; i++) {
const unit_prob = this.prob[i - this.value_start] / (rate_decay_max - rate_decay_min) / 2;
for (let j = rate_decay_min; j < rate_decay_max; j++) {
// ([i - 0.5, i + 0.5] uniform) - ([j, j + 1] uniform)
// -> [i - j - 1.5, i + 0.5 - j] with a triangular PDF
// -> approximate by
// [i - j - 1.5, i - j - 0.5] uniform &
// [i - j - 0.5, i - j + 0.5] uniform
ret.prob[i - j - 1 - ret.value_start] += unit_prob; // Part A
ret.prob[i - j - ret.value_start] += unit_prob; // Part B
}
}
*/
// Transform to "CDF"
for (let i = 1; i < this.prob.length; i++) {
this.prob[i] += this.prob[i - 1];
}
// Return this.prob[l - this.value_start] + ... + this.prob[r - 1 - this.value_start];
// This assume that this.prob is already transformed to "CDF".
const sum = (l, r) => {
l -= this.value_start;
r -= this.value_start;
if (l < 0) l = 0;
if (r > this.prob.length) r = this.prob.length;
if (l >= r) return 0;
return this.prob[r - 1] - (l == 0 ? 0 : this.prob[l - 1]);
};
for (let x = 0; x < ret.prob.length; x++) {
// i - j - 1 - ret.value_start == x (Part A)
// -> i = x + j + 1 + ret.value_start, j in [rate_decay_min, rate_decay_max)
ret.prob[x] = sum(x + rate_decay_min + 1 + ret.value_start, x + rate_decay_max + 1 + ret.value_start);
// i - j - ret.value_start == x (Part B)
// -> i = x + j + ret.value_start, j in [rate_decay_min, rate_decay_max)
ret.prob[x] += sum(x + rate_decay_min + ret.value_start, x + rate_decay_max + ret.value_start);
}
this.prob = ret.prob;
this.value_start = ret.value_start;
this.value_end = ret.value_end;
this.normalize();
}
}
/*
@ -108,33 +283,37 @@ function generate_individual_random_price(
* 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
* Would return the conditional probability given the given_prices, and modify
* the predicted_prices array.
* If the given_prices won't match, returns 0.
*/
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;
given_prices, predicted_prices, start, length, start_rate_min,
start_rate_max, rate_decay_min, rate_decay_max) {
start_rate_min *= RATE_MULTIPLIER;
start_rate_max *= RATE_MULTIPLIER;
rate_decay_min *= RATE_MULTIPLIER;
rate_decay_max *= RATE_MULTIPLIER;
const buy_price = given_prices[0];
let rate_pdf = new PDF(start_rate_min, start_rate_max);
let prob = 1;
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);
let min_pred = get_price(rate_pdf.min_value(), buy_price);
let max_pred = get_price(rate_pdf.max_value(), 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;
return 0;
}
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);
// TODO: How to deal with probability when there's fudge factor?
// Clamp the value to be in range now so the probability won't be totally biased to fudged values.
const real_rate_range =
rate_range_from_given_and_base(clamp(given_prices[i], min_pred, max_pred), buy_price);
prob *= rate_pdf.range_limit(real_rate_range);
if (prob == 0) {
return 0;
}
min_pred = given_prices[i];
max_pred = given_prices[i];
@ -145,10 +324,9 @@ function generate_decreasing_random_price(
max: max_pred,
});
rate_min -= rate_decay_max;
rate_max -= rate_decay_min;
rate_pdf.decay(rate_decay_min, rate_decay_max);
}
return true;
return prob;
}
@ -159,8 +337,9 @@ function generate_decreasing_random_price(
* 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
* Would return the conditional probability given the given_prices, and modify
* the predicted_prices array.
* If the given_prices won't match, returns 0.
*/
function generate_peak_price(
given_prices, predicted_prices, start, rate_min, rate_max) {
@ -168,15 +347,86 @@ function generate_peak_price(
rate_max *= RATE_MULTIPLIER;
const buy_price = given_prices[0];
let prob = 1;
let rate_range = [rate_min, rate_max];
// * Calculate the probability first.
// Prob(middle_price)
const middle_price = given_prices[start + 1];
if (!isNaN(middle_price)) {
const min_pred = get_price(rate_min, buy_price);
const max_pred = get_price(rate_max, buy_price);
if (middle_price < min_pred - FUDGE_FACTOR || middle_price > max_pred + FUDGE_FACTOR) {
// Given price is out of predicted range, so this is the wrong pattern
return 0;
}
// TODO: How to deal with probability when there's fudge factor?
// Clamp the value to be in range now so the probability won't be totally biased to fudged values.
const real_rate_range =
rate_range_from_given_and_base(clamp(middle_price, min_pred, max_pred), buy_price);
prob *= range_intersect_length(rate_range, real_rate_range) /
range_length(rate_range);
if (prob == 0) {
return 0;
}
rate_range = range_intersect(rate_range, real_rate_range);
}
const left_price = given_prices[start];
const right_price = given_prices[start + 2];
// Prob(left_price | middle_price), Prob(right_price | middle_price)
//
// A = rate_range[0], B = rate_range[1], C = rate_min, X = rate, Y = randfloat(rate_min, rate)
// rate = randfloat(A, B); sellPrices[work++] = intceil(randfloat(C, rate) * basePrice) - 1;
//
// => X->U(A,B), Y->U(C,X), Y-C->U(0,X-C), Y-C->U(0,1)*(X-C), Y-C->U(0,1)*U(A-C,B-C),
// let Z=Y-C, Z1=A-C, Z2=B-C, Z->U(0,1)*U(Z1,Z2)
// Prob(Z<=t) = integral_{x=0}^{1} [min(t/x,Z2)-min(t/x,Z1)]/ (Z2-Z1)
// let F(t, ZZ) = integral_{x=0}^{1} min(t/x, ZZ)
// 1. if ZZ < t, then min(t/x, ZZ) = ZZ -> F(t, ZZ) = ZZ
// 2. if ZZ >= t, then F(t, ZZ) = integral_{x=0}^{t/ZZ} ZZ + integral_{x=t/ZZ}^{1} t/x
// = t - t log(t/ZZ)
// Prob(Z<=t) = (F(t, Z2) - F(t, Z1)) / (Z2 - Z1)
// Prob(Y<=t) = Prob(Z>=t-C)
for (const price of [left_price, right_price]) {
if (isNaN(price)) {
continue;
}
const min_pred = get_price(rate_min, buy_price) - 1;
const max_pred = get_price(rate_range[1], buy_price) - 1;
if (price < min_pred - FUDGE_FACTOR || price > max_pred + FUDGE_FACTOR) {
// Given price is out of predicted range, so this is the wrong pattern
return 0;
}
// TODO: How to deal with probability when there's fudge factor?
// Clamp the value to be in range now so the probability won't be totally biased to fudged values.
const rate2_range = rate_range_from_given_and_base(clamp(price, min_pred, max_pred)+ 1, buy_price);
const F = (t, ZZ) => {
if (t <= 0) {
return 0;
}
return ZZ < t ? ZZ : t - t * (Math.log(t) - Math.log(ZZ));
};
const [A, B] = rate_range;
const C = rate_min;
const Z1 = A - C;
const Z2 = B - C;
const PY = (t) => (F(t - C, Z2) - F(t - C, Z1)) / (Z2 - Z1);
prob *= PY(rate2_range[1]) - PY(rate2_range[0]);
if (prob == 0) {
return 0;
}
}
// * Then generate the real predicted range.
// We're doing things in different order then how we calculate probability,
// since forward prediction is more useful here.
//
// 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];
}
@ -187,12 +437,8 @@ function generate_peak_price(
// Main spike 2
min_pred = predicted_prices[start].min;
max_pred = intceil(2.0 * buy_price);
max_pred = get_price(rate_max, 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];
}
@ -202,13 +448,9 @@ function generate_peak_price(
});
// Main spike 3
min_pred = intceil(1.4 * buy_price) - 1;
min_pred = get_price(rate_min, 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];
}
@ -217,7 +459,7 @@ function generate_peak_price(
max: max_pred,
});
return true;
return prob;
}
function*
@ -271,31 +513,36 @@ function*
max: buy_price,
},
];
let probability = 1;
// High Phase 1
if (!generate_individual_random_price(
given_prices, predicted_prices, 2, high_phase_1_len, 0.9, 1.4)) {
probability *= generate_individual_random_price(
given_prices, predicted_prices, 2, high_phase_1_len, 0.9, 1.4);
if (probability == 0) {
return;
}
// Dec Phase 1
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)) {
probability *= 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);
if (probability == 0) {
return;
}
// High Phase 2
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)) {
probability *= 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);
if (probability == 0) {
return;
}
// Dec Phase 2
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)) {
probability *= 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);
if (probability == 0) {
return;
}
@ -306,16 +553,17 @@ function*
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)) {
probability *= generate_individual_random_price(
given_prices, predicted_prices, prev_length, 14 - prev_length, 0.9, 1.4);
if (probability == 0) {
return;
}
yield {
pattern_description: "Fluctuating",
pattern_description: i18next.t("patterns.fluctuating"),
pattern_number: 0,
prices: predicted_prices
prices: predicted_prices,
probability,
};
}
@ -330,7 +578,9 @@ function* generate_pattern_0(given_prices) {
for (var dec_phase_1_len = 2; dec_phase_1_len < 4; dec_phase_1_len++) {
for (var high_phase_1_len = 0; high_phase_1_len < 7; high_phase_1_len++) {
for (var high_phase_3_len = 0; high_phase_3_len < (7 - high_phase_1_len - 1 + 1); high_phase_3_len++) {
yield* generate_pattern_0_with_lengths(given_prices, high_phase_1_len, dec_phase_1_len, 7 - high_phase_1_len - high_phase_3_len, 5 - dec_phase_1_len, high_phase_3_len);
yield* multiply_generator_probability(
generate_pattern_0_with_lengths(given_prices, high_phase_1_len, dec_phase_1_len, 7 - high_phase_1_len - high_phase_3_len, 5 - dec_phase_1_len, high_phase_3_len),
1 / (4 - 2) / 7 / (7 - high_phase_1_len));
}
}
}
@ -369,10 +619,11 @@ function* generate_pattern_1_with_peak(given_prices, peak_start) {
max: buy_price,
},
];
let probability = 1;
if (!generate_decreasing_random_price(
given_prices, predicted_prices, 2, peak_start - 2, 0.85, 0.9, 0.03,
0.05)) {
probability *= generate_decreasing_random_price(
given_prices, predicted_prices, 2, peak_start - 2, 0.85, 0.9, 0.03, 0.05);
if (probability == 0) {
return;
}
@ -380,22 +631,24 @@ function* generate_pattern_1_with_peak(given_prices, peak_start) {
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 (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])) {
probability *= generate_individual_random_price(
given_prices, predicted_prices, i, 1, min_randoms[i - peak_start],
max_randoms[i - peak_start]);
if (probability == 0) {
return;
}
}
yield {
pattern_description: "Large spike",
pattern_description: i18next.t("patterns.large-spike"),
pattern_number: 1,
prices: predicted_prices
prices: predicted_prices,
probability,
};
}
function* generate_pattern_1(given_prices) {
for (var peak_start = 3; peak_start < 10; peak_start++) {
yield* generate_pattern_1_with_peak(given_prices, peak_start);
yield* multiply_generator_probability(generate_pattern_1_with_peak(given_prices, peak_start), 1 / (10 - 3));
}
}
@ -424,16 +677,19 @@ function* generate_pattern_2(given_prices) {
max: buy_price,
},
];
let probability = 1;
if (!generate_decreasing_random_price(
given_prices, predicted_prices, 2, 14 - 2, 0.85, 0.9, 0.03, 0.05)) {
probability *= generate_decreasing_random_price(
given_prices, predicted_prices, 2, 14 - 2, 0.85, 0.9, 0.03, 0.05);
if (probability == 0) {
return;
}
yield {
pattern_description: "Decreasing",
pattern_description: i18next.t("patterns.decreasing"),
pattern_number: 2,
prices: predicted_prices
prices: predicted_prices,
probability,
};
}
@ -482,109 +738,128 @@ function* generate_pattern_3_with_peak(given_prices, peak_start) {
];
let probability = 1;
if (!generate_decreasing_random_price(
given_prices, predicted_prices, 2, peak_start - 2, 0.4, 0.9, 0.03,
0.05)) {
probability *= generate_decreasing_random_price(
given_prices, predicted_prices, 2, peak_start - 2, 0.4, 0.9, 0.03, 0.05);
if (probability == 0) {
return;
}
// The peak
if (!generate_individual_random_price(
given_prices, predicted_prices, peak_start, 2, 0.9, 1.4)) {
probability *= generate_individual_random_price(
given_prices, predicted_prices, peak_start, 2, 0.9, 1.4);
if (probability == 0) {
return;
}
if (!generate_peak_price(
given_prices, predicted_prices, peak_start + 2, 1.4, 2.0)) {
probability *= generate_peak_price(
given_prices, predicted_prices, peak_start + 2, 1.4, 2.0);
if (probability == 0) {
return;
}
if (peak_start + 5 < 14) {
if (!generate_decreasing_random_price(
given_prices, predicted_prices, peak_start + 5,
14 - (peak_start + 5), 0.4, 0.9, 0.03, 0.05)) {
probability *= generate_decreasing_random_price(
given_prices, predicted_prices, peak_start + 5, 14 - (peak_start + 5),
0.4, 0.9, 0.03, 0.05);
if (probability == 0) {
return;
}
}
yield {
pattern_description: "Small spike",
pattern_description: i18next.t("patterns.small-spike"),
pattern_number: 3,
prices: predicted_prices
prices: predicted_prices,
probability,
};
}
function* generate_pattern_3(given_prices) {
for (var peak_start = 2; peak_start < 10; peak_start++) {
yield* generate_pattern_3_with_peak(given_prices, peak_start);
for (let peak_start = 2; peak_start < 10; peak_start++) {
yield* multiply_generator_probability(generate_pattern_3_with_peak(given_prices, peak_start), 1 / (10 - 2));
}
}
function* generate_possibilities(sell_prices, first_buy) {
function get_transition_probability(previous_pattern) {
if (typeof previous_pattern === 'undefined' || Number.isNaN(previous_pattern) || previous_pattern === null || previous_pattern < 0 || previous_pattern > 3) {
// TODO: Fill the steady state pattern (https://github.com/mikebryant/ac-nh-turnip-prices/pull/90) here.
return [0.346278, 0.247363, 0.147607, 0.258752];
}
return PROBABILITY_MATRIX[previous_pattern];
}
function* generate_all_patterns(sell_prices, previous_pattern) {
const generate_pattern_fns = [generate_pattern_0, generate_pattern_1, generate_pattern_2, generate_pattern_3];
const transition_probability = get_transition_probability(previous_pattern);
for (let i = 0; i < 4; i++) {
yield* multiply_generator_probability(generate_pattern_fns[i](sell_prices), transition_probability[i]);
}
}
function* generate_possibilities(sell_prices, first_buy, previous_pattern) {
if (first_buy || isNaN(sell_prices[0])) {
for (var buy_price = 90; buy_price <= 110; buy_price++) {
sell_prices[0] = sell_prices[1] = buy_price;
if (first_buy) {
yield* generate_pattern_3(sell_prices);
} else {
yield* generate_pattern_0(sell_prices);
yield* generate_pattern_1(sell_prices);
yield* generate_pattern_2(sell_prices);
yield* generate_pattern_3(sell_prices);
// All buy prices are equal probability and we're at the outmost layer,
// so don't need to multiply_generator_probability here.
yield* generate_all_patterns(sell_prices, previous_pattern)
}
}
} else {
yield* generate_pattern_0(sell_prices);
yield* generate_pattern_1(sell_prices);
yield* generate_pattern_2(sell_prices);
yield* generate_pattern_3(sell_prices);
yield* generate_all_patterns(sell_prices, previous_pattern)
}
}
function row_probability(possibility, previous_pattern) {
return PROBABILITY_MATRIX[previous_pattern][possibility.pattern_number] / PATTERN_COUNTS[possibility.pattern_number];
}
function analyze_possibilities(sell_prices, first_buy, previous_pattern) {
const generated_possibilities = Array.from(generate_possibilities(sell_prices, first_buy, previous_pattern));
console.log(generated_possibilities);
function get_probabilities(possibilities, previous_pattern) {
if (typeof previous_pattern === 'undefined' || Number.isNaN(previous_pattern) || previous_pattern === null || previous_pattern < 0 || previous_pattern > 3) {
return possibilities
const total_probability = generated_possibilities.reduce((acc, it) => acc + it.probability, 0);
for (const it of generated_possibilities) {
it.probability /= total_probability;
}
var max_percent = possibilities.map(function (poss) {
return row_probability(poss, previous_pattern);
}).reduce(function (prev, current) {
return prev + current;
}, 0);
return possibilities.map(function (poss) {
poss.probability = row_probability(poss, previous_pattern) / max_percent;
return poss;
});
}
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);
// Check for a future date by checking for a range of prices
if(day.min !== day.max){
weekMins.push(day.min);
weekMaxes.push(day.max);
} else {
// If we find a set price after one or more ranged prices, the user has missed a day. Discard that data and start again.
weekMins = [];
weekMaxes = [];
}
}
if (!weekMins.length && !weekMaxes.length) {
weekMins.push(poss.prices[poss.prices.length -1].min);
weekMaxes.push(poss.prices[poss.prices.length -1].max);
}
poss.weekGuaranteedMinimum = Math.max(...weekMins);
poss.weekMax = Math.max(...weekMaxes);
}
category_totals = {}
for (let i of [0, 1, 2, 3]) {
category_totals[i] = generated_possibilities
.filter(value => value.pattern_number == i)
.map(value => value.probability)
.reduce((previous, current) => previous + current, 0);
}
for (let pos of generated_possibilities) {
pos.category_total_probability = category_totals[pos.pattern_number];
}
generated_possibilities.sort((a, b) => {
if (a.weekMax < b.weekMax) {
return 1;
} else if (a.weekMax > b.weekMax) {
return -1;
} else {
return 0;
}
return b.category_total_probability - a.category_total_probability || b.probability - a.probability;
});
global_min_max = [];
@ -605,11 +880,11 @@ function analyze_possibilities(sell_prices, first_buy, previous_pattern) {
}
generated_possibilities.unshift({
pattern_description: "All patterns",
pattern_description: i18next.t("patterns.all"),
pattern_number: 4,
prices: global_min_max,
weekGuaranteedMinimum: Math.min(...generated_possibilities.map(poss => poss.weekGuaranteedMinimum)),
weekMax: Math.max(...generated_possibilities.map(poss => poss.weekMax)),
weekMax: Math.max(...generated_possibilities.map(poss => poss.weekMax))
});
return generated_possibilities;

@ -40,6 +40,9 @@ const sell_inputs = getSellFields()
const buy_input = $("#buy")
const first_buy_radios = getFirstBuyRadios()
const previous_pattern_radios = getPreviousPatternRadios()
const permalink_input = $('#permalink-input')
const permalink_button = $('#permalink-btn')
const snackbar = $('#snackbar')
//Functions
const fillFields = function (prices, first_buy, previous_pattern) {
@ -74,15 +77,20 @@ const initialize = function () {
} else {
fillFields(prices, first_buy, previous_pattern)
}
$(document).trigger("input");
} catch (e) {
console.error(e);
}
$(document).trigger("input");
$("#permalink-btn").on("click", copyPermalink)
$("#reset").on("click", function () {
sell_inputs.forEach(input => input.value = '')
fillFields([], false, -1)
update()
if (window.confirm(i18next.t("prices.reset-warning"))) {
sell_inputs.forEach(input => input.value = '')
fillFields([], false, -1)
update()
}
})
}
@ -258,13 +266,22 @@ const calculateOutput = function (data, first_buy, previous_pattern) {
}
let output_possibilities = "";
let analyzed_possibilities = analyze_possibilities(data, first_buy, previous_pattern);
previous_pattern_number = ""
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>`;
if (previous_pattern_number != poss.pattern_number) {
previous_pattern_number = poss.pattern_number
pattern_count = analyzed_possibilities
.filter(val => val.pattern_number == poss.pattern_number)
.length
percentage_display = percent => Number.isFinite(percent) ? ((percent * 100).toPrecision(3) + '%') : '—'
out_line += `<td rowspan=${pattern_count}>${percentage_display(poss.category_total_probability)}</td>`;
}
out_line += `<td>${percentage_display(poss.probability)}</td>`;
for (let day of poss.prices.slice(1)) {
var price_class = getPriceClass(day.max);
if (day.min !== day.max) {
out_line += `<td class='${price_class}'>${day.min} to ${day.max}</td>`;
out_line += `<td class='${price_class}'>${day.min} ${i18next.t("output.to")} ${day.max}</td>`;
} else {
out_line += `<td class='${price_class}'>${day.min}</td>`;
}
@ -280,6 +297,53 @@ const calculateOutput = function (data, first_buy, previous_pattern) {
update_chart(data, analyzed_possibilities);
}
const generatePermalink = function (buy_price, sell_prices, first_buy, previous_pattern) {
let searchParams = new URLSearchParams();
let pricesParam = buy_price ? buy_price.toString() : '';
if (!isEmpty(sell_prices)) {
const filtered = sell_prices.map(price => isNaN(price) ? '' : price).join('.');
pricesParam = pricesParam.concat('.', filtered);
}
if (pricesParam) {
searchParams.append('prices', pricesParam);
}
if (first_buy) {
searchParams.append('first', true);
}
if (previous_pattern !== -1) {
searchParams.append('pattern', previous_pattern);
}
return searchParams.toString() && window.location.origin.concat('?', searchParams.toString());
}
const copyPermalink = function () {
let text = permalink_input[0];
permalink_input.show();
text.select();
text.setSelectionRange(0, 99999); /* for mobile devices */
document.execCommand('copy');
permalink_input.hide();
flashMessage(i18next.t("prices.permalink-copied"));
}
const flashMessage = function(message) {
snackbar.text(message);
snackbar.addClass('show');
setTimeout(function () {
snackbar.removeClass('show')
snackbar.text('');
}, 3000);
}
const update = function () {
const sell_prices = getSellPrices();
const buy_price = parseInt(buy_input.val());
@ -289,6 +353,14 @@ const update = function () {
buy_input[0].disabled = first_buy;
buy_input[0].placeholder = first_buy ? '—' : '...'
const permalink = generatePermalink(buy_price, sell_prices, first_buy, previous_pattern);
if (permalink) {
permalink_button.show();
} else {
permalink_button.hide();
}
permalink_input.val(permalink);
const prices = [buy_price, buy_price, ...sell_prices];
if (!window.populated_from_query) {
@ -297,7 +369,3 @@ const update = function () {
calculateOutput(prices, first_buy, previous_pattern);
}
$(document).ready(initialize);
$(document).on("input", update);
$('input[type = radio]').on("change", update);

@ -0,0 +1,52 @@
function updateContent() {
update();
$('body').localize();
}
const defaultLanguage = 'en';
i18next
.use(i18nextXHRBackend)
.use(i18nextBrowserLanguageDetector)
.init({
fallbackLng: defaultLanguage,
debug: true,
backend: {
loadPath: 'locales/{{lng}}.json',
},
}, (err, t) => {
const languages = [
['de', 'Deutsch'],
['en', 'English'],
['es-ES', 'Español'],
['fr', 'Français'],
['it', 'Italiano'],
['ru', 'Русский'],
['ja', '日本語'],
['pt-BR', 'Português'],
['zh-CN', '简体中文'],
['zh-TW', '繁體中文']
],
languageSelector = $('#language');
languages.map(([code, name]) => {
languageSelector.append(`<option value="${code}"${code == i18next.language ? ' selected' : ''}>${name}</option>`);
});
if (!languageSelector.find('[selected]').length)
languageSelector.val(defaultLanguage);
languageSelector.on('change', function () {
if (this.value == i18next.language)
return;
i18next.changeLanguage(this.value);
});
jqueryI18next.init(i18next, $);
i18next.on('languageChanged', lng => {
if (!languageSelector.find(`[value=${lng}]`).length) {
i18next.changeLanguage(defaultLanguage);
return;
}
languageSelector.val(lng);
updateContent();
});
// init set content
$(document).ready(initialize);
$(document).on('input', updateContent);
$('input[type = radio]').on('change', updateContent);
});

@ -0,0 +1,80 @@
{
"general": {
"daisy-mae": "Jorna"
},
"welcome": {
"salutation": "Hallo, und Willkommen bei der <b>Turnip Prophet</b> App auf deinem Nook Phone.",
"description": "Mit dieser App kannst du die Rübenpreise deiner Insel täglich verfolgen, aber du musst die Preise selbst eingeben!",
"conclusion": "Danach wird die Turnip Prophet App <b>magisch</b> deine Rübenpreise vorhersagen, welche du den Rest der Woche haben wirst."
},
"first-time": {
"title": "Erstmaliger Einkäufer",
"description": "Kaufst du zum ersten Mal Rüben von Jorna auf deiner Insel? <i>(Dies beeinflusst dein Verkaufsmuster)</i>",
"yes": "Ja",
"no": "Nein"
},
"patterns": {
"title": "Vorheriges Verkaufsmuster",
"description": "Wie war das Verkaufsmuster der letzten Woche? <i>(Dies beeinflusst dein Verkaufsmuster)</i>",
"pattern": "Verkaufsmuster",
"all": "Alle Verkaufsmuster",
"decreasing": "Absteigend",
"fluctuating": "Schwankend",
"unknown": "Ich weiß nicht",
"large-spike": "Stark Ansteigend",
"small-spike": "Leicht Ansteigend"
},
"prices": {
"description": "Wie hoch war der Preis für Rüben diese Woche auf deiner Insel? <i>(Wenn du zum ersten Mal Rüben kaufst, wird dieses Feld deaktiviert)</i>",
"open": {
"am": "Vorm. (Vormittag) - 8:00 Uhr bis 11:59 Uhr",
"pm": "Nachm. (Nachmittag) - 12:00 Uhr bis 22:00 Uhr"
},
"copy-permalink": "Seite teilen",
"permalink-copied": "Seitenlink kopiert!",
"reset": "Eingegebene Daten zurücksetzen",
"reset-warning": "Bist du sicher, dass du deine eingegebenen Daten zurücksetzen möchtest?\n\nDies kann nicht rückgängig gemacht werden!"
},
"weekdays": {
"monday": "Montag",
"tuesday": "Dienstag",
"wednesday": "Mittwoch",
"thursday": "Donnerstag",
"friday": "Freitag",
"saturday" : "Samstag",
"sunday": "Sonntag",
"abr": {
"monday": "Mo",
"tuesday": "Di",
"wednesday": "Mi",
"thursday": "Do",
"friday": "Fr",
"saturday" : "Sa"
}
},
"times": {
"morning": "Vorm.",
"afternoon": "Nachm."
},
"output": {
"title": "Berechnung",
"chance": "% Chance",
"to": "bis",
"minimum": "Garantiertes Minimum",
"maximum": "Potentielles Maximum",
"chart": {
"input": "Eingegebener Preis",
"minimum": "Garantiertes Minimum",
"maximum": "Potentielles Maximum"
}
},
"textbox": {
"description": "Nachdem du einige Rübenpreise eingegeben hast, macht der Turnip Prophet etwas Magie und zeigt dir die verschiedenen möglichen Verkaufsmuster an, die auf deiner Insel auftreten können.",
"development": "Diese App befindet sich noch in der Entwicklung, wird sich aber mit der Zeit verbessern!",
"thanks": "Nichts von all dem wäre möglich gewesen, ohne das <a href=\"https://twitter.com/_Ninji/status/1244818665851289602?s=20\">Ninji</a> herausgefunden hätte, wie Nepp und Schlepp ihre Rübenpreise kalkulieren.",
"support": "Hilfe, Kommentare und Beiträge sind auffindbar über <a href=\"https://github.com/mikebryant/ac-nh-turnip-prices/issues\">GitHub.com</a> (nur in Englisch).",
"contributors-text": "Oh! Und vergessen wir nicht, denen zu danken, die bis jetzt dazu beigetragen haben!",
"contributors": "Mitwirkende",
"language": "Sprache"
}
}

@ -0,0 +1,80 @@
{
"general": {
"daisy-mae": "Daisy Mae"
},
"welcome": {
"salutation": "Hello, and welcome to the <b>Turnip Prophet</b> app on your Nook Phone.",
"description": "This app lets you track your island's turnip prices daily, but you'll have to put the prices in yourself!",
"conclusion": "After that, the Turnip Prophet app will <b>magically</b> predict the turnip prices you'll have for the rest of the week."
},
"first-time": {
"title": "First-Time Buyer",
"description": "Is this your first time buying turnips from Daisy Mae on your island?<i>(This affects your pattern)</i>",
"yes": "Yes",
"no": "No"
},
"patterns": {
"title": "Previous Pattern",
"description": "What was last week's turnip price pattern?<i>(This affects your pattern)</i>",
"pattern": "Pattern",
"all": "All patterns",
"decreasing": "Decreasing",
"fluctuating": "Fluctuating",
"unknown": "I don't know",
"large-spike": "Large Spike",
"small-spike": "Small Spike"
},
"prices": {
"description": "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>",
"open": {
"am": "AM - 8:00 am to 11:59 am",
"pm": "PM - 12:00 pm to 10:00 pm"
},
"copy-permalink": "Copy permalink",
"permalink-copied": "Permalink copied!",
"reset": "Reset Turnip Prophet",
"reset-warning": "Are you sure you want to reset all fields?\n\nThis cannot be undone!"
},
"weekdays": {
"monday": "Monday",
"tuesday": "Tuesday",
"wednesday": "Wednesday",
"thursday": "Thursday",
"friday": "Friday",
"saturday" : "Saturday",
"sunday": "Sunday",
"abr": {
"monday": "Mon",
"tuesday": "Tue",
"wednesday": "Wed",
"thursday": "Thu",
"friday": "Fri",
"saturday" : "Sat"
}
},
"times": {
"morning": "AM",
"afternoon": "PM"
},
"output": {
"title": "Output",
"chance": "% Chance",
"to": "to",
"minimum": "Guaranteed Minimum",
"maximum": "Potential Maximum",
"chart": {
"input": "Input Price",
"minimum": "Guaranteed Minimum",
"maximum": "Potential Maximum"
}
},
"textbox": {
"description": "After you've listed some turnip prices, the Turnip Prophet will run some numbers and display the different possible patterns that your island may experience.",
"development": "This app is still in development, but will improve over time!",
"thanks": "None of this would have been possible without <a href=\"https://twitter.com/_Ninji/status/1244818665851289602?s=20\">Ninji's work</a> figuring out just how Timmy and Tommy value their turnips.",
"support": "Support, comments and contributions are available through <a href=\"https://github.com/mikebryant/ac-nh-turnip-prices/issues\">Github</a>",
"contributors-text": "Oh! And let's not forget to thank those who have contributed so far!",
"contributors": "Contributors",
"language": "Language"
}
}

@ -0,0 +1,80 @@
{
"general": {
"daisy-mae": "Juliana"
},
"welcome": {
"salutation": "¡Hola! Te damos la bienvenida a la aplicación <b>Turnip Prophet</b> para tu Nookófono.",
"description": "Esta aplicación te permite monitorizar la fluctuación del precio de los nabos en tu isla, ¡pero tendrás que introducir tú manualmente los precios diarios!",
"conclusion": "Una vez hecho, la aplicación Turnip Prophet predecirá <b>mágicamente</b> el precio que tendrán los nabos el resto de la semana."
},
"first-time": {
"title": "Comprador(a) primerizo(a)",
"description": "¿Ha sido esta la primera vez que compras nabos?<i>(Esta información afectará a tu patrón)</i>",
"yes": "Sí",
"no": "No"
},
"patterns": {
"title": "Patrón anterior",
"description": "¿Qué patrón describió el precio de los nabos la semana pasada?<i>(Esta información afectará a tu patrón)</i>",
"pattern": "Patrón",
"all": "Todos los patrones",
"decreasing": "Decreciente",
"fluctuating": "Fluctuante",
"unknown": "No lo sé",
"large-spike": "Pico alto",
"small-spike": "Pico moderado"
},
"prices": {
"description": "¿Cuál fue el precio más alto que alcanzaron los nabos en tu isla durante la semana? <i>(Si esta ha sido tu primera vez comprando nabos, este campo se desactivará)</i>",
"open": {
"am": "AM - De 8:00 a 11:59",
"pm": "PM - De 12:00 a 10:00"
},
"copy-permalink": "Copiar permalink",
"permalink-copied": "¡Permalink copiado!",
"reset": "Reiniciar Turnip Prophet",
"reset-warning": "¿Seguro que quieres reiniciar todos los campos?\n\n¡Esto no se puede deshacer!"
},
"weekdays": {
"monday": "Lunes",
"tuesday": "Martes",
"wednesday": "Miércoles",
"thursday": "Jueves",
"friday": "Viernes",
"saturday" : "Sábado",
"sunday": "Domingo",
"abr": {
"monday": "LU",
"tuesday": "MA",
"wednesday": "MI",
"thursday": "JU",
"friday": "VI",
"saturday" : "SA"
}
},
"times": {
"morning": "AM",
"afternoon": "PM"
},
"output": {
"title": "Predicción",
"chance": "Probabilidad (%)",
"to": "a",
"minimum": "Mínimo garantizado",
"maximum": "Máximo potencial",
"chart": {
"input": "Precio de entrada",
"minimum": "Mínimo garantizado",
"maximum": "´Máximo potencial"
}
},
"textbox": {
"description": "Cuando introduzcas algunos precios, el Turnip Prophet empezará a hacer sus cálculos y te mostrará algunos posibles patrones para el precio de los nabos en tu isla.",
"development": "Esta aplicación está aún en desarrollo, ¡pero la seguiremos mejorando!",
"thanks": "Nada de esto habría sido posible sin el <a href=\"https://twitter.com/_Ninji/status/1244818665851289602?s=20\">trabajo de Ninji</a> para averiguar cómo Tendo y Nendo valoran los nabos.",
"support": "Para asistencia, comentarios y contribuciones, no dudes en pasarte por <a href=\"https://github.com/mikebryant/ac-nh-turnip-prices/issues\">Github</a>.",
"contributors-text": "¡Ah! ¡Y no nos olvidemos de todos los que han puesto su granito de arena hasta ahora!",
"contributors": "Contribuidores",
"language": "Idioma"
}
}

@ -0,0 +1,80 @@
{
"general": {
"daisy-mae": "Porcelette"
},
"welcome": {
"salutation": "Bonjour et bienvenue sur l'application du <b>Turnip Prophet</b> de ton Nook Phone.",
"description": "Cette appli te permet de garder un œil quotidien sur le cours du navet de ton île, en le renseignant ici par toi-même !",
"conclusion": "Une fois les prix renseignés, l'appli du Turnip Prophet va <b>magiquement</b> prédire le cours du navet de ton île pour le reste de la semaine."
},
"first-time": {
"title": "Premier Achat",
"description": "Est-ce la toute première fois que t'achètes des navets à Porcelette sur ton île ?<i>(Cela influencera le type de ta courbe actuelle)</i>",
"yes": "Oui",
"no": "Non"
},
"patterns": {
"title": "Courbe du cours précédent",
"description": "De quel type était la courbe de ton cours du navet la semaine dernière ?<i>(Cela influencera le type de ta courbe actuelle)</i>",
"pattern": "Type de Courbe",
"all": "Tous les types",
"decreasing": "Décroissante",
"fluctuating": "Variable",
"unknown": "Je ne sais pas",
"large-spike": "Grand Pic",
"small-spike": "Petit Pic"
},
"prices": {
"description": "À quel prix Porcelette vendait ses navets sur ton île cette semaine ?<i>(Si c'est la première fois que t'en achètes, ce champ sera desactivé)</i>",
"open": {
"am": "Matin (AM) - de 8:00 à 11:59",
"pm": "Après-midi (PM) - de 12:00 à 22:00"
},
"copy-permalink": "Copier le permalien",
"permalink-copied": "Permalien copié !",
"reset": "Réinitialiser les données",
"reset-warning": "Es-tu sûr·e de vouloir réinitialiser tous les champs ?\n\nCe choix est définitif !"
},
"weekdays": {
"monday": "Lundi",
"tuesday": "Mardi",
"wednesday": "Mercredi",
"thursday": "Jeudi",
"friday": "Vendredi",
"saturday" : "Samedi",
"sunday": "Dimanche",
"abr": {
"monday": "Lun",
"tuesday": "Mar",
"wednesday": "Mer",
"thursday": "Jeu",
"friday": "Ven",
"saturday" : "Sam"
}
},
"times": {
"morning": "AM",
"afternoon": "PM"
},
"output": {
"title": "Résultats",
"chance": "% Chance",
"to": "à",
"minimum": "Minimum Garanti",
"maximum": "Maximum Potentiel",
"chart": {
"input": "Prix renseigné",
"minimum": "Minimum Garanti",
"maximum": "Maximum Potentiel"
}
},
"textbox": {
"description": "Après avoir renseigné quelques prix, le Turnip Prophet fera des calculs et affichera les différents types possibles pour les courbes que ton île pourrait avoir.",
"development": "Cette appli est en développement, mais elle s'améliore jour après jour !",
"thanks": "Rien de tout ça n'aurait pu être possible sans <a href=\"https://twitter.com/_Ninji/status/1244818665851289602?s=20\">le travail de Ninji</a> et son analyse sur comment Méli et Mélo déterminent la valeur des Navets.",
"support": "Aide, commentaires et contributions sont disponibles via <a href=\"https://github.com/mikebryant/ac-nh-turnip-prices/issues\">Github</a>",
"contributors-text": "Oh ! Et n'oublions pas de remercier toutes les personnes ayant contribué jusqu'à maintenant !",
"contributors": "Contributeurs",
"language": "Langue"
}
}

@ -0,0 +1,80 @@
{
"general": {
"name": "Turnip Prophet",
"daisy-mae": "Brunella"
},
"welcome": {
"salutation": "Ciao e benvenuto nell'app <b>Turnip Prophet</b> del tuo Nook Phone.",
"description": "Questa applicazione ti permetterà di tenere traccia del prezzo giornaliero delle rape. Ma dovrai inserire i prezzi da te!",
"conclusion": "Se lo fai Turnip Prophet predirrà <b>magicamente</b> i prezzi delle rape che avrai per il resto della settimana."
},
"first-time": {
"title": "Primo Acquisto",
"description": "È la prima volta che acquisti sulla tua isola le rape da Brunella?<i>(influisce sul comportamento dei prezzi)</i>",
"yes": "Sì",
"no": "No"
},
"patterns": {
"title": "Comportamento Precedente",
"description": "Qual è stato il comportamento dei prezzi delle rape nella scorsa settimana?<i>(influisce sul comportamento dei prezzi)</i>",
"pattern": "Comportamento",
"all": "Tutti i comportamenti",
"decreasing": "Decrescente",
"fluctuating": "Oscillante",
"unknown": "Non lo so!",
"large-spike": "Grande picco",
"small-spike": "Piccolo picco"
},
"prices": {
"description": "Qual era il prezzo di acquisto delle rape sulla tua isola questa settimana? <i>(Se è la prima volta che le compri questo campo è disabilitato)</i>",
"open": {
"am": "Mattina - dalle 8:00 alle 11:59",
"pm": "Pomeriggio - dalle 12:00 alle 22:00"
},
"copy-permalink": "Copia permalink",
"permalink-copied": "Permalink copiato!",
"reset": "Resetta Turnip Prophet",
"reset-warning": "Sei sicuro di voler resettare tutti i campi?\n\nNon può essere annullato!"
},
"weekdays": {
"monday": "Lunedì",
"tuesday": "Martedì",
"wednesday": "Mercoledì",
"thursday": "Giovedì",
"friday": "Venerdì",
"saturday": "Sabato",
"sunday": "Domenica",
"abr": {
"monday": "Lun",
"tuesday": "Mar",
"wednesday": "Mer",
"thursday": "Gio",
"friday": "Ven",
"saturday": "Sab"
}
},
"times": {
"morning": "AM",
"afternoon": "PM"
},
"output": {
"title": "Risultati",
"chance": "Probabilità %",
"to": "a",
"minimum": "Minimo Garantito",
"maximum": "Massimo Potenziale",
"chart": {
"input": "Prezzo Iniziale",
"minimum": "Minimo Gatantito",
"maximum": "Massimo Potenziale"
}
},
"textbox": {
"description": "Dopo aver inserito alcuni prezzi, Turnip Prophet calcolerà e mostrerà i possibili comportamenti del prezzo delle rape nella tua isola.",
"development": "Quest'applicazione è in ancora in sviluppo ma migliorerà col tempo!",
"thanks": "Niente di questo sarebbe possibile senza <a href=\"https://twitter.com/_Ninji/status/1244818665851289602?s=20\">il lavoro di Ninji</a> nello scoprire come Mirko e Marco valutano le rape.",
"support": "Chiedi supporto o lascia commenti e contributi su <a href=\"https://github.com/mikebryant/ac-nh-turnip-prices/issues\">Github</a>",
"contributors-text": "Oh! Non dimentichiamoci di ringraziare chi ha contribuito fin'ora!",
"contributors": "Collaboratori"
}
}

@ -0,0 +1,79 @@
{
"general": {
"daisy-mae": "ウリ"
},
"welcome": {
"salutation": "こんにちは!タヌキ開発特製スマホの最新アプリ <b>Turnip Prophet</b> へようこそ。",
"description": "このアプリは、無人島のカブ価を毎日予測することができます。そのためには、まず自分でデータを入力することが必要です!",
"conclusion": "そうすれば、Turnip Prophetは<b>魔法</b>のように、あなたのこの一週間のカブ価を予測します。"
},
"first-time": {
"title": "はじめての購入",
"description": "あなたが自分の島でカブを購入したのは今回が初めてですか?<i>(答えによってパターンが変化します)</i>",
"yes": "はい",
"no": "いいえ"
},
"patterns": {
"title": "先週のパターン",
"description": "先週のカブ価変化パターンを選んでください。<i>(答えによってパターンが変化します)</i>",
"pattern": "パターン",
"all": "全てのパターン",
"decreasing": "ジリ貧型",
"fluctuating": "波型",
"unknown": "わかりません",
"large-spike": "跳ね大型(3期型)",
"small-spike": "跳ね小型(4期型)"
},
"prices": {
"description": "今週のカブ価は? <i>(初めてカブを購入した場合は、この入力欄は無効にされています)</i>",
"open": {
"am": "午前 - AM 8:00 AM 11:59",
"pm": "午後 - PM 12:00 PM 10:00"
},
"copy-permalink": "パーマリンクをコピー",
"permalink-copied": "パーマリンクがコピーされました!",
"reset": "Turnip Prophetをリセット",
"reset-warning": "本当に全てをリセットしますか?\n\nリセットしたら後戻りはできませんよ"
},
"weekdays": {
"monday": "月曜日",
"tuesday": "火曜日",
"wednesday": "水曜日",
"thursday": "木曜日",
"friday": "金曜日",
"saturday" : "土曜日",
"sunday": "日曜日",
"abr": {
"monday": "月",
"tuesday": "火",
"wednesday": "水",
"thursday": "木",
"friday": "金",
"saturday" : "土"
}
},
"times": {
"morning": "午前",
"afternoon": "午後"
},
"output": {
"title": "結果",
"chance": "% チャンス",
"to": "",
"minimum": "保証される最小の収入",
"maximum": "予測される限界の収入",
"chart": {
"input": "カブ価",
"minimum": "保証される最小の収入",
"maximum": "予測される限界の収入"
}
},
"textbox": {
"description": "複数のカブ価を入力すると、Turnip Prophetはそれをもとに、可能性のある変動パターンを計算して表示します。",
"development": "このアプリはまだ開発中ですが、より良いデータを提供できるようにがんばっています!",
"thanks": "このアプリが作成できたのは、タヌキ商店のカブ価パターンを解析した<a href=\"https://twitter.com/_Ninji/status/1244818665851289602?s=20\">Ninji氏の成果</a>のおかげです。ありがとうございます!",
"support": "疑問·コメント·提案などは、<a href=\"https://github.com/mikebryant/ac-nh-turnip-prices/issues\">Github</a>までお願いします。",
"contributors-text": "そして、このアプリの作成を手伝っていただいた方々に感謝します!",
"contributors": "貢献者"
}
}

@ -0,0 +1,80 @@
{
"general": {
"daisy-mae": "Daisy Mae"
},
"welcome": {
"salutation": "Olá, e bem-vindo ao aplicativo <b>Turnip Prophet</b> em seu Nook Phone.",
"description": "Este aplicativo lhe permite acompanhar os preços diários de nabo em sua ilha, porém você terá que colocar os preços você mesmo!",
"conclusion": "Depois disso, o aplicativo Turnip Prophet irá <b>magicamente</b> prever os preços dos nabos que você terá pelo resto da semana."
},
"first-time": {
"title": "Comprador de primeira viagem",
"description": "Esta é a primeira vez que você compra nabos de Daisy Mae em sua ilha? <i>(Isso afeta seu padrão)</i>",
"yes": "Sim",
"no": "Não"
},
"patterns": {
"title": "Padrão Anterior",
"description": "Qual foi o padrão de preços de nabo da semana passada? <i>(Isso afeta seu padrão)</i>",
"pattern": "Padrão",
"all": "Todos padrões",
"decreasing": "Diminuindo",
"fluctuating": "Flutuante",
"unknown": "Eu não sei",
"large-spike": "Grande Pico",
"small-spike": "Pequeno Pico"
},
"prices": {
"description": "Qual foi o preço dos nabos esta semana em sua ilha? <i>(Se esta é a primeira vez que compra nabos, este campo ficará desativado)</i>",
"open": {
"am": "AM - 8:00 am até 11:59 am",
"pm": "PM - 12:00 pm até 10:00 pm"
},
"copy-permalink": "Copiar permalink",
"permalink-copied": "Permalink copiado!",
"reset": "Redefinir Turnip Prophet",
"reset-warning": "Tem certeza de que deseja redefinir todos os campos?\n\nIsso não pode ser desfeito!"
},
"weekdays": {
"monday": "Segunda-feira",
"tuesday": "Terça-feira",
"wednesday": "Quarta-feira",
"thursday": "Quinta-feira",
"friday": "Sexta-feira",
"saturday" : "Sábado",
"sunday": "Domingo",
"abr": {
"monday": "Seg",
"tuesday": "Ter",
"wednesday": "Qua",
"thursday": "Qui",
"friday": "Sex",
"saturday" : "Sab"
}
},
"times": {
"morning": "AM",
"afternoon": "PM"
},
"output": {
"title": "Resultado",
"chance": "% Chance",
"to": "to",
"minimum": "Mínimo Garantido",
"maximum": "Potencial Máximo",
"chart": {
"input": "Preço de entrada",
"minimum": "Mínimo Garantido",
"maximum": "Potencial Máximo"
}
},
"textbox": {
"description": "Depois de listar alguns preços de nabo, o Turnip Prophet irá calcular alguns números e exibirá os diferentes padrões possíveis que sua ilha pode experienciar.",
"development": "Este aplicativo ainda está em desenvolvimento, mas melhorará com o tempo!",
"thanks": "Nada disso seria possível sem o <a href=\"https://twitter.com/_Ninji/status/1244818665851289602?s=20\">trabalho de Ninji's</a> descobrindo como Timmy e Tommy valorizam seus nabos.",
"support": "Suporte, comentários e contribuições estão disponíveis através do <a href=\"https://github.com/mikebryant/ac-nh-turnip-prices/issues\">Github</a>",
"contributors-text": "Oh! E não vamos nos esquecer de agradecer àqueles que contribuíram até agora!",
"contributors": "Contribuidores",
"language": "Linguagem"
}
}

@ -0,0 +1,80 @@
{
"general": {
"daisy-mae": "Дейзи Мэй"
},
"welcome": {
"salutation": "Добрый день! Добро пожаловать в приложение <b>Препсказатель</b> на Вашем Нукофоне.",
"description": "Это приложение позволяет Вам ежедневно отслеживать стоимость репы на Вашем острове. Будьте внимательны, Вам придется вводить ее вручную!",
"conclusion": "После этого Препсказатель <b>как по волшебству</b> предскажет стоимость репы в Вашем магазине на этой неделе."
},
"first-time": {
"title": "Новичок рынка репы",
"description": "Вы впервые приобретаете репу у Дейзи Мэй на своем острове?<i>(Это влияет на модель стоимости)</i>",
"yes": "Да",
"no": "Нет"
},
"patterns": {
"title": "Предыдущая модель",
"description": "Какая модель стоимости репы была у Вас на прошлой неделе?<i>(Это влияет на модель стоимости)</i>",
"pattern": "Модель",
"all": "Все модели",
"decreasing": "Постоянное снижение",
"fluctuating": "Колебание стоимости",
"unknown": "Не могу сказать точно",
"large-spike": "Большой скачок",
"small-spike": "Малый скачок"
},
"prices": {
"description": "Какова была стоимость репы у Дейзи Мэй на Вашем острове на этой неделе? <i>(Если вы новичок рынка репы, данное поле будет недоступно)</i>",
"open": {
"am": "Утро - 8:00 - 11:59",
"pm": "День - 12:00 - 22:00"
},
"copy-permalink": "Скопировать постоянную ссылку",
"permalink-copied": "Постоянная ссылка скопирована!",
"reset": "Перезагрузить Препсказателя",
"reset-warning": "Вы уверены, что хотите обнулить все поля?\n\nДанное действие необратимо!"
},
"weekdays": {
"monday": "Понедельник",
"tuesday": "Вторник",
"wednesday": "Среда",
"thursday": "Четверг",
"friday": "Пятница",
"saturday" : "Суббота",
"sunday": "Воскресенье",
"abr": {
"monday": "Пн",
"tuesday": "Вт",
"wednesday": "Ср",
"thursday": "Чт",
"friday": "Пт",
"saturday" : "Сб"
}
},
"times": {
"morning": "Утро",
"afternoon": "День"
},
"output": {
"title": "Вывод модели",
"chance": "Вероятность в %",
"to": "-",
"minimum": "Гарантированный минимум",
"maximum": "Возможный максимум",
"chart": {
"input": "Введенная стоимость",
"minimum": "Гарантированный минимум",
"maximum": "Возможный максимум"
}
},
"textbox": {
"description": "После того, как Вы введете уже известную Вам стоимость репы, Препсказатель посчитает и покажет различные вероятные для Вашего острова модели стоимости репы.",
"development": "Это приложение пока еще в разработке, но со временем обязательно станет лучше!",
"thanks": "Если бы <a href=\"https://twitter.com/_Ninji/status/1244818665851289602?s=20\">Ninji</a> не смог разузнать, как Тимми и Томми определяют стоимость репы, данного приложения могло бы и не быть!",
"support": "Поддержать нас, прокомментировать, а также внести свой вклад Вы можете на <a href=\"https://github.com/mikebryant/ac-nh-turnip-prices/issues\">Github</a>",
"contributors-text": "Точно! Нельзя забывать тех, кто уже помог проекту!",
"contributors": "Вклад внесли",
"language": "Язык"
}
}

@ -0,0 +1,80 @@
{
"general": {
"daisy-mae": "曹卖"
},
"welcome": {
"salutation": "大家好欢迎使用Nook手机上的<b>大头菜预测工具</b>。",
"description": "这个APP可以让你每天跟踪自己岛上大头菜的价格但你得自己把价格填写进去",
"conclusion": "之后,大头菜预测工具会<b>神奇地</b>预测出本周剩余时间的大头菜价格。"
},
"first-time": {
"title": "首次购买",
"description": "你是第一次在自己岛上购买大头菜吗?<i>(将影响预测趋势)</i>",
"yes": "是",
"no": "否"
},
"patterns": {
"title": "上周趋势",
"description": "上周大头菜的价格趋势是?<i>(将影响预测趋势)</i>",
"pattern": "趋势",
"all": "所有趋势",
"decreasing": "递减型",
"fluctuating": "波动型",
"unknown": "不知道",
"large-spike": "大幅上涨(三期型)",
"small-spike": "小幅上涨(四期型)"
},
"prices": {
"description": "本周你的岛上大头菜的购买价格是多少?<i>(如果你是第一次购买大头菜,这个字段不可用)</i>",
"open": {
"am": "上午 - 8:00 ~ 11:59",
"pm": "下午 - 12:00 ~ 22:00"
},
"copy-permalink": "复制价格分享链接",
"permalink-copied": "链接已复制!",
"reset": "重置大头菜预测工具",
"reset-warning": "你确定要重置所有字段吗?\n\n此操作不可撤销"
},
"weekdays": {
"monday": "周一",
"tuesday": "周二",
"wednesday": "周三",
"thursday": "周四",
"friday": "周五",
"saturday" : "周六",
"sunday": "周日",
"abr": {
"monday": "周一",
"tuesday": "周二",
"wednesday": "周三",
"thursday": "周四",
"friday": "周五",
"saturday" : "周六"
}
},
"times": {
"morning": "上午",
"afternoon": "下午"
},
"output": {
"title": "结果",
"chance": "几率(%)",
"to": "~",
"minimum": "保底价格",
"maximum": "最高价格",
"chart": {
"input": "输入价格",
"minimum": "保底价格",
"maximum": "最高价格"
}
},
"textbox": {
"description": "在填写一些大头菜价格后,大头菜预测工具将预测大头菜的价格并显示本周可能的趋势。",
"development": "APP仍在开发中但会随着时间的推移不断完善",
"thanks": "如果不是<a href=\"https://twitter.com/_Ninji/status/1244818665851289602?s=20\">Ninji</a>发现豆狸和粒狸如何给大头菜定价的,这一切将不可能实现。",
"support": "可以在<a href=\"https://github.com/mikebryant/ac-nh-turnip-prices/issues\">Github</a>获得支持,或讨论和贡献",
"contributors-text": "哦!别忘记感谢那些至今为止做出过贡献的人。",
"contributors": "贡献者",
"language": "语言"
}
}

@ -0,0 +1,80 @@
{
"general": {
"daisy-mae": "曹賣"
},
"welcome": {
"salutation": "你好歡迎使用Nook手機上的 <b>大頭菜預測工具</b> 。",
"description": "這個程式可以讓你每天追蹤你島上的大頭菜價格,但你必須自己記錄價格!",
"conclusion": "接下來,大頭菜預測工具將 <b>神奇地</b> 預測本週剩餘時間的大頭菜價格。"
},
"first-time": {
"title": "首次購買",
"description": "這是您第一次從自己的島上和曹賣購買大頭菜嗎?<i>(將影響您這次的模型)</i>",
"yes": "是",
"no": "否"
},
"patterns": {
"title": "上次的模型",
"description": "上週的大頭菜價格模型如何?<i>(將影響您這次的模型)</i>",
"pattern": "模型",
"all": "所有模型",
"decreasing": "遞減型",
"fluctuating": "波型",
"unknown": "不知道",
"large-spike": "三期型",
"small-spike": "四期型"
},
"prices": {
"description": "本週大頭菜的購買價格是多少?<i>(如果這是您第一次購買大頭菜,則此欄位會被停用)</i>",
"open": {
"am": "上午 - 08:00 to 11:59",
"pm": "下午 - 12:00 to 22:00"
},
"copy-permalink": "複製價格分享網址",
"permalink-copied": "網址已複製!",
"reset": "清除資料",
"reset-warning": "是否確定要清除所有資料?\n\n此動作無法復原"
},
"weekdays": {
"monday": "星期一",
"tuesday": "星期二",
"wednesday": "星期三",
"thursday": "星期四",
"friday": "星期五",
"saturday" : "星期六",
"sunday": "星期日",
"abr": {
"monday": "週一",
"tuesday": "週二",
"wednesday": "週三",
"thursday": "週四",
"friday": "週五",
"saturday" : "週六"
}
},
"times": {
"morning": "上午",
"afternoon": "下午"
},
"output": {
"title": "結果",
"chance": "機率(%)",
"to": "~",
"minimum": "保底價格",
"maximum": "最高價格",
"chart": {
"input": "輸入價格",
"minimum": "保底價格",
"maximum": "最高價格"
}
},
"textbox": {
"description": "在您紀錄了一些大頭菜價格後,大頭菜預測工具會預測,並顯示你島上可能出現的不同模型。",
"development": "此工具仍在開發中,但會隨著時間的推移而改善!",
"thanks": "如果沒有<a href=\"https://twitter.com/_Ninji/status/1244818665851289602?s=20\">Ninji 的幫忙</a>來弄清楚豆狸和粒狸如何對他們的大頭菜的估價,這一切都是不可能實現的。",
"support": "可透過 <a href=\"https://github.com/mikebryant/ac-nh-turnip-prices/issues\">Github</a>獲得支援、討論及貢獻",
"contributors-text": "哦!讓我們不要忘記感謝那些迄今為止作出貢獻的人!",
"contributors": "貢獻者",
"language": "語言"
}
}

@ -8,11 +8,11 @@
"theme_color": "#def2d9",
"icons": [
{
"src": "/img/192.png",
"src": "/img/favicon-192.png",
"sizes": "192x192"
},
{
"src": "/img/512.png",
"src": "/img/favicon-512.png",
"sizes": "512x512"
}
]

@ -1,90 +1,75 @@
// PWA Code adapted from https://github.com/pwa-builder/PWABuilder
const CACHE = "pwa-precache";
const CACHE = "pwa-precache-v1";
const precacheFiles = [
"/index.html",
"/js/predictions.js",
"/js/scripts.js",
"/css/styles.css",
"https://code.jquery.com/jquery-3.4.1.min.js",
"/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] Install Event processing");
console.log("[PWA] Skip waiting on install");
self.skipWaiting();
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);
})
);
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());
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;
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
);
});
}
)
);
event.respondWith(
(async () => {
let response;
try {
// Fetch from network first.
response = await fetch(event.request);
event.waitUntil(updateCache(event.request, response.clone()));
} catch (error) {
try {
// Try if there's locally cached version.
response = await fromCache(event.request);
} catch (error) {
console.log("[PWA] Network request failed and no cache." + error);
throw error;
}
}
return response;
})()
);
});
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");
}
// 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;
});
});
return matching;
});
});
}
function updateCache(request, response) {
return caches.open(CACHE).then(function (cache) {
return cache.put(request, response);
});
return caches.open(CACHE).then(function (cache) {
return cache.put(request, response);
});
}

Loading…
Cancel
Save