はじめに
最近、スマホでボードゲームのようなものは作れないかと思い、色々妄想をしていたところに、
今回の夏祭りのイベントを知って、思い切って作ってみることにしました。
フルタイトルをつけるなら、**「普段javascriptを触ってない者が土日でjavascriptのみを使って、
今合コンカップル判定アプリを作るとしたら、色々とつまづきながらも、デザイン・UXは諦めつつ
なんとか形になりました」**と、どこかの小説サイトのようなタイトルです。
あと最初に謝っておきます。
javascriptのみと言っておきながら言語比率は100%ではなく96.2%です。
他の言語成分として html 3.1%、Shell 0.7% が含まれております。
使い方
流れとしては
フェーズ | 画面1 | 画面2 |
---|---|---|
1.メンバー登録(最大8人) | ||
2. 各個人アンケート(1台のスマホをメンバー参加者に順に回して右記の画面1、2を操作してもらいます。) | ||
3. カップル判定結果発表 |
となります。
開発中に苦労したポイント
その1 画面遷移
HTMLを最小限にするために、コントロールの書き出し、画面遷移もjavascriptのみで行っています。
普段であれば、hiddenタグを埋め込んだりして次画面のhtmlへ値を受け渡ししたりするのですが、
javascriptでその辺も全てjavascriptで制御しています。
特に画面内の入力値の読み取り、コントロールの削除、生成、画面遷移の制御方法 普段意識しないことを
ゴリゴリのパワープレイで誤魔化し誤魔化し乗り切っています。
参考サイト:DOMで動的にボタンを生成し、ボタンを削除するサンプルコード
参考サイト:【JavaScript】すべての子要素を削除するときは、cloneNode(false) して replaceChild するのが多分いちばん速い
参考サイト:【JavaScript】addEventListenerで関数に引数をわたす
その2 CORSエラー
FireFox にてテストしていましたが、セキュリティの関係上ローカルのファイルを読み込めないということがわかりました。
ブラウザの設定でセキュリティレベルを下げる(about:config -> security.fileuri.strict_origin_policy をfalse にする)か、Webサーバを起動するかといった方法で回避する必要があります。
今回は簡易Webサーバを立ち上げ回避しました。
その結果、簡易Webサーバを起動するためのシェルが0.7%含まれることになりました。
参考サイト:Firefox でローカルのHTMLが外部ファイル(Javascript等)を読み込めない問題の対応
参考サイト:ワンライナーWebサーバを集めてみた
その3 デバッグ
ブラウザの機能でF12でデベロッパー画面があるとはいえ、HTMLのタイポに気づかず半日程度悩みまくり、
こんな恥ずかしい質問もしてしまいました(苦笑)
開発に慣れていれば、すぐ気づけたのかなと思いますが、、、、(笑)
ユニットテストについては、最初から諦めました。
その4 カップリングの判定方法
ここの処理の仕方も悩みました。人数もそんなに多くならないので単純に全員が各個人を全員一つづつ両思いか調べてもいいのですが、なんとなく芸が無いのでこだわって作ってみました。
方法としては、ある人が好きな人の番号を保存しておき、好きな人の好きな人がある人の番号を指すか確認する様にしました。
これなら各個人が全員の相性を見ていくことにはならないので、効率的です。(たぶん)
ソース
#!/bin/bash
ruby -run -e httpd . -p 5000
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>合コンカップル判定</title>
</head>
<body><script src="js/main_screen.js"></script></body>
</html>
{
let members = [
{ "name": "名前 むつき", "fav": ""},
{ "name": "名前 きさらぎ", "fav": ""},
{ "name": "名前 やよい", "fav": ""},
{ "name": "名前 うつき", "fav": ""},
{ "name": "名前 さつき", "fav": ""},
{ "name": "名前 みなつき", "fav": ""},
{ "name": "名前 ふみつき", "fav": ""},
{ "name": "名前 はづき", "fav": ""}
]; // ここの値に意味はないですが、データ構造の参考です。
// 参加者登録
function screen1() {
let key = 0;
const message = document.createElement('h1');
message.innerText = '参加メンバーを登録してください';
document.body.appendChild(message);
members.forEach( function(user) {
var hdiv = document.createElement('div');
var memText = document.createElement('input');
memText.type = 'text';
memText.value = user['name'];
hdiv.appendChild(memText);
document.body.appendChild(hdiv);
});
const addButton = document.createElement('input');
addButton.type = 'button';
addButton.value ='アンケート開始';
addButton.addEventListener('click', screen1_post); // addButton.onclieck = screen1_post(); これだとscript読み込み時に動いちゃった。
document.body.appendChild(addButton);
}
function screen1_post(){
let elms = document.body.getElementsByTagName("input");
let joined_members = [];
for (let i=0; i<elms.length; i++){
if( elms[i].type == "text" && elms[i].value != "" ){
joined_members.push( { "name": elms[i].value, "fav": ""} );
}
};
members = joined_members;
screen2_1(0);
}
// 好きな人選択画面 カバー
function screen2_1(id) {
// 画面クリア
while( document.body.firstChild ){
document.body.removeChild( document.body.firstChild );
}
// 画面メッセージ
const userName = document.createElement('h1');
userName.innerText = members[id]['name'] + ' さん。';
document.body.appendChild(userName);
const message = document.createElement('h1');
message.innerText = '準備ができたら、下のボタンを押してください';
document.body.appendChild(message);
// ぼたん
const addButton = document.createElement('input');
addButton.type = 'button';
addButton.value ='ここ押して';
addButton.addEventListener('click', {"id": id, "handleEvent": screen2_2});
document.body.appendChild(addButton);
}
function screen2_2(id) {
// 画面クリア
while( document.body.firstChild ){
document.body.removeChild( document.body.firstChild );
}
// 画面作成
const message = document.createElement('h1');
message.innerText = members[this.id]['name'] + ' さん。どれか一つだけボタンを押してください。';
document.body.appendChild(message);
// メンバー一覧
for( let i=0; i<members.length; i++ ){
var hdiv = document.createElement('div');
var memText = document.createElement('span');
memText.innerText = members[i]['name'];
var favBtn = document.createElement('input');
favBtn.type = 'Button';
favBtn.value = ' 好み';
favBtn.addEventListener('click', {"id": this.id, "fav": i, "handleEvent": screen2_2_post});
var hateBtn = document.createElement('input');
hateBtn.type = 'Button';
hateBtn.value = 'ない ';
hateBtn.addEventListener('click', {"id": this.id, "fav": -1, "handleEvent": screen2_2_post});
hdiv.appendChild(memText);
hdiv.appendChild(favBtn);
hdiv.appendChild(hateBtn);
document.body.appendChild(hdiv);
};
}
function screen2_2_post(id, fav){
members[this.id]['fav'] = this.fav;
this.id++;
if( members.length > this.id ){
screen2_1(this.id);
} else {
screen3_1();
}
}
// 結果発表画面 カバー
function screen3_1() {
// 画面クリア
while( document.body.firstChild ){
document.body.removeChild( document.body.firstChild );
}
// 画面メッセージ
const message = document.createElement('h1');
message.innerText = '結果発表!';
document.body.appendChild(message);
// ぼたん
const addButton = document.createElement('input');
addButton.type = 'button';
addButton.value ='ここ押して';
addButton.addEventListener('click', screen3_2);
document.body.appendChild(addButton);
}
function love2_id(id){
let pos = -1;
if( Number.isFinite(members[id]['fav']) ){
pos = members[id]['fav'];
if( pos > 0 ) { // 自分 が 自分を好きは省くので 0 = 0 も除外する
let res = members[pos]['fav'];
if( res < 0 || res != id || pos <= id) {
pos = -1; // 既に検索済みのペア(pos <= id)も無視する
}
}
}
return pos;
}
function screen3_2(){
// 画面クリア
while( document.body.firstChild ){
document.body.removeChild( document.body.firstChild );
}
// 画面メッセージ
const message = document.createElement('h1');
message.innerText = '結果発表!';
document.body.appendChild(message);
// お互いに好きと入れた人検索
let love2 = [];
for( let i=0; i<members.length; i++){
let result = love2_id(i);
if( result >= 0 ){
love2.push( {"id": i, "fav": result} );
}
}
// 表示
if( love2.length > 0 ){
for( let i=0; i<love2.length; i++){
const message = document.createElement('h1');
message.innerText = members[love2[i]["id"]]["name"] + 'さん ♡' + members[love2[i]["fav"]]["name"] + 'さん';
document.body.appendChild(message);
}
const msg = document.createElement('h1');
msg.innerText = 'おめでとうございます!!'
document.body.appendChild(msg);
} else {
const message = document.createElement('h1');
message.innerText = '残念ながら、マッチングしませんでした。';
document.body.appendChild(message);
}
}
// 実行
screen1();
}
本ソースはGitHubにも登録してあります。
さいごに
スマホでボードゲームを作る利点として、司会進行役をスマホで代用すること というのがあると思います。
今回の記事ではカップリング判定アプリでしたが、司会進行役がスマホになることにより、
カップル成立しなかった場合、誰が誰を好きと押したのか知る方法がありませんので、安心して使えると思っています。
うまく応用すれば王様ゲーム版も作れるのじゃないかなと思います。
ただし、私は酒を受け付けない体質ですので、合コン自体行かないのですが(笑)