JavaScript:狩猟鳥獣クイズ【わな猟】

JavaScript

アプリについて

JavaScriptで作成した狩猟免許試験【わな猟】に出題される狩猟鳥獣(捕っていい動物)と非狩猟鳥獣(捕ってはいけない動物)16種類を「捕れる」「捕れない」の2択式で解答するだけのアプリです。

ブラウザで気軽にできます。狩猟免許試験前の息抜きにどうぞ。

実行イメージ

全問終了時のイメージ(正解と不正解がわかるようになっている)

プログラム

HTML部分

 index.html

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width,user-scalable=yes">
	<link rel="stylesheet" href="style.css">
	<script src="main.js" type="text/javascript"></script>
	<title>狩猟鳥獣クイズ【わな猟】</title>
</head>
<body>
	<header>
		<h1>狩猟鳥獣クイズ【わな猟】</h1>
	</header>

	<div id="contents"></div>

	<footer id="footer">
		<small>
			<p>&copy; <a href="https://torisky.com/" target="_blank">torisky.com</a></p>
		</small>
	</footer>
</body>
</html>

JavaScript部分

main.js

/*
 * 狩猟鳥獣クイズ【わな猟】
 *		わな猟狩猟免許試験に出題される狩猟鳥獣と非狩猟鳥獣16種類について
 *		「捕れる」「捕れない」で解答します
 */

// 出題される問題データ
let mondai = [	// kotae 1: 捕れる 2: 捕れない

	{ text: "タヌキ", kotae : 1 },	// 狩猟鳥獣(捕れる)
	{ text: "キツネ", kotae : 1 },
	{ text: "テン", kotae : 1 },
	{ text: "イタチ(オス)", kotae : 1 },
	{ text: "ニホンジカ", kotae : 1 },
	{ text: "ミンク", kotae : 1 },
	{ text: "アライグマ", kotae : 1 },
	{ text: "ハクビシン", kotae : 1 },
	{ text: "アナグマ", kotae : 1 },
	
	{ text: "モモンガ", kotae : 2 },	// 狩猟鳥獣と誤認されやすい鳥獣(捕れない)
	{ text: "オコジョ", kotae : 2 },
	{ text: "カモシカ", kotae : 2 },
	{ text: "イタチ(メス)", kotae : 2 },
	{ text: "二ホンリス", kotae : 2 },
	{ text: "ムササビ", kotae : 2 },
	{ text: "ニホンザル", kotae : 2 },
];

let count;		// 出題番号(0~)
let contents;		// コンテンツ表示箇所(問題と解答ボタン等)
let footer;		// フッター表示箇所

/*
 * pythonやC#のString.format的なメソッドをJavaScriptで実現する
 *
 * 	参考:https://www.it-swarm-ja.tech/ja/javascript/printfstringformat%E3%81%A8%E5%90%8C%E7%AD%89%E3%81%AEjavascript/957771974/
 *
 */
String.prototype.format = function(){
	let formatted = this;
	for(let arg in arguments){
		formatted = formatted.replace("{" + arg + "}", arguments[arg]);
	}
	return formatted;
};

/*
 * 初期化処理(リトライ時も利用)
 */
function init(){
	contents.innerHTML = "";	// コンテンツクリア
	count = 0;				// 出題カウント(終了時チェック用)

	// 問題データシャッフル
	suffleMondai();

	// 問題出題
	nextMondai();
}

/*
 * 問題の出題順をシャッフル
 */
function suffleMondai(){
	for(let i=0; i<mondai.length; i++){
		let r = Math.floor(Math.random() * mondai.length);
		let work = mondai[i];
		mondai[i] = mondai[r];
		mondai[r] = work;
	}
}

/*
 * 次の問題を表示
 */
function nextMondai(){
	// 終了チェック
	if(count >= mondai.length){
		checkKaitou();
		return;
	}

	// 問題表示用pタグ生成
	let text = document.createElement("p");
	text.id = "mondai{0}".format(count);
	text.innerHTML = "{0}".format(mondai[count].text);	// 問題表示

	// 解答表示用ラジオボタン生成
	let kaitou = document.createElement("p");

	// 解答表示用pタグ内にラジオボタンを挿入
	kaitouText = "<label for='yes{0}'><input type='radio' id='yes{1}' name='kaitou{2}'>捕れる</label><label for='no{3}'><input type='radio' id='no{4}' name='kaitou{5}'>捕れない</label>".format(count, count, count, count, count, count);
	console.log(kaitouText);
	kaitou.innerHTML = kaitouText;

	// コンテンツ部分にエレメント追加
	contents.appendChild(text);
	contents.appendChild(kaitou);
	
	// ラジオボタンイベント設定
	let yes = document.getElementById("yes{0}".format(count));
	let no = document.getElementById("no{0}".format(count));

	yes.addEventListener("click", nextMondai);
	no.addEventListener("click", nextMondai);
	yes.addEventListener("click", clickOnce);	// 2度と押せなくする
	no.addEventListener("click", clickOnce);	// 2度と押せなくする
	
	// 一番下が常に見えるようにスクロールする
	footer.scrollIntoView(false);

	// 出題数カウント
	count++;
}

/*
 * ラジオボタンは2度と押せなくする
 */
function clickOnce(){
	// ラジオボタンの2つ組を取得
	let targets = document.getElementsByName(this.name);
	console.log(targets);
	
	// ラジオボタンを押せなくする
	for(let i in targets){
		targets[i].disabled = "disabled";		// イメージ: <input type="radio" name="***" disabled>
	}
}

/*
 * リトライボタンの生成
 */
function createRetry(){
	let retryButton = document.createElement("button");
	retryButton.textContent = "もう一度";
	retryButton.setAttribute("class", "retry-button");	// .retry-buttonクラスを設定
	retryButton.addEventListener("click", init);		// クリックすると初期化
	contents.appendChild(retryButton);				// コンテンツに部分に追加

	// 最初の問題表示部分までスクロールする
	contents.scrollIntoView(true);
}

/*
 * 解答チェック
 */
function checkKaitou(){
	// 正解問題リスト、不正解問題リスト、不正解した問題で選択したラジオボタンラベル
	let ok = [], ng = [], label = [];

	// 終了アラート
	alert("全問終了");
	
	// 正解は〇、不正解は×と正しい選択肢を示す
	for(let i in mondai){
		let mondaiNo = document.getElementById("mondai{0}".format(i));	// 問題表示部分
		let kaitou = document.getElementsByName("kaitou{0}".format(i));	// 解答ラジオボタン

		// 正解・不正解をチェック
		let checked = 0;

		if(kaitou[0].checked) checked = 1;		// 「捕れる」にチェックした
		else if(kaitou[1].checked) checked = 2;	// 「捕れない」にチェックした

		if(mondai[i].kotae === checked){
			ok.push(mondaiNo);		// 正解した問題をokオブジェクトに追加
		}
		else{
			ng.push(mondaiNo);		// 不正解の問題をngオブジェクトに追加

			// 不正解のラベル部分をlabelオブジェクトに追加
			if(checked == 1) label.push(kaitou[1]);
			else label.push(kaitou[0]);
		}
	}

	// 正解の表示
	console.log("正解数 = " + ok.length);
	for(let p of ok){
		p.innerHTML = "{0}<span class='ok'>〇</span>".format(p.textContent);	// 〇をつける
	}
	
	// 不正解の表示
	console.log("不正解数 = " + ng.length);
	for(let i in ng){
		ng[i].innerHTML = "{0}<span class='ng'>×</span>".format(ng[i].textContent);	// ×をつける
		// 正解の選択肢に赤枠をつける
		label[i].labels[0].style.borderRadius = "20px";
		label[i].labels[0].style.border = "solid 2px red";
	}
	// リトライボタンの生成
	createRetry();
}

/*
 * 起動時の処理
 */
window.addEventListener("load", function(){
	console.log("問題データ数 = {0}".format(mondai.length));

	// DOM要素取得(コンテンツ部分、フッター部分)
	contents = document.getElementById("contents");
	footer = document.getElementById("footer");
	
	// 初期化処理
	init();

});

CSS部分

style.css

/* style.css */
@charset "utf-8";

*{
	margin: 0;
	padding: 0;
}

body{
	font-family: Helvetica, 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', '游ゴシック', meiryo, sans-serif;
}

/* ヘッダ */
header{
	background-color: orange;
	color: black;
}

header h1{
	font-size: 24pt;
	padding: 20px 3em;
	line-height: 1.7em;
	letter-spacing: .2em;
	text-align: center;
}

/* コンテンツ */
div#contents{
	overflow: auto;
	background-color: white;
	width: 800px;
	margin: 1em auto;
	padding: 0;
}

div#contents p{
	font-size: 18pt;
	padding: 0.5em;
	line-height: 1.7em;
	letter-spacing: .2em;
	text-align: justify;
	color: black;
}

div#contents label{	/* ラジオボタンの文字 */
	padding: 0.5em;
	font-size: 16pt;
	transition: all 0.5s ease 0s;
}

div#contents label:active{
	font-weight: 900;	/*  */
	transform: translateY(-0.18em);	/* 上方向に少し移動 */
}

/* フッタ */
footer{
	overflow-y: scroll;	/* JS: scrollIntoView()を利用するため */
	margin-top: 1em;
	text-align: center;
	padding: 20px 40px;
	font-size: 14pt;
	background-color: black;
	color: yellowgreen;
}

/* 汎用タグ */
small{
	font-size: 14pt;
}

a{
	color: yellowgreen;
	text-decoration: none;
}

a:hover{
	color: white;
}

/* リトライボタン部分 */
button{
	-webkit-appearance: none;	/* iOS対策 */
	font-size: 16pt;
}

.retry-button{
	width: 100%;
	margin-top: 1em;
	padding: 10px;
	text-align: center;
	box-shadow: 3px 3px 7px #ddd;	/* ボタンの影 */
	margin-bottom: 0.5em;
	transition: all 0.3s ease 0s;	/* イベント毎の表示を0.3秒後にする*/
}

.retry-button:hover{	/* マウスが乗ったとき */
	box-shadow: 3px 3px 7px #555;	/* 少し影を濃く */
	transform: translateY(-0.18em);	/* 上方向に少し移動 */
}

/* 正解の〇 */
.ok{
	padding-left: 1em;
	font-weight: 900;
	color: royalblue;
}

/* 不正解の× */
.ng{
	padding-left: 1em;
	font-weight: 900;
	color: tomato;
}

/* メディアクエリ設定 */
@media screen and (min-width:376px) and (max-width:960px) {
/* タブレット用のcssを記述 */
	header h1{
		padding: 20px;
		font-size: 16pt;
	}
	div#contents{
		width: auto;
		margin: 10px 10px;
		padding: 1em;
		font-size: 14pt;
	}
	footer{
		padding: 20px;
	}
}
 
@media screen and (max-width:376px) {
/* スマホ用のcssを記述 */
	header h1{
		padding: 20px;
		font-size: 14pt;
	}
	div#contents{
		width: auto;
		margin: 0 0;
		padding: 5px 10px;
		font-size: 12pt;
	}
	footer{
		padding: 20px;
	}
	small{
		font-size: 11pt;
	}
}

コメント

タイトルとURLをコピーしました