Last active
April 1, 2023 10:41
-
-
Save angiodianxin/89873c01bf244edaa0599b0df8a7b620 to your computer and use it in GitHub Desktop.
マビノギの数当てゲーム支援ツール
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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