Skip to content

Instantly share code, notes, and snippets.

@angiodianxin
Last active April 1, 2023 10:41
Show Gist options
  • Save angiodianxin/89873c01bf244edaa0599b0df8a7b620 to your computer and use it in GitHub Desktop.
Save angiodianxin/89873c01bf244edaa0599b0df8a7b620 to your computer and use it in GitHub Desktop.
マビノギの数当てゲーム支援ツール
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ハコのカギ</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
font-size: 1.5rem;
}
header {
position: relative;
height: 4.5rem;
width: 46.5rem;
background: linear-gradient(60deg, #007FB1, #44A5CB);
color: #FFFFFF;
}
#reset_button {
font-size: 2.5rem;
transform: rotate(0);
transition: none;
}
#reset_button.clicked {
transform: rotate(360deg);
transition: transform .3s;
}
.header_left {
position: absolute;
left: 1rem;
top: .5rem;
bottom: .5rem;
}
.app_name {
font-size: 2.5rem;
}
.credit {
margin-left: 1rem;
color: inherit;
text-decoration: inherit;
}
.header_right {
position: absolute;
right: 1rem;
top: .5rem;
bottom: .5rem;
text-align: right;
}
#games_count {
font-size: 2.5rem;
}
#trials_table {
margin: .3rem 1rem;
display: grid;
grid-template-columns: 1.5rem 2rem 9rem 12rem 12rem 3rem;
grid-column-gap: 1rem;
}
#trials_table>* {
line-height: 3rem;
vertical-align: middle;
}
.trialHead.trial {
grid-column: 1 / 3;
text-align: center;
}
.trial_icon {
grid-column: 1;
vertical-align: middle;
}
.trial_count {
grid-column: 2;
text-align: right;
}
.number {
grid-column: 3;
text-align: center;
}
.hit {
grid-column: 4;
text-align: center;
color: #009250;
}
.blow {
grid-column: 5;
text-align: center;
color: #EDAD0B;
}
.miss {
grid-column: 6;
text-align: center;
color: #C7243A;
}
.number_input {
grid-column: 3;
height: 3rem;
width: 100%;
padding: 0;
font-size: 2.3rem;
text-align: center;
}
.hit>button,
.blow>button {
position: relative;
height: 3rem;
width: 3rem;
z-index: 10;
margin-bottom: .5rem;
border-radius: .7rem;
font-size: 2.5rem;
}
.hit>button {
background-color: #C6EDDB;
/* border: .3rem solid #009250; */
border: none;
}
.blow>button {
background-color: #FCF1D3;
/* border: .3rem solid #EDAD0B; */
border: none;
}
.hit>button.selected {
background-color: #009250;
color: #FFFFFF;
}
.blow>button.selected {
background-color: #EDAD0B;
color: #FFFFFF;
}
#candidates {
display: flex;
grid-column: 1 / 7;
margin-top: 2rem;
height: 40rem;
flex-wrap: wrap;
align-content: flex-start;
overflow-y: auto;
}
.candidate {
display: inline-block;
width: 4rem;
height: 2.5rem;
margin: .2rem;
text-align: center;
font-size: 1.5rem;
line-height: 2.5rem;
background-color: #E3E3E3;
border: none;
}
.monospace {
font-weight: 500;
font-family:
/* Linux/Android/ChromeOS */
'Liberation Serif', 'Noto Sans CJK JP',
/* Debian/Ubuntu */
'TakaoGothic', 'VL Gothic',
/* Windows */
'Yu Gothic', 'MS Gothic',
/* Mac/iOS */
'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Osaka-Mono',
'Noto Sans JP', Monospace;
}
</style>
<script src="https://kit.fontawesome.com/b212fc707c.js" crossorigin="anonymous"></script>
</head>
<body>
<header>
<div class="header_left"><span class="app_name">ハコのカギ</span><a href="https://twitter.com/midkashi"
target="_blank" class="credit">by みづかし<i class="fa-brands fa-twitter"></i></a></div>
<div class="header_right"><span id="games_count"></span>ゲーム目 <i id="reset_button"
class="fa-solid fa-rotate-right" onclick="handle_reset()"></i></div>
</header>
<div id="trials_table"></div>
<script src="https://code.jquery.com/jquery-3.6.4.min.js"
integrity="sha256-oP6HI9z1XaZNBrJURtCoUT5SUnxFr8s3BzRl+cbzUq8=" crossorigin="anonymous"></script>
<script>
let solver;
let games_count = 1;
class Solver {
constructor() {
this._trials = [new Trial()];
}
add() {
let i = this._trials.length;
let previous = this.trial(i);
this._trials.push(new Trial(i + 1, Solver.solve(previous)));
refresh_candidates();
}
trial(i = this._trials.length) {
return this._trials[i - 1];
}
static solve(previous) {
const nums = Solver.permutation(true);
let candidates = previous.candidates.filter(
Solver.query(previous.number, previous.hit, previous.blow)
);
let scores = {};
let lowest_score = Infinity;
let lowest_score_num;
if (candidates.length <= 2) {
return [candidates[0], candidates];
}
for (const num of nums) {
scores[num] = {
"00": -candidates.length / 10,
"01": -candidates.length / 10,
"02": -candidates.length / 10,
"03": -candidates.length / 10,
"10": -candidates.length / 10,
"11": -candidates.length / 10,
"12": -candidates.length / 10,
"20": -candidates.length / 10,
"21": -candidates.length / 10,
"30": -candidates.length / 10,
"variance": 0
};
}
for (const num in scores) {
let score = scores[num];
for (const candidate of candidates) {
score[Solver.calc(candidate, num).join("")]++;
}
score["variance"] = (
score["00"] ** 2 +
score["01"] ** 2 +
score["02"] ** 2 +
score["03"] ** 2 +
score["10"] ** 2 +
score["11"] ** 2 +
score["12"] ** 2 +
score["20"] ** 2 +
score["21"] ** 2 +
score["30"] ** 2
) * (candidates.includes(num) ? 1.1 : 1);
if (score["variance"] < lowest_score) {
lowest_score = score["variance"];
lowest_score_num = num;
}
}
return [lowest_score_num, candidates];
}
change(changed_i) {
let len = this._trials.length;
if (changed_i == len) {
return;
}
for (let i = changed_i; i < len; i++) {
const trial = this.trial(i);
const next_trial = this.trial(i + 1);
next_trial.candidates = trial.candidates.filter(
Solver.query(trial.number, trial.hit, trial.blow)
);
}
this.trial(len).number = Solver.solve(this.trial(len - 1))[0];
refresh_candidates();
}
static query(number, hit, blow) {
return (candidate) => {
const [_hit, _blow] = Solver.calc(number, candidate);
return hit == _hit && blow == _blow;
}
}
static calc(number, candidate) {
const hit = (
(candidate[0] == number[0]) +
(candidate[1] == number[1]) +
(candidate[2] == number[2])
);
const blow = (
-hit +
candidate.includes(number[0]) +
candidate.includes(number[1]) +
candidate.includes(number[2])
);
return [hit, blow];
}
static permutation(allow_void = false) {
let pe = [];
for (let i = 1; i <= 9; i++) {
if (allow_void) {
pe.push(`${i}--`);
}
for (let j = 1; j <= 9; j++) {
if (i == j) continue;
if (allow_void) {
pe.push(`${i}${j}-`);
}
for (let k = 1; k <= 9; k++) {
if (i == k || j == k) continue;
pe.push(`${i}${j}${k}`);
}
}
}
return pe;
}
}
class Trial {
constructor(i = 1, [number, candidates] = ["123", Solver.permutation()]) {
this._i = i;
this._number = number;
this._hit = null;
this._blow = null;
this._candidates = candidates;
this._is_tail = true;
$(
`<div class='trial${i} trial_icon'>` +
` <i class='fa-solid fa-key'></i>` +
`</div>` +
`<div class='trial${i} trial_count'>${i}</div>`
).insertBefore("#candidates");
this._number_input = $(
`<input class='trial${i} number_input monospace' type='tel' id='number_input${i}' minlength='3' maxlength='3', value='${number}' onkeyup='solver.trial(${i}).read_number();'>`
).insertBefore("#candidates");
this._hit_wrap = $(`<div class='trial${i} hit'></div>`).insertBefore("#candidates");
this._blow_wrap = $(`<div class='trial${i} blow'></div>`).insertBefore("#candidates");
this._miss_wrap = $(`<div class='trial${i} miss'></div>`).insertBefore("#candidates");
this._hit_wrap.append(
`<button onclick='solver.trial(${i}).hit = 0;' value='0'>0</button>` +
`<button onclick='solver.trial(${i}).hit = 1;' value='1'>1</button>` +
`<button onclick='solver.trial(${i}).hit = 2;' value='2'>2</button>` +
`<button onclick='solver.trial(${i}).hit = 3;' value='3'>3</button>`
);
this._blow_wrap.append(
`<button onclick='solver.trial(${i}).blow = 0;' value='0'>0</button>` +
`<button onclick='solver.trial(${i}).blow = 1;' value='1'>1</button>` +
`<button onclick='solver.trial(${i}).blow = 2;' value='2'>2</button>` +
`<button onclick='solver.trial(${i}).blow = 3;' value='3'>3</button>`
);
}
read_number() {
console.log(this._i);
const num = this._number_input.val();
if (num.length == 3) {
this._number = num;
solver.change(this._i);
}
}
get number() {
return this._number;
}
set number(v) {
console.log(v);
this._number = v;
this._number_input.val(v ?? "");
}
get hit() {
return this._hit;
}
set hit(v) {
this._hit = v;
this._hit_wrap.children().removeClass("selected");
this._hit_wrap.children(`[value=${v}]`).addClass("selected");
this._miss_wrap.text(3 - this._hit - this._blow);
if (!this._is_tail) {
solver.change(this._i);
} else if (this._blow !== null) {
this._is_tail = false;
solver.add();
}
}
get blow() {
return this._blow;
}
set blow(v) {
this._blow = v;
this._blow_wrap.children().removeClass("selected");
this._blow_wrap.children(`[value=${v}]`).addClass("selected");
this._miss_wrap.text(3 - this._hit - this._blow);
if (!this._is_tail) {
solver.change(this._i);
} else if (this._hit !== null) {
this._is_tail = false;
solver.add();
}
}
get candidates() {
return this._candidates;
}
set candidates(v) {
this._candidates = v;
}
}
function refresh_candidates() {
$("#candidates").empty();
for (const candidate of solver.trial().candidates) {
$("#candidates").append(
`<button onclick='solver.trial().number = "${candidate}";' class='candidate'>${candidate}</button>`
)
}
}
function handle_reset() {
$("#reset_button").addClass("clicked");
setTimeout(() => {
$("#reset_button").removeClass("clicked");
}, 400);
init();
}
function init() {
$("#games_count").text(games_count++);
$("#trials_table").empty();
$("#trials_table").append(
"<div class='trialHead trial'>回数</div>" +
"<div class='trialHead number'>番号</div>" +
"<div class='trialHead hit'>●</div>" +
"<div class='trialHead blow'>▲</div>" +
"<div class='trialHead miss'>×</div>" +
"<div id='candidates'></div>"
);
solver = new Solver();
refresh_candidates();
}
init();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment