目次
02日 あけましておめでとうございます
11日 続・グノーシア
27日 node.jsの2つのイベント処理
毎年恒例の今年のおせち写真。
鶏ハム、豚の角煮(煮玉子も含む)、伊達巻。これが僕が作ったもの。
いくら も僕が作ったが、秋に筋子から作って冷凍してあった。
栗きんとんは、次女が中心になって長女も手伝い、最後の仕上げだけ僕が行ったもの。
松前漬けも次女によるものだが、すでに裁断した材料と、出汁汁がセットになったものを作っただけ。
酢蓮、煮〆は妻が作ったもの。
写真に写っていないが、金柑の甘露煮もつくった。
昆布巻、ままかり、ワカサギの甘露煮、かまぼこ、さくらんぼの練り切りは市販のものを並べただけ。
今年はコロナで初詣も親戚の集まりもなし。
家族でボードゲームなどしながら、のんびりと過ごしています。
先日のクリスマスプレゼントは、ナインタイル ポケモンドコダと、書籍「大人が楽しい トランプゲーム30選」。
ナインタイルは結構有名なパーティゲームだが、はじめてあそんだ。
単純なルールだがよくできていて、大人でも子供でも、初めて遊ぶ人でも楽しめる。
各プレイヤーには9枚のタイルが配られる。
その絵柄を、お題となるカードに描いてあるものと同じように、3x3 で並べるだけ。
一番早い人だけがポイントを獲得できる。
ただ、タイルは表裏に別の柄が描かれている。
このため、タイルをひっくり返して絵柄を見つける必要があるが、当たり前だが裏は見えないため必要な絵柄を素早く見つけ出すのは困難だ。
そして、「ポケモンドコダ」は、ナインタイルの絵柄を、ポケモンのキャラクターに変えたもの。
…なのだけど、これ、ちょっと遊びにくくなっていると思う。
元のゲームでは、幾何学模様なのでどちらから見ても良かったのだが、ポケモンキャラクターには「方向」があるため、テーブルを囲んで遊ぶときに、逆方向から見ている人が遊びづらいのだ。
大人が楽しい トランプゲーム30選 は、ボードゲーム販売店の「すごろくや」がまとめたもの。
有名な定番ゲームは載っておらず、有名ではないが面白いゲームや、近年考案されたものを中心にしている。
近年のものについては、ゲームの考案者の名前が記されているのだが、すごろくやが考えているものもあったりする。
1プレイにかかる時間も、5分程度から1時間超まで目安が書かれている。
まだ、せいぜい10分程度で終わるゲームしか遊んでいない。
また、ゲームによっては人数制限が厳しい(偶数人数でしか遊べない、4人限定、など)とか、トランプと別にチップを必要とするとか、遊びにくくする要素もある。
そういうものも、残念ながらまだ遊んでいない。
しかし、トランプは汎用性が高い。
…というのも、上の書籍に書いたゲームを色々と試していたのだが、結局定番ゲームも遊び始めてしまうからだ。
スピード、51は家族の定番ゲーム。
今年はこれに、対戦ソリティアが加わった。
同じサイズのトランプが2セット必要だからなかなか遊ぶのは難しいのだけど、あったので。
対戦ソリティアは、チェックめいと!という漫画で紹介されていて知った。
ソリティアって一人遊びの意味なのだけど、それで対戦するゲーム。
非常に面白い。
遊ぼうと思ったときに2人しか集まらなかったときは、対戦ソリティアかスピードが定番ゲームになっている。
(テレビゲームを遊んでいる最中の人とか、2人しか集まらないときはあるので)
しかし、この漫画自体、すごろくや協力なのだよね…。
すごろくやは僕としては以前から信頼しているのだが、ただの「ゲーム販売店」ではなく、積極的に面白いゲームを紹介していく姿勢が好き。
同じテーマの日記(最近の一覧)
別年同日の日記
申し訳ありませんが、現在意見投稿をできない状態にしています。 |
グノーシア、というゲームについては、購入して少し遊んだ時点で一度書いている。
我が家では、家族全員が気に入って繰り返し遊んでいる。
いち早く「完全クリア」した長女は、その後もわずかな暇があるときに遊び続けている。
(まとまった時間があるときはスプラ2やるとか、使える時間に応じてゲームを変えている。
グノーシアは、1回5分程度から遊べる)
さて、僕も少しづつ遊び続け、年末年始の休みでやっと完全クリアに至った。
やはりいいゲームだった。感想を書いておこう。
ここで「完全クリア」と書いてあるのは、単にエンディングを見たのではない、という程度。
本当は、きっとまだ「完全」ではないだろう。
グノーシア自体は、人狼を元にしたゲームだ。
閉鎖された宇宙船内で、人間に感染し、人間に敵対する「グノーシア」と多数決で戦うゲーム。
見た目は人間と変わらないため、外見からは判別できない。
ただし、船に一人だけ乗り込んでいる「エンジニア」は、船が空間転移…定期的に行われる、ワープ航行の際の特殊な状況を利用して、一人だけグノーシアかどうかを検査できる。
しかし、それで誰がグノーシアかを確定できるのは、当のエンジニアのみ。
グノーシアは「嘘をつく」ことができるため、エンジニアを名乗って情報を混乱させる。
本人以外は、情報が本物か偽物かもわからない。
ゲーム上、5回の「話し合い」が1ターンで、ターンごとに投票して一人の「コールドスリープ」を決定する。
ここは人狼に比べて穏やかなところ。人狼では投票で「処刑」するのだが、眠ってもらうだけだ。
そして、コールドスリープ状態にあるものは、これも船で一人だけの「ドクター」が検査して、人間かグノーシアかを判別できる。
もちろん、ドクターを名乗って情報を混乱させるグノーシアもいる。
グノーシアも、ただやられるのを待つだけではない。
ターンの最後に、一人だけ「消滅」させることができる。
空間転移の際の特殊な状況を利用し、人間を一人、完全に消し去るのだ。
このほか、絶対に人間であると互いに保証できる、二人一組の「留守番」、人間だがグノーシアを崇拝し嘘をつく「AC主義者」、存在していることが異常で、最後まで残ると宇宙を崩壊させる「バグ」など、いくつかの役職があり、それらの存在が状況を複雑にする。
ただし、ここまでは単に舞台や役職名を置き換えただけの人狼にすぎない。
グノーシアには、ストーリーがある。
いや、ストーリーと言ってよいのかどうか…世界観、という方が良いかもしれない。
グノーシアとは何なのか。なぜ人間に敵対するのか。
なぜ、どうやって人間を「消滅」させるのか。
1回のプレイは、グノーシアを全員眠らせて人間が勝利するか、もしくはグノーシアが過半数を占めて船を乗っ取るかで終わる。
しかし、時々この世界についての情報の断片を入手できる。
それは、登場人物の生い立ちだったり、全く意味の分からない…でも、すべてが明らかになった時に納得できるイベントだったりする。
少しづつ違う設定で繰り返し遊ぶことになるのだが、これも「そういう世界観」の一部だ。
プレイヤーと、もう一人の重要キャラクターだけが、並行宇宙を彷徨い、繰り返し同じ時間を過ごす能力を持っているのだ。
なんで? その能力は何のためにあるの?
それも謎の一つになっているが、ストーリー上やがて解き明かされる。
そして、多少ネタバレになってしまうが、「際限ない繰り返し」から脱出することが、ゲームクリアの条件となる。
それを達成するとエンディングが流れ、「1周目」のクリアとなる。
…のだけど、先に「完全クリア」と書いた通り、1周目クリアは完全クリアではない。
まだ謎がたくさん残っているし、ひとまずのエンディングではあるが、未解決問題も残っているのだ。
またネタバレで申し訳ないが、プレイヤーがゲームを始めるときに、性別を選ぶことができる。
ゲーム上性別はほとんど関係ないのだが、ストーリー上わずかな影響を与える。
このため、すべてを知ろうと思ったら、少なくとも2周クリアする必要がある。
(性別は三種類。男、女、汎、になっている。先にクリアした長男によれば、汎専用のイベントは用意されていないらしい)
そして、2周目をすべて終わらせる前のどこかで、1周目で未解決だった問題が解消される。
これが本当のエンディングだ。
すべての伏線が収束して綺麗に閉じる。
まぁ、ゲームとしての面白さを優先しているので、ストーリー的な荒さは多少あるのだけど。
実は、子供のプレイで先にこのエンディングは見てしまっているのだけど、自分で見るとまた違った印象になった。
それまでの過程…断片情報の積み重ねによる世界観の把握があるかどうかで、意味合いが変わってくるためだ。
なので、多少のネタバレがあってもこのゲームは楽しめる。
家族で遊ぶのにもお勧め。
さて、人狼面倒くさそう、という人向けに、このゲームが人狼と違って面倒くさくはないことを書いておこう。
相手が人間ではなく、コンピューターの動かすキャラなので自分のペースで進められる、というのは大きな要素の一つだ。
これは遊び始めてすぐの日記でも書いた。
でも、それ以上に、本物の…対人の人狼とは違う気楽さがある。
ゲーム進行に伴い、自分の能力があがり、使えるコマンドが増えるのだ。
対人の人狼では、「なんか嘘くさいな」と思っても確証を持つことが難しい。
でも、グノーシアでは自分の能力値次第では、相手が「嘘を言っている」と気づくことがある。
確実に嘘をついている、と分かるキャラがいて、そのキャラと妙に仲の良いキャラがいれば、それがグノーシアのグループではないか、と芋づる式に見つけ出すことができる。
(グノーシアが複数人いる場合、グノーシア同士は誰が仲間か知っている)
もっとも、だからこそ人間なのに嘘をつく「AC主義者」などの配役もあるのだけど、AC主義者はグノーシアが誰かは知らないため、微妙な反応の違いで見分けられたりするのが、また楽しい。
コマンドも、最初の内は「疑う」「かばう」「同調する」などの簡単なものしか使えないのだが、能力が上がるにつれて「反論する」「同意を求める」「哀しむ」「絶対に敵だ」…果ては「土下座する」なんてものもあり、適切に使うことで楽にゲームを進められるようになる。
1回のターンは5回の話し合いから成るのだが、ある程度能力があがってからの僕の1ターン目はこんな感じ。
自分はエンジニアでやった場合。
・名乗り出ろ 留守番
・名乗り出ろ ドクター
・役割を明かす エンジニア
・人間だと言え
・(流れに任せて静観)
「名乗り出ろ」は、役割を持つキャラに、役割を明かすように指示するコマンド。
自分の能力値が低いと誰も名乗り出ないこともある。
(役割は重要なものなので、明かすことでグノーシアに消される危険があるため)
留守番は互いに保証する必要があるため、必ず「人間」2名が名乗り出る。
ドクターとエンジニアは、偽物も名乗りを上げる。
そして「人間だと言え」は、全員に「自分は人間だ」と言わせるコマンド。
グノーシアは嘘をつくことになるので、嘘に敏感な…自分も含め、能力値が高いキャラなら嘘を見抜くことで、グノーシアに気づくことができる。
自分がエンジニアの場合、この後怪しい人間を調べていくことになる。
もっとも、自分がグノーシアを知ったとしても、それを他のキャラに信じてもらうのが大変なのだけど…
ちなみに、上の戦略を十分に能力値がないことにやると、目立ち過ぎてグノーシアに狙われたり、怪しまれてコールドスリープさせられたりする。
逆に言えば「育てたキャラなら無双できる」のであり、一人用のコンピューターゲームらしい気楽さが、そこにある。
人狼が面倒くさいと思うような人でも楽しめる人狼。よくできている。
ちなみに、登場人物は自分を含めて15人。
コンピューターが担当する14人は、細かな性格付けができていて、同じような状況でも人によって反応が違ったりする。
この性格を覚えるのも、ゲームを勝ち抜くためのコツで、対人戦だったらそんな簡単にはいかないだろう。
(人間には当然性格はあるが、「こういう性格だからこういう行動をとる」と言えるほど単純ではない。
グノーシアはゲームなので、各人の性格による行動の違いはあるが、各人が予想外の行動をとることは少ない)
2周もやると、それらの性格付けも含め、キャラクターが愛すべきものになっていく。
最初の内は怖かった謎のキャラも、様々なバックグラウンドがあるだけで、みんな優しいいいやつなんだ。
何度も遊べるゲームシステム、よくできた世界観、綺麗に収束するシナリオ、生き生きとしたキャラクターの性格付け…
非常によくできたゲームです。
改めて、お勧めです。
同じテーマの日記(最近の一覧)
関連ページ
別年同日の日記
申し訳ありませんが、現在意見投稿をできない状態にしています。 |
仕事で node.js を使用しているのだが、思わぬところで引っかかったので記しておこう。
…と、いきなり話を始めてもわからないので、前提知識から。
node.js は、サーバ側で Javascript を使用するための仕組みだ。
そして、Javascript は「Webブラウザで使う」ことを前提に設計された言語だ。
設計当時の OS は、Windows3.1 と MacOS 8 …
どちらも、今のマルチタスクとは違って、「アプリケーションの善意」でマルチタスクを実現していた。
(OS が割り込みで CPU 時間を管理するのが今の方式だが、Win3.1 や MacOS 8 は、アプリケーションが「短い時間で OS に処理を返す」ことを前提にしていた。
処理を返さないプログラムがいると、全体の動作が停止し、破綻した。)
いきなりすごい昔話が始まったが、その時代に設計された「アプリケーション内の組み込み言語」として、長時間の処理を「させるわけにはいかなかった」。
そこで、Javascript では「イベント駆動」という仕組みを使った。
画面がクリックされた、ボタンが押された、マウスカーソルが特定の領域に入った、など、あらかじめ設定した「イベント」が起きた場合に、指定したプログラムを呼び出してもらう。
イベント駆動でないなら、プログラムが動く条件を調べるところから自前で作らないといけない。
そして、条件を調べるためには、ユーザーがいつ押すかわからない「ボタン」が、押される瞬間を待ち続けないといけない。
…ここで「待ち続ける」というのがダメなのだ。
先に書いたように、短い時間で OS に処理を返さないと、全体が破綻するのだから。
そんなわけで Javascript はイベント駆動だし、なにかを「待つ」必要のある処理を「書けない」ように設計されている。
待つ必要がある場合には、その事象が起きた場合のイベントがあらかじめ用意されているから、そのイベントに対して処理を設定するのだ。
結果として、Javascript のプログラムはイベントを待って少しだけ処理する、というプログラムの断片だらけになる。
ブラウザ側なら、それでいいだろう。
ブラウザはユーザーインターフェイス(UI) の塊で、ユーザーが何かしたら、このプログラムを動かす、ということの連続でできている。
Javascript の組み込みイベントも、そうしたブラウザに合わせて設定されている。
この設定はブラウザメーカが勝手に作っているわけではなく、ECMA という団体で標準化されている。
だから、どこのブラウザでも同じ Javascript プログラムが動く、はずだ。
(実際には、すでにメンテナンスされていない IE とかは仕様が古すぎて最近のプログラムが動かなかったり、Chrome と FireFox で若干の仕様差があったりする。)
話しは戻って node.js なのだが、これはサーバ側で Javascript を動かす仕組みだ。
そして、ECMA の定めるイベントには、サーバで必要とするような事象が十分に考慮されていない。
先に書いたように、Javascript はイベント駆動だ。イベントなしにプログラムを書くことは、まぁできなくはないのだけど、やりづらい。
でも、サーバ向けのイベントは用意されていないし、そもそもサーバで作りたいプログラムは、ブラウザ側の UI と違って非常に多岐にわたる。
そこで、node.js では「ユーザが自由にイベントを拡張できるライブラリ」を作った。これは標準ライブラリとして、node.js を使える環境では必ず使える。
そして、多くの標準ライブラリで、このライブラリを使ったイベント処理を実現している。
なかなかうまい仕組みで、サーバ上でのプログラムを、違和感なくイベント処理で作ることができる。
さて、ここからが今日の本題。
Javascirpt に組み込みのイベントと、node.jsの標準ライブラリが提供するイベント。
同じ「イベント」の仕組みなので同じような動作をする、と思ってプログラムをしていたら、全くそうではなかったのだ。
気づかずに落とし穴にはまってしまった。
何か違う、と気づいてから情報を求めて探し回ったが、この違いに言及している日本語の記事には出会えなかった。
英語で探していてもほとんど情報がなく、「少し違うよ」程度に書かれている記事はあっても、具体的な情報がない。
最終的に、node.js のプログラムを読んで違いを理解した。
具体的には、次のようなプログラムが問題になったのだ。
const stream = fs.createReadStream("sample.text", {encoding:'utf8'})
const reader = readline.createInterface({input: stream})
const headline = await new Promise((resolve) => {
reader.on('line', (line) => {
reader.close()
return resolve(line.trim())
}).on('close', () => {
return resolve('')
})
})
これはプログラムの断片で、ライブラリとして標準提供されている fs と readline を必要とする。
短いのに Javascript らしい、非常にわかりにくいイベントプログラムになっているので説明しよう。
まず、stream を作っている。
これは、ファイルを読み込んで、読み込みに成功すると、「読み込んだ」というイベントを起こしてデータを渡してくれる。
ここで、特に指定が無ければファイルの頭から終わりまで、読み込みバッファ(指定が無い場合は 64Kbyte)毎に勝手にデータを読み続けてくれる、というのがミソなのだが、ここではあまり意識する必要はない。
この stream を入力として、reader を作っている。
stream で流れてくるデータを、行ごとに分解して、できた行ごとにイベントを起こしてデータを渡してくれる、一種のフィルタだ。
このプログラムでは、この reader に対して2つのイベント処理プログラムを設定している。
line は読み取った行を渡してもらうイベントで、行を受け取ると reader を終了し、行を結果として返す。
(resolve は結果を返す仕組みだが、詳細後述)
また、行が1つもないと…つまり、0バイトのファイルだと、line が来ないで close が来る。
この場合は「空文字列」を結果として返している。
つまりは、テキストファイルの最初の行を取り出すプログラムになっている。
さて、resolve という変わったやり方で結果を返しているのは、これらのプログラム全体が、
Promise という関数の中で呼び出されているためだ。
これもイベントを作り出す仕組みで、「何かを待つ」必要があるときに使う。
先に書いた通り Javascript では何かを待つことはできないのだが、Promise は最近作られた巧妙な仕組みだ。
Promise は、Promise オブジェクトと呼ばれるデータを返す。この時に待ち時間は発生しない。
そして、Promise オブジェクトは、渡された後でも値が変化する、という何とも奇妙なものだ。
最初は「未解決」という状態になっている。
その後、resolve を呼ばれると「解決済み」となり、そのとき resolve に渡された値を読み出すことができる。
await という制御命令は、Promise オブジェクトを引数とする。
そして、await があると、Javascript はその前後でプログラムを分割する。
(以降のプログラムを、勝手に別の関数にまとめると思って欲しい)
そして、await 命令でいったんプログラムの実行を終了してしまう。
await は「Promise が解決した」というイベントを待ち、イベントが発生すると以降のプログラムの処理を始める。
これにより、「待つ」という動作が、見事にイベント駆動に置き換えられ、短い時間で処理を終了する、という Javascript の理念を守ることができる。
ここで、もう一つの Javascript の特徴を説明しておこう。
このあとの話で必要になるからだ。
Javascript の大きな特徴は2つある。一つは、ここまでに書いた「イベント駆動」だ。
もう一つが「シングルスレッド」。
Windows などの OS は。複数のプログラムを同時に動かすことができる。
これを「マルチスレッド」と呼ぶ。
これに対して、Javascript はシングルスレッド。1つのプログラムしか動かせない、という意味だ。
Javascript はイベント駆動で、このイベントはブラウザの場合なら、ユーザーの操作などで引き起こされる。
マウスを動かした、ボタンをクリックした、などだ。
でも、「イベント」が起きても、すぐにイベントの処理プログラムが動くわけではない。
すでに動いているプログラムがあるなら、そのプログラムが最後まで実行終了するまで待たされる。
多数のイベントが有るときは、イベントは処理待ちの「キュー」に貯められる。
そして、動いているプログラムがないときに、順次処理されていく。
複数のプログラムを同時に動かす、というのは、コンピューター的には実は結構「無理している」処理で、無駄が多いのだ。
それに対して、1つのプログラムを動かすだけなら、その仕事に専念できるので効率よく動かすことができる。
シングルスレッドと、必要なときには仕事を「溜めて」おけるイベントキューの組み合わせで、効率よく仕事をこなせる。
これが Javascript の特徴で、node.js が高速だと言われる理由でもある。
さて、話を戻す。先程のプログラムでは、2種類のイベントが出てきた。
Promise によるイベント処理と、reader(readline) によるイベント処理だ。
このふたつが全然違うことで問題が起きる、というのが今日の話のテーマだ。
先程描いたように Promise はプログラムを小さな単位に自動的に区切り、1回のプログラム実行時間を短いものにする。
await new Promise の前と後ろでプログラムは区切られる。
後ろのプログラムは、resolve が実行された後で実行される。
ここで、resolve が「以降のプログラム」を動かすわけではない、ということにも言及しておこう。
resolve は、promise オブジェクトを「解決済み」に変更する役割しか持たない。
resolve はイベントをキューに積むだけで、「解決」したときの、await 以降のプログラムを動かすわけではない。
実際に await 以降のプログラムが動き始めるのは、また別のタイミングなのだ。
ところが、reader によるイベント処理はそうなっていない。
そのため、先に書いたプログラムは正しく動作しない。
具体的には、reader.close() に落とし穴がある。
これが resolve と同じように、close イベントを発生させるだけで実際の処理は後回し、であればよいのだが、実際には reader.close() 関数呼び出しの中で、reader.on('close', ~ に書かれているプログラムが呼び出されてしまうのだ。
その結果、イベント処理は「最小の処理時間」を実現するのではなく、実行時間を引き延ばすことになっている。
さらに、line イベントのプログラムの「途中で」close イベントのプログラムが始まってしまうことで、close イベントの resolve が先に動いてしまう。
Promise はそこで解決してしまうため、行のデータを渡す、という一番大切なことが実現されない。
なぜこんなことになるのか。
node.js の提供する「イベントを実現するライブラリ」は、実際にはイベント「風」のふるまいを行うだけで、Javascript のイベントとは全く別の動作をするためだ。
Javascript のイベントは、Promise の resolve のように、「イベントが起きた」という記録だけを行い、そのイベントに紐づいた処理は後で起動される。
しかし、node.js のイベントライブラリは、「イベントが起きた」ことを伝えると、そのことを伝える関数の中で、イベントに紐づいた処理を呼び出してしまう。
Javascript はシングルスレッド…プログラムの途中で別のプログラムが動き始めることは無い、と保証されている言語なのだが、この仕組みだと、その前提さえ崩れてしまう。
(イベントを起こすと、そのイベントによって割り込みが起こったような挙動になる)
node.js のイベント標準ライブラリの名前は EventEmitter 。
これ自体は非常に便利なものだし、批判したいわけではない。
言語の持つ仕組みではなく、その言語自身で書かれたライブラリとしてイベントを「疑似的に」実現しているので、挙動が違うのも仕方がないところ。
ただ、使う上でこの知識を持っていないと、思わぬところで謎の挙動に悩まされることになる。
最初に書いた通り、日本語でこのことを解説する記事を見かけなかったので、ここに記しておく次第。
(EventEmitter の使い方、というような記事は多数あるのだけど)
同じテーマの日記(最近の一覧)
別年同日の日記
申し訳ありませんが、現在意見投稿をできない状態にしています。 |