Merge branch 'master' of https://github.com/mikebryant/ac-nh-turnip-prices into mikebryant-master

master
Lawrence Luk 4 years ago
commit e16aba23ed

@ -1,6 +1,7 @@
# ac-nh-turnip-prices
Price calculator/predictor for Turnip prices
[![discord](https://img.shields.io/badge/discord-join-7289DA.svg?logo=discord&longCache=true&style=flat)](https://discord.gg/bRh74X8)
Price calculator/predictor for Turnip prices
## Scope
This tool is:

@ -513,39 +513,21 @@ input[type=number] {
to {bottom: 0; opacity: 0;}
}
/* Animation */
.parallax>use {
animation: move-forever 25s cubic-bezier(.55, .5, .45, .5) infinite;
}
/* Cloud SVG placement */
.parallax>use:nth-child(1) {
animation-delay: -2s;
animation-duration: 7s;
transform: translate3d(-30px, 0, 0);
}
.parallax>use:nth-child(2) {
animation-delay: -3s;
animation-duration: 10s;
transform: translate3d(-90px, 0, 0);
}
.parallax>use:nth-child(3) {
animation-delay: -4s;
animation-duration: 13s;
transform: translate3d(45px, 0, 0);
}
.parallax>use:nth-child(4) {
animation-delay: -5s;
animation-duration: 20s;
}
@keyframes move-forever {
0% {
transform: translate3d(-90px, 0, 0);
}
100% {
transform: translate3d(85px, 0, 0);
}
transform: translate3d(20px, 0, 0);
}
/*Shrinking for mobile*/
@ -555,3 +537,76 @@ input[type=number] {
min-height: 40px;
}
}
/*Darkmodjs*/
.darkmode-layer, .darkmode-toggle {
z-index: 1;
}
.darkmode-toggle {
opacity: 0.7;
transition: opacity 0.15s;
}
.darkmode-toggle:hover {
opacity: 0.9;
}
.darkmode-toggle:active {
transition: opacity 0s;
opacity: 1;
}
.darkmode-toggle:focus {
outline: none;
}
div.darkmode-background{
background: #fef0e3;
background-image:
radial-gradient(#fff 20%, transparent 0),
radial-gradient(#fff 20%, transparent 0);
position: inherit;
}
body.darkmode--activated{
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: 'Varela Round', sans-serif;
width: 100%;
margin: 0 0 0 0;
position: absolute
}
body.darkmode--activated div[class^="dialog-box"],
body.darkmode--activated div[class^="nook-phone"],
body.darkmode--activated form[class^="input__form"],
{
background: #fee0c4;
color: #010F1D;
}
body.darkmode--activated svg[class^="waves"]{
background: #fef0e3;
}
body.darkmode--activated a,
body.darkmode--activated b,
body.darkmode--activated input[type=number]:not(:placeholder-shown){
color: #586472;
}
body.darkmode--activated input[type="radio"]+label,
body.darkmode--activated input[type=number]:placeholder-shown{
background: #bda284;
}
body.darkmode--activated input[type="radio"]:checked+label{
background: #7b6955;
}
body.darkmode--activated i{
color: #7b6955;
}

@ -313,6 +313,8 @@
<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/translations.js"></script>
<script src="js/contributors.js"></script>
<script>

@ -18,34 +18,35 @@ const chart_options = {
};
function update_chart(input_data, possibilities) {
var ctx = $("#chart");
datasets = [
{
let ctx = $("#chart"),
datasets = [{
label: i18next.t("output.chart.input"),
data: input_data.slice(1),
fill: false,
},
{
}, {
label: i18next.t("output.chart.minimum"),
data: possibilities[0].prices.slice(1).map(day => day.min),
fill: false,
},
{
}, {
label: i18next.t("output.chart.maximum"),
data: possibilities[0].prices.slice(1).map(day => day.max),
fill: "-1",
},
];
],
labels = [i18next.t("weekdays.sunday")].concat(...[i18next.t("weekdays.abr.monday"), i18next.t("weekdays.abr.tuesday"), i18next.t("weekdays.abr.wednesday"), i18next.t("weekdays.abr.thursday"), i18next.t("weekdays.abr.friday"), i18next.t("weekdays.abr.saturday")].map(
day => [i18next.t("times.morning"),
i18next.t("times.afternoon")].map(
time => `${day} ${time}`)));
if (chart_instance) {
chart_instance.data.datasets = datasets;
chart_instance.data.labels = labels;
chart_instance.update();
} else {
chart_instance = new Chart(ctx, {
data: {
datasets: datasets,
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")],
labels: labels
},
options: chart_options,
type: "line",

@ -3,12 +3,14 @@ function getContributors() {
const container = $('#contributors');
jQuery.ajax('https://api.github.com/repos/mikebryant/ac-nh-turnip-prices/contributors', {})
.done(function (data) {
const contributorList = [];
data.forEach((contributor, idx) => {
container.append(`<a href="${contributor.html_url}">${contributor.login}</a>`);
contributorList.push(`<a href="${contributor.html_url}">${contributor.login}</a>`);
if (idx < data.length - 1) {
container.append(', ');
contributorList.push(', ');
}
});
container.append(contributorList.join(''));
});
}
}

@ -1,7 +1,3 @@
// 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,
@ -38,43 +34,14 @@ 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 RATE_MULTIPLIER * (given_price - 0.99999) / buy_price;
}
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 range_length(range) {
return range[1] - range[0];
}
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;
@ -89,54 +56,6 @@ function range_intersect_length(range1, range2) {
return range_length(range_intersect(range1, range2));
}
/*
* 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 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) {
rate_min *= RATE_MULTIPLIER;
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);
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 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];
}
predicted_prices.push({
min: min_pred,
max: max_pred,
});
}
return prob;
}
/*
* Probability Density Function of rates.
* Since the PDF is continuous*, we approximate it by a discrete probability function:
@ -274,7 +193,94 @@ class PDF {
}
}
/*
class Predictor {
constructor(prices, first_buy, previous_pattern) {
// 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
this.fudge_factor = 0;
this.prices = prices;
this.first_buy = first_buy;
this.previous_pattern = previous_pattern;
}
intceil(val) {
return Math.trunc(val + 0.99999);
}
minimum_rate_from_given_and_base(given_price, buy_price) {
return RATE_MULTIPLIER * (given_price - 0.99999) / buy_price;
}
maximum_rate_from_given_and_base(given_price, buy_price) {
return RATE_MULTIPLIER * (given_price + 0.00001) / buy_price;
}
rate_range_from_given_and_base(given_price, buy_price) {
return [
this.minimum_rate_from_given_and_base(given_price, buy_price),
this.maximum_rate_from_given_and_base(given_price, buy_price)
];
}
get_price(rate, basePrice) {
return this.intceil(rate * basePrice / RATE_MULTIPLIER);
}
* multiply_generator_probability(generator, probability) {
for (const it of generator) {
yield {...it, probability: it.probability * probability};
}
}
/*
* 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 return the conditional probability given the given_prices, and modify
* the predicted_prices array.
* If the given_prices won't match, returns 0.
*/
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];
const rate_range = [rate_min, rate_max];
let prob = 1;
for (let i = start; i < start + length; i++) {
let min_pred = this.get_price(rate_min, buy_price);
let max_pred = this.get_price(rate_max, buy_price);
if (!isNaN(given_prices[i])) {
if (given_prices[i] < min_pred - this.fudge_factor || given_prices[i] > max_pred + this.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 =
this.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];
}
predicted_prices.push({
min: min_pred,
max: max_pred,
});
}
return prob;
}
/*
* This corresponds to the code:
* rate = randfloat(start_rate_min, start_rate_max);
* for (int i = start; i < start + length; i++)
@ -287,7 +293,7 @@ class PDF {
* the predicted_prices array.
* If the given_prices won't match, returns 0.
*/
function generate_decreasing_random_price(
generate_decreasing_random_price(
given_prices, predicted_prices, start, length, start_rate_min,
start_rate_max, rate_decay_min, rate_decay_max) {
start_rate_min *= RATE_MULTIPLIER;
@ -300,17 +306,17 @@ function generate_decreasing_random_price(
let prob = 1;
for (let i = start; i < start + length; i++) {
let min_pred = get_price(rate_pdf.min_value(), buy_price);
let max_pred = get_price(rate_pdf.max_value(), buy_price);
let min_pred = this.get_price(rate_pdf.min_value(), buy_price);
let max_pred = this.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) {
if (given_prices[i] < min_pred - this.fudge_factor || given_prices[i] > max_pred + this.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(given_prices[i], min_pred, max_pred), buy_price);
this.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;
@ -327,10 +333,10 @@ function generate_decreasing_random_price(
rate_pdf.decay(rate_decay_min, rate_decay_max);
}
return prob;
}
}
/*
/*
* This corresponds to the code:
* rate = randfloat(rate_min, rate_max);
* sellPrices[work++] = intceil(randfloat(rate_min, rate) * basePrice) - 1;
@ -341,7 +347,7 @@ function generate_decreasing_random_price(
* the predicted_prices array.
* If the given_prices won't match, returns 0.
*/
function generate_peak_price(
generate_peak_price(
given_prices, predicted_prices, start, rate_min, rate_max) {
rate_min *= RATE_MULTIPLIER;
rate_max *= RATE_MULTIPLIER;
@ -354,16 +360,16 @@ function generate_peak_price(
// 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) {
const min_pred = this.get_price(rate_min, buy_price);
const max_pred = this.get_price(rate_max, buy_price);
if (middle_price < min_pred - this.fudge_factor || middle_price > max_pred + this.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);
this.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) {
@ -393,15 +399,15 @@ function generate_peak_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) {
const min_pred = this.get_price(rate_min, buy_price) - 1;
const max_pred = this.get_price(rate_range[1], buy_price) - 1;
if (price < min_pred - this.fudge_factor || price > max_pred + this.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 rate2_range = this.rate_range_from_given_and_base(clamp(price, min_pred, max_pred)+ 1, buy_price);
const F = (t, ZZ) => {
if (t <= 0) {
return 0;
@ -424,8 +430,8 @@ function generate_peak_price(
// 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;
let min_pred = this.get_price(rate_min, buy_price) - 1;
let max_pred = this.get_price(rate_max, buy_price) - 1;
if (!isNaN(given_prices[start])) {
min_pred = given_prices[start];
max_pred = given_prices[start];
@ -437,7 +443,7 @@ function generate_peak_price(
// Main spike 2
min_pred = predicted_prices[start].min;
max_pred = get_price(rate_max, buy_price);
max_pred = this.get_price(rate_max, buy_price);
if (!isNaN(given_prices[start + 1])) {
min_pred = given_prices[start + 1];
max_pred = given_prices[start + 1];
@ -448,7 +454,7 @@ function generate_peak_price(
});
// Main spike 3
min_pred = get_price(rate_min, buy_price) - 1;
min_pred = this.get_price(rate_min, buy_price) - 1;
max_pred = predicted_prices[start + 1].max - 1;
if (!isNaN(given_prices[start + 2])) {
min_pred = given_prices[start + 2];
@ -460,10 +466,9 @@ function generate_peak_price(
});
return prob;
}
}
function*
generate_pattern_0_with_lengths(
* 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) {
/*
@ -516,14 +521,14 @@ function*
let probability = 1;
// High Phase 1
probability *= generate_individual_random_price(
probability *= this.generate_individual_random_price(
given_prices, predicted_prices, 2, high_phase_1_len, 0.9, 1.4);
if (probability == 0) {
return;
}
// Dec Phase 1
probability *= generate_decreasing_random_price(
probability *= this.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) {
@ -531,14 +536,14 @@ function*
}
// High Phase 2
probability *= generate_individual_random_price(given_prices, predicted_prices,
probability *= this.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
probability *= generate_decreasing_random_price(
probability *= this.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);
@ -553,7 +558,7 @@ function*
const prev_length = 2 + high_phase_1_len + dec_phase_1_len +
high_phase_2_len + dec_phase_2_len;
probability *= generate_individual_random_price(
probability *= this.generate_individual_random_price(
given_prices, predicted_prices, prev_length, 14 - prev_length, 0.9, 1.4);
if (probability == 0) {
return;
@ -565,9 +570,9 @@ function*
prices: predicted_prices,
probability,
};
}
}
function* generate_pattern_0(given_prices) {
* generate_pattern_0(given_prices) {
/*
decPhaseLen1 = randbool() ? 3 : 2;
decPhaseLen2 = 5 - decPhaseLen1;
@ -578,15 +583,15 @@ 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* 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),
yield* this.multiply_generator_probability(
this.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));
}
}
}
}
}
function* generate_pattern_1_with_peak(given_prices, peak_start) {
* generate_pattern_1_with_peak(given_prices, peak_start) {
/*
// PATTERN 1: decreasing middle, high spike, random low
peakStart = randint(3, 9);
@ -621,17 +626,17 @@ function* generate_pattern_1_with_peak(given_prices, peak_start) {
];
let probability = 1;
probability *= generate_decreasing_random_price(
probability *= this.generate_decreasing_random_price(
given_prices, predicted_prices, 2, peak_start - 2, 0.85, 0.9, 0.03, 0.05);
if (probability == 0) {
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]
let min_randoms = [0.9, 1.4, 2.0, 1.4, 0.9, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4]
let 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++) {
probability *= generate_individual_random_price(
probability *= this.generate_individual_random_price(
given_prices, predicted_prices, i, 1, min_randoms[i - peak_start],
max_randoms[i - peak_start]);
if (probability == 0) {
@ -644,15 +649,15 @@ function* generate_pattern_1_with_peak(given_prices, peak_start) {
prices: predicted_prices,
probability,
};
}
}
function* generate_pattern_1(given_prices) {
* generate_pattern_1(given_prices) {
for (var peak_start = 3; peak_start < 10; peak_start++) {
yield* multiply_generator_probability(generate_pattern_1_with_peak(given_prices, peak_start), 1 / (10 - 3));
yield* this.multiply_generator_probability(this.generate_pattern_1_with_peak(given_prices, peak_start), 1 / (10 - 3));
}
}
}
function* generate_pattern_2(given_prices) {
* generate_pattern_2(given_prices) {
/*
// PATTERN 2: consistently decreasing
rate = 0.9;
@ -679,7 +684,7 @@ function* generate_pattern_2(given_prices) {
];
let probability = 1;
probability *= generate_decreasing_random_price(
probability *= this.generate_decreasing_random_price(
given_prices, predicted_prices, 2, 14 - 2, 0.85, 0.9, 0.03, 0.05);
if (probability == 0) {
return;
@ -691,9 +696,9 @@ function* generate_pattern_2(given_prices) {
prices: predicted_prices,
probability,
};
}
}
function* generate_pattern_3_with_peak(given_prices, peak_start) {
* generate_pattern_3_with_peak(given_prices, peak_start) {
/*
// PATTERN 3: decreasing, spike, decreasing
@ -738,27 +743,27 @@ function* generate_pattern_3_with_peak(given_prices, peak_start) {
];
let probability = 1;
probability *= generate_decreasing_random_price(
probability *= this.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
probability *= generate_individual_random_price(
probability *= this.generate_individual_random_price(
given_prices, predicted_prices, peak_start, 2, 0.9, 1.4);
if (probability == 0) {
return;
}
probability *= generate_peak_price(
probability *= this.generate_peak_price(
given_prices, predicted_prices, peak_start + 2, 1.4, 2.0);
if (probability == 0) {
return;
}
if (peak_start + 5 < 14) {
probability *= generate_decreasing_random_price(
probability *= this.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) {
@ -772,52 +777,66 @@ function* generate_pattern_3_with_peak(given_prices, peak_start) {
prices: predicted_prices,
probability,
};
}
}
function* generate_pattern_3(given_prices) {
* generate_pattern_3(given_prices) {
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));
yield* this.multiply_generator_probability(this.generate_pattern_3_with_peak(given_prices, peak_start), 1 / (10 - 2));
}
}
}
function get_transition_probability(previous_pattern) {
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];
// Use the steady state probabilities of PROBABILITY_MATRIX if we don't
// know what the previous pattern was.
// See https://github.com/mikebryant/ac-nh-turnip-prices/issues/68
// and https://github.com/mikebryant/ac-nh-turnip-prices/pull/90
// for more information.
return [4530/13082, 3236/13082, 1931/13082, 3385/13082];
}
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);
* generate_all_patterns(sell_prices, previous_pattern) {
const generate_pattern_fns = [this.generate_pattern_0, this.generate_pattern_1, this.generate_pattern_2, this.generate_pattern_3];
const transition_probability = this.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]);
yield* this.multiply_generator_probability(generate_pattern_fns[i].bind(this)(sell_prices), transition_probability[i]);
}
}
}
function* generate_possibilities(sell_prices, first_buy, previous_pattern) {
* 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);
yield* this.generate_pattern_3(sell_prices);
} else {
// 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)
yield* this.generate_all_patterns(sell_prices, previous_pattern)
}
}
} else {
yield* generate_all_patterns(sell_prices, previous_pattern)
yield* this.generate_all_patterns(sell_prices, previous_pattern)
}
}
}
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);
analyze_possibilities() {
const sell_prices = this.prices;
const first_buy = this.first_buy;
const previous_pattern = this.previous_pattern;
let generated_possibilities = []
for (let i = 0; i < 6; i++) {
this.fudge_factor = i;
generated_possibilities = Array.from(this.generate_possibilities(sell_prices, first_buy, previous_pattern));
if (generated_possibilities.length > 0) {
console.log("Generated possibilities using fudge factor %d: ", i, generated_possibilities);
break;
}
}
const total_probability = generated_possibilities.reduce((acc, it) => acc + it.probability, 0);
for (const it of generated_possibilities) {
@ -846,7 +865,7 @@ function analyze_possibilities(sell_prices, first_buy, previous_pattern) {
poss.weekMax = Math.max(...weekMaxes);
}
category_totals = {}
let category_totals = {}
for (let i of [0, 1, 2, 3]) {
category_totals[i] = generated_possibilities
.filter(value => value.pattern_number == i)
@ -862,7 +881,7 @@ function analyze_possibilities(sell_prices, first_buy, previous_pattern) {
return b.category_total_probability - a.category_total_probability || b.probability - a.probability;
});
global_min_max = [];
let global_min_max = [];
for (var day = 0; day < 14; day++) {
prices = {
min: 999,
@ -888,4 +907,5 @@ function analyze_possibilities(sell_prices, first_buy, previous_pattern) {
});
return generated_possibilities;
}
}

@ -265,7 +265,8 @@ const calculateOutput = function (data, first_buy, previous_pattern) {
return;
}
let output_possibilities = "";
let analyzed_possibilities = analyze_possibilities(data, first_buy, previous_pattern);
let predictor = new Predictor(data, first_buy, previous_pattern);
let analyzed_possibilities = predictor.analyze_possibilities();
previous_pattern_number = ""
for (let poss of analyzed_possibilities) {
var out_line = "<tr><td class='table-pattern'>" + poss.pattern_description + "</td>"

@ -19,12 +19,13 @@ i18next
['es-ES', 'Español'],
['fr', 'Français'],
['it', 'Italiano'],
['ru', 'Русский'],
['ja', '日本語'],
['ko', '한국어'],
['pt-BR', 'Português'],
['ru', 'Русский'],
['zh-CN', '简体中文'],
['zh-TW', '繁體中文']
],
].sort(),
languageSelector = $('#language');
languages.map(([code, name]) => {
languageSelector.append(`<option value="${code}"${code == i18next.language ? ' selected' : ''}>${name}</option>`);

@ -25,10 +25,10 @@
"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>",
"description": "¿Cuál fue el precio de los nabos en su isla esta 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"
"pm": "PM - De 12:00 a 22:00"
},
"copy-permalink": "Copiar permalink",
"permalink-copied": "¡Permalink copiado!",
@ -65,7 +65,7 @@
"chart": {
"input": "Precio de entrada",
"minimum": "Mínimo garantizado",
"maximum": "´Máximo potencial"
"maximum": "Máximo potencial"
}
},
"textbox": {

@ -65,7 +65,7 @@
"maximum": "Massimo Potenziale",
"chart": {
"input": "Prezzo Iniziale",
"minimum": "Minimo Gatantito",
"minimum": "Minimo Garantito",
"maximum": "Massimo Potenziale"
}
},

@ -0,0 +1,80 @@
{
"general": {
"daisy-mae": "무파니"
},
"welcome": {
"salutation": "안녕하세요, Nook Inc. 스마트폰의 <b>Turnip Prophet</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 ~ 오후 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": "기여해 주신 분들",
"language": "언어"
}
}

@ -3,19 +3,19 @@
"daisy-mae": "曹賣"
},
"welcome": {
"salutation": "你好,歡迎使用Nook手機上的 <b>大頭菜預測工具</b> 。",
"description": "這個程式可以讓你每天追蹤你島上的大頭菜價格,但你必須自己記錄價格!",
"conclusion": "接下來,大頭菜預測工具將 <b>神奇地</b> 預測本週剩餘時間的大頭菜價格。"
"salutation": "你好,歡迎使用 Nook 手機上的 <b>Turnip Prophet</b>。",
"description": "這個工具可以讓你每天追蹤自己島上的大頭菜價格,但你必須自己輸入價格!",
"conclusion": "接下來,Turnip Prophet 將 <b>神奇地</b> 預測本週剩餘時間的大頭菜價格。"
},
"first-time": {
"title": "首次購買",
"description": "這是您第一次從自己的島上和曹賣購買大頭菜嗎?<i>(將影響這次的模型)</i>",
"description": "這是你第一次從自己島上和曹賣購買大頭菜嗎?<i>(將影響這次的模型)</i>",
"yes": "是",
"no": "否"
},
"patterns": {
"title": "上次的模型",
"description": "上週的大頭菜價格模型如何?<i>(將影響您這次的模型)</i>",
"description": "上週大頭菜的價格模型是什麼?<i>(將影響這次的模型)</i>",
"pattern": "模型",
"all": "所有模型",
"decreasing": "遞減型",
@ -25,10 +25,10 @@
"small-spike": "四期型"
},
"prices": {
"description": "本週大頭菜的購買價格是多少?<i>(如果這是您第一次購買大頭菜,則此欄位會被停用)</i>",
"description": "本週自己島上的大頭菜買價?<i>(若這是你第一次購買大頭菜,則此欄位會被停用)</i>",
"open": {
"am": "上午 - 08:00 to 11:59",
"pm": "下午 - 12:00 to 22:00"
"am": "上午 - 08:00 11:59",
"pm": "下午 - 12:00 22:00"
},
"copy-permalink": "複製價格分享網址",
"permalink-copied": "網址已複製!",
@ -69,11 +69,11 @@
}
},
"textbox": {
"description": "在您紀錄了一些大頭菜價格後,大頭菜預測工具會預測,並顯示你島上可能出現的不同模型。",
"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": "哦!讓我們不要忘記感謝那些迄今為止作出貢獻的人!",
"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": "語言"
}

Loading…
Cancel
Save