- 投稿日:2019-10-11T22:07:57+09:00
23行のhtmlでマインスイーパーを作った(Qiitaで遊べるよ!!)
はじめに
以前、html+css+jsで作ったマインスイーパーの記事を書いたのだが200行のVue.jsでスネークゲームを作ったや100行のHaskellでスネークゲームを作ったの記事がトレンドに上がっているのを見つけ、自分もより短いプログラムでマインスイーパーを実装したくなった。
まずは100行を目指してプログラムを書いていたのだが、どうせならもっと短くしようと思いたった7行でテトリスを実装「七行プログラミング」とは、JavaScript ショートコードテクニック集(ES6含む)を参考にしてコードを圧縮した。
その結果、当初の目標を大幅に超える23行でマインスイーパーを実装できた。さあ、マインスイーパーで遊ぶのだ!
See the Pen HTML_Minesweeper_Min by T.D (@td12734) on CodePen.
大きい画面で遊びたい人はこちら
Qiitaだと横が30行ぐらいでゲームの盤面が潰れ始めるので大画面推奨。参考:Windows版における難易度
- 難易度:横×縦(地雷数)
- 初級:9×9(10)
- 中級:16×16(40)
- 上級:30×16(99)
操作説明
- 左クリック:マスを開ける
- 右クリック:空いていないマスの上に旗を立てる
- キーボード:盤面の縦横サイズ、地雷の数を入力する
※スマホで遊ぶ場合、長タップする事で右クリック可能です。
遊び方
マスをクリックして、地雷が無いマスを全て開けるゲームです。地雷のマスをクリックしたら負け。
開けたマスに書いてある数字はそのマス周囲8マスのうち、地雷があるマスの個数です。
やり直しは「Start」ボタンを押してください。
上の入力フィールドに数値を入力し、ゲームの難易度を変更することも可能です。プログラム
コード
ゲームは以下のコードで実装した。七行プログラミングのルールに従い、1行は79文字以下としている。
<body>w<input id=W type=number required value=9 min=8 max=70>h<input id=H type= number required value=9 min=8 max=25>*<input id=M type=number required value=9 min=9><input type=button value=Start onclick=S()><p id=p><table id=t style= "border:solid;border-collapse:collapse"><script>window.onload=S=function(){if(R (W))W.value=9;if(R(H))H.value=9;M.max=((w=W.value)-1)*((h=H.value)-1);if(R(M))M .value=10;m=f=z=M.value;s=performance.now();T();for(i=t.rows.length;i-->0;t. deleteRow(0));a=new Array(h);for(i=h;i-->0;){a[i]=new Array(w);r=t.insertRow(0) ;for(j=w;j-->0;){d=r.insertCell(o=a[i][j]=0);[d.w,d.h,d.style]=[j,i,"width:24"+ "px;height:24px;border:solid;text-align:center;cursor:default"];d.onclick= function(){if(!V()||this[v].match("[#-8]"))return;if(!o)do if(a[y=(Math.random( )*h)|0][x=(Math.random()*w)|0]<1&&(Math.abs(this.w-x)>1||Math.abs(this.h-y)>1) &&z--)a[y][x]=1;while(z);if(!a[this.h][this.w])N(this);else if(f="Lose")for(i=h ;i-->0;)for(j=w;j-->0;)if(a[i][j])t.rows[i].cells[j][v]="※"};d.oncontextmenu= function(){if(V()&&!this[v].match("[*-8]"))if(!this[v]&&f-->0)this[v]="#";else if(++f)this[v]="";return !1}}}};T=()=>((p[v="innerHTML"]=((k=((e=((performance. now()-s)/1e3)|0)/60)|0)<10?"0":"")+k+((l=e%60)<10?":0":":")+l+(+f>=f?" #":" ")+ f)&&V())?setTimeout(T):0;V=()=>+f>=f;R=r=>!r.reportValidity();function N(c){if( !c||c[v].match("[*-8]"))return;c.style.background="gray";if(~c[v].indexOf("#") &&f++)T();if(!(c[v]=(a[y=c.h][(x=c.w)-1]>0)+(a[y][x+1]>0)+(y>0&&a[y-1][x-1]>0)+ (y>0&&a[y-1][x]>0)+(y>0&&a[y-1][x+1]>0)+(y<h-1&&a[y+1][x-1]>0)+(y<h-1&&a[y+1][x ]>0)+(y<h-1&&a[y+1][x+1]>0))&&(c[v]="-"))for(c.i=9;c.i-->0;)if(c.i!=4)N(((X=c.w +1-(c.i/3|0))>=0)*((Y=c.h+1-c.i%3)>=0)*(X<w)*(Y<h)>0?t.rows[Y].cells[X]:0);if( ++o>=w*h-m)f="Win"}</script>何書いてあるか分からないと思うので書き下したコードを以下に貼る。
<body> w<input id=W type=number required value=9 min=8 max=70> h<input id=H type=number required value=9 min=8 max=25> *<input id=M type=number required value=9 min=9> <input type=button value=Start onclick=S()> <p id=p> <table id=t style="border:solid;border-collapse:collapse"> <script> window.onload = S = function () { if (R(W)) W.value = 9; if (R(H)) H.value = 9; M.max = ((w = W.value) - 1) * ((h = H.value) - 1); if (R(M)) M.value = 10; m = f = z = M.value; s = performance.now(); T(); for (i = t.rows.length; i-- > 0; t.deleteRow(0)); a = new Array(h); for (i = h; i-- > 0;) { a[i] = new Array(w); r = t.insertRow(0); for (j = w; j-- > 0;) { d = r.insertCell(o = a[i][j] = 0); [d.w, d.h, d.style] = [j, i, "width:24px;height:24px;border:solid;text-align:center;cursor:default"]; d.onclick = function () { if (!V() || this[v].match("[#-8]")) return; if (!o) do if (a[y = (Math.random() * h) | 0][x = (Math.random() * w) | 0] < 1 && (Math.abs(this.w - x) > 1 || Math.abs(this.h - y) > 1) && z--) a[y][x] = 1; while (z); if (!a[this.h][this.w]) N(this); else if (f = "Lose") for (i = h; i-- > 0;)for (j = w; j-- > 0;)if (a[i][j]) t.rows[i].cells[j][v] = "※" }; d.oncontextmenu = function () { if (V() && !this[v].match("[*-8]")) if (!this[v] && f-- > 0) this[v] = "#"; else if (++f) this[v] = ""; return !1 } } } }; T = () => ((p[v = "innerHTML"] = ((k = ((e = ((performance.now() - s) / 1e3) | 0) / 60) | 0) < 10 ? "0" : "") + k + ((l = e % 60) < 10 ? ":0" : ":") + l + (+f >= f ? " #" : " ") + f) && V()) ? setTimeout(T) : 0; V = () => +f >= f; R = r => !r.reportValidity(); function N(c) { if (!c || c[v].match("[*-8]")) return; c.style.background = "gray"; if (~c[v].indexOf("#") && f++) T(); if (!(c[v] = (a[y = c.h][(x = c.w) - 1] > 0) + (a[y][x + 1] > 0) + (y > 0 && a[y - 1][x - 1] > 0) + (y > 0 && a[y - 1][x] > 0) + (y > 0 && a[y - 1][x + 1] > 0) + (y < h - 1 && a[y + 1][x - 1] > 0) + (y < h - 1 && a[y + 1][x] > 0) + (y < h - 1 && a[y + 1][x + 1] > 0)) && (c[v] = "-")) for (c.i = 9; c.i-- > 0;)if (c.i != 4) N(((X = c.w + 1 - (c.i / 3 | 0)) >= 0) * ((Y = c.h + 1 - c.i % 3) >= 0) * (X < w) * (Y < h) > 0 ? t.rows[Y].cells[X] : 0); if (++o >= w * h - m) f = "Win" } </script>結構長くなったように見えるがこれでも49行しかない。
ただ、コードの解読はまだ困難だと思うので以下で説明を行う。変数、関数の説明
コード圧縮の都合上、全ての変数名と関数名は1文字になっている。
その結果、可読性が大幅に失われたのでここでは変数名、関数名について説明する。
変数、関数名
- 1文字に圧縮する前に付けたであろう名前
- この行以降は動作説明など
グローバル変数
a
- mineArray
- 地雷があるか否かを格納した配列
- 0なら空きマス、1なら地雷
d
- td
- テーブルのセル
e
- elapsedTime
- ゲーム開始から経過秒
f
- flags,finishString
- 持っている旗(残りの地雷)の数
- 地雷を踏んだ時、地雷以外のマスを全て開けた時はゲーム結果の文字列を格納する
h
- height
- 盤面の縦サイズ
i
,j
- お馴染みのループ調整変数
k
- 特に無し
- 経過分数
l
- 特に無し
- 経過秒数
m
- mines
- 地雷の数
o
- opendCells
- 既にクリックして開けたマスの数
p
- pElement
- 経過時間、残りの旗の数、勝敗を表示するpタグの要素
r
- tr
- reportValidity
- 初期化時は
R
の引数の役割を果たす- その後、テーブルの行を格納する
s
- startTime
- ゲーム開始時間
t
- tableElement
- ゲームの盤面
v
- innerHTMLValue
- 文字列の"innerHTML"
w
- width
- 盤面の横サイズ
x
- xRandom
- 地雷敷設時に使用する、ランダムな横の座標
- 周囲の地雷の数を数える時、横の座標を格納する
y
- yRandom
- 地雷敷設時に使用する、ランダムな縦の座標
- 周囲の地雷の数を数える時、縦の座標を格納する
z
- 特に無し
- 地雷敷設時に使用する、敷設すべき残りの地雷数
H
- HeightInput
- 縦サイズの入力フィールド
M
- MineInput
- 地雷の入力フィールド
W
- WidthInput
- 横サイズの入力フィールド
X
- 特に無し
- 周囲のマスを開ける時、横の座標を格納する
Y
- 特に無し
- 周囲のマスを開ける時、縦の座標を格納する
ローカル変数
c
- cell
- セル
h
- height
- セルの縦座標
i
- お馴染みのループ調整変数
w
- width
- セルの横座標
関数
N
- NoMineCellClick
- 地雷が無いマスをクリックしたときの処理を行う
R
- ReportInValidity
- 入力フィールドの数値が
不正
か- 不正ならtrue
S
- Start
- Startボタンを押した時やページを開いた時に初期化処理を行う
T
- TextChange
- タイマー,旗の数のテキストを変更する
V
- isValidGame
- ゲームが有効か(終了していないか)
- fが数値(旗の数)ならtrue
動作説明、工夫点
全部解説が必要そうだが、面倒なので一部だけ解説を行う。
気が向けば全部解説するかも。html部分
<script>
タグ以外は閉じタグが無くても動作するので、閉じタグを消して文字を削減している。
属性値の"
もstyle以外は無くても動くので省略している。
また、idを設定しているが、idと同じ名前の変数を宣言していなければgetElementById
をしていなくてもそのidを持つ要素にアクセス可能。変数への数値の代入方法
地雷の個数の設定を行う以下の3行を見て頂きたい。
M.max = ((w = W.value) - 1) * ((h = H.value) - 1); if (R(M)) M.value = 10; m = f = z = M.value;これを分かりやすく書くとこうなる。
w = W.value; h = H.value; M.max = (w - 1) * (h - 1); if (R(M)) { M.value = 10; } m = M.value; f = M.value; z = M.value;変数は文中でも値を設定可能なので、w,hは初めて使われるM.maxの設定時に代入する。
この行では変数-1をする都合上カッコで括っているので文字数は変化しないが、括る必要がなければ変数名とセミコロンの2文字を省略できる。
このような代入をif文中を含めあらゆる所で行い、文字数をちまちま消している。
また、値が同じ変数は一度に代入可能である。
今回はm,f,zはどれもM.valueの値を取るので一度に代入している。小数点以下を切り捨てる
切り捨ては通常
Math.floor()
を使うがこれは長すぎる。
なのでビット演算を利用して{数値}|0
で小数点以下を切り捨てている。ゲームが終了していないかどうか
V = () => +f >= f;上のようにゲームが終了していないかどうかを判断しているが、分かりやすく書くと下と同じことをやっている。
function V () { return isNumber(f); }
f
には周囲の地雷の個数、つまり1から8までの数字と勝敗、つまりWinとLoseの文字列が入り得るがfが数字ならゲームが終わっていない(true)を返す。
ただ、isNumberは長いのでfの前に+を付けて数字に変換し、変換前後で値が変わっていないかで数字かどうかを判断している。
仮にfが1なら1 >= 1 → true
、fがLoseならNaN >= Lose → false
のように判断される。開けたマスの周囲のマスの地雷の個数を調べ、0個なら周囲のマスを全部開ける
実は最後の方のこの1行だけでこれらの処理をやっている。
if (!(c[v] = (a[y = c.h][(x = c.w) - 1] > 0) + (a[y][x + 1] > 0) + (y > 0 && a[y - 1][x - 1] > 0) + (y > 0 && a[y - 1][x] > 0) + (y > 0 && a[y - 1][x + 1] > 0) + (y < h - 1 && a[y + 1][x - 1] > 0) + (y < h - 1 && a[y + 1][x] > 0) + (y < h - 1 && a[y + 1][x + 1] > 0)) && (c[v] = "-")) for (c.i = 9; c.i-- > 0;)if (c.i != 4) N(((X = c.w + 1 - (c.i / 3 | 0)) >= 0) * ((Y = c.h + 1 - c.i % 3) >= 0) * (X < w) * (Y < h) > 0 ? t.rows[Y].cells[X] : 0);かなり強引に1行にしたのだが、分かりやすく書くとこうなる。
y = c.h; x = c.w; //c[v](c.innerHTML)に周りの地雷の個数を入れる c[v] = (a[y][x - 1] > 0) + (a[y][x + 1] > 0) + (y > 0 && a[y - 1][x - 1] > 0) + (y > 0 && a[y - 1][x] > 0) + (y > 0 && a[y - 1][x + 1] > 0) + (y < h - 1 && a[y + 1][x - 1] > 0) + (y < h - 1 && a[y + 1][x] > 0) + (y < h - 1 && a[y + 1][x + 1] > 0); if (c[v] === 0) { c[v] = "-"; for (c.i = (9 - 1); c.i > 0; c.i--) { X = c.w + 1 - (c.i / 3 | 0); Y = c.h + 1 - c.i % 3; //自分と同じセルでは無く、範囲外のセルでもない時 if (c.i != 4 && X >= 0 && Y >= 0 && X < w && Y < h) { //周りのマスを開ける N(t.rows[Y].cells[X]); } } }ここの処理を読む上で重要になって来るのは
true
は1
、false
は0
と判断される認識である。
例えば(a[y][x - 1] > 0)
がtrueなら1、falseなら0が入る。
- 投稿日:2019-10-11T16:18:24+09:00
A-FrameでWebVRアプリケーションを作成する-クリックイベントを追加してみる
はじめに
前回の記事ではA-Frameを使って色々なオブジェクトを配置しました。今回はこれらに動きを加えていきたいと思います。
準備
準備としてオブジェクトを配置します。前回と同様にデモからソースを引っ張ってきました。
index.html<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello, WebVR! • A-Frame</title> <meta name="description" content="Hello, WebVR! • A-Frame"> <script src="https://aframe.io/releases/0.9.2/aframe.min.js"></script> </head> <body> <a-scene background="color: #FAFAFA"> <a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9" shadow></a-box> <a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E" shadow></a-sphere> <a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D" shadow></a-cylinder> <a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4" shadow></a-plane> </a-scene> </body> </html>オブジェクトの色を変える
それでは早速イベントを追加していきます。クリックをした時に、視点の中心にあるオブジェクトの色を変えてみたいです。
まずは今のままでは視点の中心が分かりにくいのでカーソルを表示します。<a-scene>タグ内に以下を挿入します。
<a-camera> <a-cursor></a-cursor> </a-camera>
色を変更するイベントを追加します。クリックの度に変更する色を赤、緑、青と変化させるようにしました。
イベントの書き方は公式のドキュメントを参考にしています。
https://aframe.io/docs/0.9.0/introduction/javascript-events-dom-apis.html<script> var i = -1; var COLORS = ['red', 'green', 'blue']; AFRAME.registerComponent('camera-listener', { init: function () { this.el.addEventListener('click', function (evt) { i = (i + 1) % COLORS.length; this.setAttribute('material', 'color', COLORS[i]); }); } }); </script>各オブジェクトにこのイベントを割り当てます。"camera-listener"を追記するだけです。
<a-box camera-listener position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9" shadow></a-box> <a-sphere camera-listener position="0 1.25 -5" radius="1.25" color="#EF2D5E" shadow></a-sphere> <a-cylinder camera-listener position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D" shadow></a-cylinder>オブジェクトを移動させる
次にオブジェクトを移動させてみましょう。動かしたいオブジェクトをクリック、持っていきたい位置にマウスを動かし再度クリックしたらそこに配置します。
動かしやすいように床を大きくし、オブジェクトを一つだけにしました。<a-scene background="color: #FAFAFA"> <a-box position="0 0.5 -3" height="1" color="#4CC3D9" shadow></a-box> <a-plane position="0 0 -4" rotation="-90 0 0" width="10" height="10" color="#7BC8A4" shadow></a-plane> </a-scene><script> AFRAME.registerComponent('move', { init: function () { var isMove = false; this.el.addEventListener('click', function (evt) { var mode = document.getElementById("mode"); isMove = !isMove; mode.setAttribute('visible', isMove); }); var plane = document.getElementById('plane1'); plane.addEventListener('mouseenter', function (evt) { var box = document.getElementById("box1"); if(isMove) { evt.detail.intersection.point.y += box.getAttribute('height') / 2; box.setAttribute('position', evt.detail.intersection.point); } }); } }); </script><a-camera cursor="rayOrigin: mouse;"></a-camera> <a-box move id="box1" position="0 0.5 -3" height="1" color="#4CC3D9" shadow></a-box> <a-plane id="plane1" position="0 0 -4" rotation="-90 0 0" width="10" height="10" color="#7BC8A4" shadow></a-plane> <a-text id="mode" value="Move" color="#000" position="-0.9 2 -3" scale="1.5 1.5 1.5" visible="false"></a-text>解説
var isMove = false; this.el.addEventListener('click', function (evt) { var mode = document.getElementById("mode"); isMove = !isMove; mode.setAttribute('visible', isMove); });これは箱をクリックした時のイベントです。クリックの度にisMoveを書き換えながら、"Move"の表示非表示を切り替えます。
var plane = document.getElementById('plane1'); plane.addEventListener('mouseenter', function (evt) { var box = document.getElementById("box1"); if(isMove) { evt.detail.intersection.point.y += box.getAttribute('height') / 2; box.setAttribute('position', evt.detail.intersection.point); } });これは地面にマウスカーソルが当たる度に呼ばれるイベントです。isMoveが真ならばマウスカーソルの位置に移動します。
<a-camera cursor="rayOrigin: mouse;"></a-camera>これはマウスでオブジェクトを操作したかったので追加しています。詳細は公式のドキュメントをご確認ください。
https://aframe.io/docs/0.9.0/components/cursor.htmlおわりに
今回はオブジェクトの色を変えたり、移動させたりしました。
次回はこれらを使った簡単なゲームを作ってみたいです。
- 投稿日:2019-10-11T15:06:14+09:00
東京高専プロコンゼミ総勢32名の名刺をHTMLで刷った
第30回高専プロコン、突如出現した移動部門で盛り上がっていますね。
私は今回自宅待機を選んだので酷い目に合わずほっとしているのですが、よく考えると参戦勢よりも関東自宅待機勢の方が酷い目に合うのでは?と思ってるところです1。さて、プロコンといえば「名刺クエスト」。高専生同士が名刺を交換し、交換した名刺の数で勝敗を決めるという、プロコンの中でも最もフェアな部門です。
東京高専の名刺のデザインは一昨年から私が担当していました。友人のMacに入っているCreative Cloudのイラレを使ってデザイン・印刷を行っていましたが、今年は彼が貸してくれないので、HTMLとCSSで書くことにしました。
この記事を参考にしました。
paper.cssを使ってブラウザだけで名刺を印刷する
https://qiita.com/okoppe8/items/abcafdad3a894bca7f38名刺をデザインする
名刺のデザインをどうしようかな~と考えてた時に、ちょうどポケットの中に入ってたのは定期券でした。
IC定期券風名刺を刷れば面白いんじゃないか、と思いました。東京高専ですし、緑色なのはまだしもピンク色のは東京の象徴と言えます。
また、通生が比較的多い(多分)東京高専の象徴とも言えます。そこでこんなデザインを作りました。
また、一応緑色風と真面目なデザインも作っておきました。
カードの外形
91mm×55mmなので、こんな感じに指定しています。
meishi.csscard { position: absolute; width: 91mm; height: 55mm; }背景の文字
ピンク色定期券の文字は、Google Fontsで色々と検討した結果、Nova Monoがイメージに近いという結論に至りました。Google Fontsの読み込んで使っています。
上の文字
定期券風の文字は、通学定期を参考にして描きました。基本的にFlexboxレイアウトにして、配置していきます。
定期上の文字は横幅2倍だったり半分だったりするのですが、これはcssのtransform要素で解決できます。teiki.css.double { transform: scale(2, 1); /* width方向に2倍 */ }ただし、transformには重大な欠点がありまして、Flexboxによる配置後にtransformが実行されるようなのです。
例えば、上の名刺で言う「仕事」は半幅、「名刺作り……」2倍幅のtransformを実行しているのですが、Flexboxが配置された後にtransformされるので、何もしなければ変なマージンが空いてしまいます。transform: matrixを使って平行移動も噛ませることでなんとかしましたが、どうにかならないのかな、と思う仕様でした。
また、矢印はfontawesomeで実装しています。
名刺のPDF生成
名刺は一度10枚単位の名刺用紙に刷り、そこから切り取り線に沿って切り取るという方式です。
そのため、名刺用紙に刷るPDFが必要でした。今回取ったフローとしては、以下の通りです。
- Google FormでSpreadsheet上にデータを集める
- Spreadsheet上のデータをクリップボード経由でExcelに出し、CSVで出力
それが完了すれば、次のようなコードで自動的にPDFに変換できます。
- CSV読み込み
- データに対し以下を繰り返す。
- 対象データをテンプレートエンジンに掛けて、HTMLを生成
- そのHTMLをHTTPサーバーで配信
- google-chromeのヘッドレスモードを使い、--print-to-pdfでPDF生成
- HTTPサーバーを閉じる
今思えばHTTPサーバーでまとめて配信すれば良かったのですが、プログラムの複雑性の問題や時間がなく適当に書き殴りたかったのでforループ一個で済むこの方式になりました。
まず予めHTMLのテンプレートを作成しました。上のHTMLとCSSをコピーしてきて、名前欄やID欄などをGolangのhtml/template方式で変数展開に書き換えた後、JavaScriptで10枚に増やす処理を追加しました。
するとこんな感じになりました
後はこれをHTTPサーバーで配信して、ChromeのheadlessモードでPDF生成すればPDFの出来上がりで、このPDFをサイズ100%で刷れば名刺が完成します。
何人も連続で刷る場合、PDFを連結して刷った方が良いです。Popplerに付属するpdfuniteが使いやすかったです。
困ったこと・反省点
- transformの仕様
- Go言語を選んだのはミス。ChromeとHTMLテンプレートエンジンを使うのであればNode.jsが相応しかった
- Macの印刷設定がまいどまいどサイズ95%に戻るせいで、慣れないうちは何度かミスプリントしてしまった。
- 全般的に人力が多かったので限界っぽくなった。次はもっと自動化したい。
- 緑色の定期券も作ろうとしたのだが、PDFから直接印刷しようとすると変な模様が出てしまう。画像化すれば問題はないものの、プリンタの読み込みに時間がかかるので、量産できなかった。
最後に
プロコン期間中、東京高専プロコンゼミと名刺交換して伝説の高専生の本名を手に入れよう!
本記事には登場しなかった「真面目なデザイン」「緑色の定期券風」の名刺も手に入る!2
- 投稿日:2019-10-11T14:45:29+09:00
JavaScriptでマインスイーパをつくる
動機
暇なときになんとなくマインスイーパを作ってみたら楽しかったので、記録として。
(ゲーム終了判定などは割愛してます。。)マインスイーパのルールと処理
遊び方はここに分かりやすく載っています。
http://dotpico.com/mine/ja/rule.php左クリックの処理
- クリックすると、そのセルを開くことができる
- そのセルに爆弾が入っていたら、その時点でゲームオーバー
- そのセルに爆弾が入っていなければ、周囲に存在する爆弾の数が表示される
- そのセルに爆弾が入っておらず、周囲に1つも爆弾が存在しないとき、周囲のセルが自動的に開かれる
右クリックの処理
- 1回右クリックすると、そのセルに「フラグ」をつけることができる
- 2回右クリックすると、そのセルに「?」をつけることができる
- 3回右クリックすると、元に戻る
ダブルクリックの処理
- 開いたセルに書いてある数字と、その周囲の「フラグ」の数が一致しているとき、そのセルをダブルクリックすると、「周囲の開かれていないセル」を一度に開くことができる
実装プログラム
index.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <style> td { padding: 0px; width: 40px; height: 40px !important; text-align: center; border: 1px solid rgb(60, 60, 60); } /** 未開封のクラス */ .closed { background-color: lightgray; color: black; cursor: pointer; } /** 開封して爆弾だったときのクラス */ .bombed { background: red; color: white; cursor: auto; } /** 開封して爆弾ではなかったときのクラス */ .opened { background: white; color: black; cursor: auto; } </style> </head> <body> <div id="msg"></div> <table> <tbody id="target"> <!-- 中身はJavaScriptでつくる --> </tbody> </table> <script src="main.js" type="text/javascript"></script> </body> </html>main.js'use strict'; //=================================== // マインスイーパ用のセルクラス // セルはこのクラスのインスタンスとする //=================================== class MSCell extends HTMLTableCellElement { //----------------------------------- // コンストラクタ //----------------------------------- constructor() { super(); // イベント登録 this.addEventListener('click', this.clickFunc); this.addEventListener('contextmenu', this.clickRightFunc); this.addEventListener('dblclick', this.clickDblFunc); } //----------------------------------- // マインスイーパ初期設定 // x座業、y座標、爆弾かどうかをパラメータにとる //----------------------------------- init(x, y, bombFlg) { // 開封フラグ(未開封のときfalse/開封済みのときtrue) this.openedFlg = false; // x座標 this.x = x; // y座標 this.y = y; // 爆弾フラグ(爆弾のときtrue/爆弾でなければfalse) this.bombFlg = bombFlg; // 見た目のクラス this.classList.add('closed') } //----------------------------------- // 周辺セルを設定する // 周辺セルと、周辺セルの合計爆弾数を設定する //----------------------------------- setArounds(arounds) { // 周辺セル this.arounds = arounds; // 周辺セルの爆弾数 this.aroundBombCount = this.arounds.filter(around => around.bombFlg).length; } //----------------------------------- // そのセルの中身を表示する //----------------------------------- show() { if (this.bombFlg) { // 爆弾のときは「爆」 this.textContent = '爆'; // 見た目の変更 this.classList.remove('closed'); this.classList.add('bombed'); } else { // 爆弾ではないとき if (this.aroundBombCount > 0) { // 周辺の爆弾数が1個以上のときは数を表示 this.textContent = this.aroundBombCount; } // 見た目の変更 this.classList.remove('closed'); this.classList.add('opened'); } } //----------------------------------- // セルを左クリックしたときの関数 //----------------------------------- clickFunc() { if (this.openedFlg) { // 開封済みのときは何もしない return; } if (this.textContent === '旗' || this.textContent === '?') { // 「旗」や「?」がついてるときも何もしない return; } // 開封済みにする this.openedFlg = true; // このセルを開く this.show(); if (this.bombFlg) { // このセルが爆弾のときはゲームオーバーなので全セルを開く msCells.forEach(button => button.show()); } else { // このセルが爆弾でないとき if (this.aroundBombCount === 0) { // 周囲に爆弾が無いときは周囲のセルを全部開く this.arounds.forEach(around => around.clickFunc()); } } } //----------------------------------- // セルを右クリックしたときの関数 //----------------------------------- clickRightFunc(e) { // 右クリックメニュー禁止 e.preventDefault(); if (this.openedFlg) { // 既に開かれている場合は何もしない return; } if (this.textContent === '') { // 旗を表示 this.textContent = '旗'; } else if (this.textContent === '旗') { // ?を表示 this.textContent = '?'; } else if (this.textContent === '?') { // 元に戻す this.textContent = ''; } } //----------------------------------- // セルをダブルクリックしたときの関数 //----------------------------------- clickDblFunc() { if (!this.openedFlg) { // 既に開かれている場合は何もしない return; } // 周囲の旗の数を取得 let flgCount = this.arounds.filter(around => around.textContent === '旗').length; // 周囲の旗の数と、クリックしたセルに表示されている爆弾数が一致していれば // 周囲のセルをすべて開く if (this.aroundBombCount === flgCount) { this.arounds.forEach(around => around.clickFunc()); } } } //=================================== // カスタム要素の定義 //=================================== customElements.define('ms-td', MSCell, { extends: 'td' }); //=================================== // 全セルを格納しておく変数 //=================================== let msCells = []; //=================================== // ゲーム初期化用関数 //=================================== let initGame = function (xSize, ySize) { // ボタン配置 for (let y = 0; y < ySize; y++) { let tr = document.createElement('tr'); for (let x = 0; x < xSize; x++) { // セルを作る let msCell = document.createElement('td', { is: 'ms-td' }); // セルの初期化 msCell.init(x, y, Math.random() * 100 < 10); // セルをtrにいれておく tr.appendChild(msCell); // msCellsにも入れておく msCells.push(msCell); } document.getElementById('target').appendChild(tr); } // aroundsの設定 msCells.forEach(msCell => { // 周囲8マスを取得 let arounds = msCells.filter(otherCell => { if (msCell === otherCell) { return false; } let xArea = [msCell.x - 1, msCell.x, msCell.x + 1]; let yArea = [msCell.y - 1, msCell.y, msCell.y + 1]; if (xArea.indexOf(otherCell.x) >= 0) { if (yArea.indexOf(otherCell.y) >= 0) { return true; } } return false; }); // 周囲8マスをaroundsとして設定 msCell.setArounds(arounds); }); } //=================================== // ゲーム初期化 //=================================== initGame(15, 15);実装のポイント
セルの作り方
JavaScriptのcreateElementでtableタグの中身をつくっていますが、
その中のtdタグは、独自に定義したカスタム要素「MsCell」です。(Msはマインスイーパの略)
MsCellはHTMLTableCellElementを継承しています。そのため、見た目は普通のtdタグと同じですが、
マインスイーパに必要なopenedFlgなどのプロパティと、clickFuncなどの関数を持っています。セルに対する動作
ポイントは二つです。
開いたセルに爆弾が入っていなければ、そのセルの周囲に存在する爆弾の数が表示される
つまり、セルはその周囲のセルの中身を知っている必要があります。
そこで、セルにaroundsというプロパティを持たせ、周囲の8セルを参照させておくことにしました。開いたセルに爆弾が入っておらず、そのセルの周囲に1つも爆弾が存在しないとき、周囲のセルが自動的に開かれる
クリックされたセルの周囲の爆弾数で0であれば、そのセルのaroundsもすべて再帰的にクリックしていきます。
また、再帰処理を抜けるための条件として、openedFlg(未開封:false/開封済:true)を持たせています。参考文献
- 投稿日:2019-10-11T11:38:38+09:00
line-heightについて考えませんか
Webサービスの制作におけるline-heightの扱い方とかをまとめました。
コーディングのポイント
CSSをリセットしてから、bodyにline-heightを入れていると思いますがそれは大きな間違いです。
bodyにline-heightを入れてしまうと全ての要素に適応されてしまうため、buttonやinputにもline-heightが入ってしまいます。button { height: 56px; line-height: 56px; }上のように実装している場合はそこまで影響はないですが、下のように実装するとデザインに崩れが生じます。
button { padding: 16px 32px; }文字が一行で収まるような要素にはline-heightを1に指定し、複数行になる場合は個々にline-heightを指定すると良いです?
body { line-height: 1; } p { line-height: 1.8: } button { padding: 16px 32px; }和文フォントと欧文フォント
和文フォントの14pxと欧文フォントの14pxでは、和文フォントの方がはるかに大きく見えます。
仮想ボディの占有率が違うため、大きく見えてしまうのです。font-sizeは、和文は13〜16px、欧文フォントは15〜18px
line-heightは、和文は1.4〜1.8、欧文フォントは1.2〜1.4
くらいを目安に指定するとユーザビリティを考慮したものになると思います。太字と細字
上で、ある程度のline-heightがきまりましたが、太字と細字でもline-heightは変えた方が良いです。
強調の意図があって太文字を選ぶと思うので、そういうときは普通のline-heightより少し値を小さくしてあげると文字のバランスが整います(文字が1行で収まる想定の場合はこの記述はそもそもline-heightいりません)?body { line-height: 1; } h1, h2, h3 { line-height: 1.4: } p { line-height: 1.8; }印象のつけかた
line-height、font-sizeだけでサービスの印象が大きく変わります。
どんな印象にしたいかを考えて、サイズを決めていきます。以下、参考になれば。(スマホ画面での情報です。)
ニュース系メディア
ニュース系メディアの特徴はとにかく1画面に対しての情報量が多いです。
font-sizeは16px、line-heightは1.7前後。
左右marginは10px前後なのでmarginを大きく取らない代わりにline-heightを多めにとって、文字のつまりを最小限に抑えながら可読性を重視しています。女性向けキュレーションメディア
女性向けのメディアはかわいらしさを重視しています。
そのため、フォントサイズを小さめにしていて、letter-spacingをとっているケースもあります。
font-sizeは14px、line-heightは1.5前後。
写真と文章を切り分けるためにline-height少なめに、写真とのmarginは大きめに取っています。IT企業のブログ
font-sizeは16px、line-heightは1.8前後。
letter-spacingを入れていることもあります。
左右marginは多くとりませんが、letter-spacingを入れることで文字の間隔が空き、marginの狭さをカバーしつつ、1画面のコンテンツ表示量を多くしています。
漢字をたくさん含んでいる文章でも詰まっていると感じないように工夫されています。技術系メディア
font-sizeは16px、line-heightは1.8前後。
文章内にコードが挿入されるので、line-heightは多めにとっています。
文章間のmarginが少ないので、hrやコードをうまく利用する設計になっています。おわりに
参考になるものはまだまだたくさんあるので随時更新できればと思います。
メディアに限らず、文字の可読性は重要です。
読む時やクリック、タップをするときに少しでも違和感があるとそれだけで機会損失をする恐れがあります。
ターゲットを見極めて、綺麗なコードと見た目の設計を心がけましょう
- 投稿日:2019-10-11T10:44:27+09:00
中身を切り替えできるお手軽モーダルウィンドウの作り方(HTML/SCSS)
はじめに
中身を切り替えできるお手軽モーダルウィンドウの作り方。
使用言語
- HTML
- SCSS(CSSでも可)
コード
See the Pen JjjYvOG by k1-web (@k1-web) on CodePen.
やってること
ラジオボタンを判定にしてチェックされているかどうかで表示非表示を切り替えている。
「#close(閉じる)」か「#overlay(背景)」を押すなどのチェックしている状態だと、モーダルウィンドウを非表示にする。
「#handle-1」にチェックが付いていたら閉じる・背景・コンテンツを表示させる。
表示させるコンテンツは「#handle-1」に隣接する次の要素を表示させる。xxx.html<input type="radio" name="modal" id="close"> <input type="radio" name="modal" id="overlay" checked> <input type="radio" name="modal" id="handle-1"> <div id="modal-1" class="wrap wrap-1"></div>xxx.css#handle-1 + #modal-1 { display: none; } #handle-1:checked + #modal-1 { display: block; }背景がスクロールするのがイヤな人向け追加jQuery
PCとSPとで少し記述は違いますが、以下のjQueryを追加することで、背景スクロールも制御できます。
PC用
pc.js$('input[name="modal"], label.handle').on('change blur click focus', function(){ if ( $('input#close').prop('checked') || $('input#overlay').prop('checked') ){ $('html, body').removeAttr('style'); } else { $('html, body').css({'overflow':'hidden'}); } });SP用
sp.js$('input[name="modal"], label.handle').on('change blur click focus', function(){ if ( $('input#close').prop('checked') || $('input#overlay').prop('checked') ){ $('html, body').removeAttr('style'); $('body').off('.noScroll'); } else { $('html, body').css({'overflow':'hidden'}); $('body').on('touchmove.noScroll', function(e) { e.preventDefault(); }); } });
- 投稿日:2019-10-11T02:17:27+09:00
【初心者】営業から転職のために【備忘録2】
10/8~10/10 勉強したこと
- 背景に画像を入れる・その際は画像が繰り返し表示になるので、画面いっぱいに表示も可能
- 中央寄せの際はwidthを書いてからmargin:0 auto;
- 透過の書き方(全体と背景だけの分が違う)
- 文字の間隔・行間の書き方
- hoverのアニメーションのつけ方
- 要素の違い
- ブロック要素→要素が横いっぱい広がって縦に並ぶ
- インライン要素→平べったく横に並ぶ
- 幅は高さは指定できない
- インラインブロック要素→並びがインライン・中身はブロック
10/8~10/10 気を付けたいと感じたこと
- 前の記事でCSSを書く際じclass名の前に.を付け忘れることがあったが、class名でないときは.が不要なので気を付けたい。
- border-radius:〇〇px;は値が大きくなるほど丸に近くなる。
- padding:の左右だけ設定したい場合に、上下の0pxを入れ忘れることがあるので気を付ける。
振り返り
まだ覚えたことを自分の物にできていないので、復習の時間をしっかり取るようにしていきたい。
(一度書いたものをリセットしてスライドを見ないで書き直し)
すべて覚えきりたいが、まずは何ができるか・どんなことができたか
少しずつ進めていきたい。HTMLとCSSは18日までに復習含めてひと段落させたい。
やっていく中で、指示を見ずに見本を見て考えて出来るようにしていきたい。