2018年10月20日

React/TypeScriptでリバーシゲームを作る (1) - ボードの描画と石の配置

実は2011年に「Androidでオセロゲームを作る」というのをやった事があります。5回のブログ記事に分けて少しずつ作り込んで行ったのですが、このシリーズが今でも結構アクセスされているみたいです。

これを読み返しながら、ふと「今だったら、ReactでWeb版を作っておけばネイティブアプリ化も結構簡単に出来るし色々と応用が効くのでは?」と思い付きました。

そこで、作り始めて見ました。

現時点で動作しているものはこちらで試せます。
https://reversi-d1kqojbar.now.sh/


タイトル画面


ゲーム画面

ソースコードはこちら

https://github.com/mikehibm/reversi-react/tree/blog-1


まだ画面をクリックして石を置くことが出来るようになっただけでゲームのルールが何も実装されていません。

しかし、リバーシゲーム作りは楽しいですね!


以下、ここまでに書いたコードの大まかな説明です。

コンポーネント階層


今のところ下のような階層になっています。

  • App (アプリケーションの大枠)
    • Menu (メニュー画面)
    • Game (ゲーム画面)
      • Board (ボードの背景や枠線などを描画するコンポーネント)
        • Cell (一つのセルを描画するコンポーネント)



状態管理


アプリケーションの状態は store.ts ファイルで一括管理しています。

ここでエクスポートしている Store クラスが EventEmitter を継承しており、状態に変更があったときにイベントを発生(emit)することでそれを各コンポーネントに通知して、各コンポーネントが画面を更新するという流れになっています。

例えば、メニュー画面でスタートボタンが押されたときには、setPage()メソッドが呼ばれて Store クラス内で保持している page 変数の値が変更されるので、その際に 'page_changed' というイベントを発生させています。

このイベントは App コンポーネントが監視していて、イベント発生時に最新の page の値を受け取って メニュー画面を表示するのかゲーム画面を表示するのかを切り替えています。


class App extends React.Component<Props, State> {
state = { page: 'menu' };
onChangeStore = () => {
const { page } = store.getState();
this.setState({ page });
};
componentDidMount() {
store.on('page_changed', this.onChangeStore);
}
componentWillUnmount() {
store.off('page_changed', this.onChangeStore);
}
public render() {
const { page } = this.state;
return (
<div className="App">
<header className="App-header">
<h1 className="App-title">Reversi</h1>
</header>
{page === 'menu' ? <Menu /> : <Game />}
</div>
);
}
}
view raw blog-1-App.tsx hosted with ❤ by GitHub



ボードとセルの描画


Boardコンポーネントでは、SVGを使ってボードの緑色の背景や縦と横の罫線などを描画しています。

また重要なのは、Cellコンポーネントの配列を作成してそれを子コンポーネントとして描画していることです。ボード上には 8 x 8 で64個のCellコンポーネントが配置されることになります。

Cellコンポーネントでは、propsとして渡された座標値にしたがってSVGのRectangleを描画します。このRectangleがマウスのクリックイベントを受け取る入り口になります。

また、propsのひとつとしてcolorが渡されますが、これはそのセルが空白なのか、黒の石が配置されているのか、または白の石が配置されているのか、を示します。

Cellコンポーネントではその値によって黒または白の円を描画する処理を行っています。

あるセル上でマウスがクリックされた時は、次の流れで最終的にセルに石が描画されるようになっています。


  1. handleClick()メソッドが呼ばれる。
  2. Storeクラスの setStone()メソッドが呼ばれる。
  3. クリックされた場所に石が無ければ、その場所の colorプロパティを現在の手番の色に変更する。
  4. Storeクラスが'board_changed'イベントを発生する。
  5. Boardコンポーネントがイベントを受け取り、ボード全体を再描画する。
  6. クリックされた場所に対応するCellコンポーネントが石を描画する。


こんな感じで、ひとまずボードの描画と石の配置までは出来るようになりました。

次回は、リバーシゲームとしてのルールを実装して実際に遊べるようになるまでを作ってみたいと思います!





React/TypeScriptでリバーシゲームを作る


(1) - ボードの描画と石の配置
https://blog.makotoishida.com/2018/10/reacttypescript.html

(2) - ゲームロジック
https://blog.makotoishida.com/2018/11/reacttypescript-2.html

(3) - 思考ルーチンその1
https://blog.makotoishida.com/2019/01/reacttypescript-3-1.html

(4) - 思考ルーチンその2
https://blog.makotoishida.com/2019/03/reacttypescript-3-2.html

(5) - アニメーション
https://blog.makotoishida.com/2019/03/reacttypescript-5.html









2018年8月10日

ReactでCSVファイルを読み込んでクライアント側で帳票を出力する

やりたかった事


あるWebサービスからCSV形式でデータをエクスポート出来るのですが、そのデータからちょっとした帳票を印刷したいという要望がありました。

そこでなるべく手っ取り早く、ブラウザだけで動くものが出来ないかと考えて作ってみたのが今回のプログラムです。

大まかな流れは、
  1. ローカルフォルダにあるCSVファイルを指定するとブラウザ上のJavaScriptでその内容を読み込む。
  2. 読み込んだ内容からHTMLで帳票を生成して表示する。
  3. ブラウザの印刷機能を使って手動で印刷またはPDFとして保存する。
という感じになります。


CSVファイルの例


User Client Project Description Start date Start time End date End time
Mike 株式会社◯◯◯ 売上管理システム開発 データ取込エラーの調査 2018-07-04 18:15:00 2018-07-04 18:45:00
Mike 株式会社◯◯◯ 売上管理システム開発 データ取込エラーの調査 2018-07-05 12:10:00 2018-07-05 13:31:00
Mike 株式会社◯◯◯ 売上管理システム開発 データ取込エラー対策の実装 2018-07-06 10:54:00 2018-07-06 11:05:00
Mike 株式会社◯◯◯ 売上管理システム開発 AWS・RDSの設定・動作確認 2018-07-13 14:01:57 2018-07-13 14:22:57
Mike 株式会社◯◯◯ 売上管理システム開発 EC2インスタンスの作成・動作確認 2018-07-18 08:10:14 2018-07-18 08:30:14
Mike △△株式会社 iOSアプリ開発 サーバーからの画像ダウンロード処理の変更 2018-07-20 11:55:05 2018-07-20 12:05:07
Mike △△株式会社 iOSアプリ開発 画像アップロード処理の実装 2018-07-20 13:00:14 2018-07-20 13:51:48
Mike 株式会社◯◯◯ 売上管理システム開発 月次レポート出力画面作成 2018-07-30 10:37:06 2018-07-30 10:59:56
Mike 株式会社◯◯◯ 売上管理システム開発 月次レポート出力画面の仕様変更・テスト 2018-07-30 11:27:59 2018-07-30 12:20:59
Mike 株式会社◯◯◯ 売上管理システム開発 次期バージョンの追加機能についてミーティング 2018-07-31 21:00:00 2018-07-31 21:41:00
view raw sample_data.csv hosted with ❤ by GitHub





作成したい帳票


実際に動いているもの

https://mikehibm.github.io/react-csv-example/




1. プロジェクトを作成する


プロジェクトは前回のエントリーと同様に Create React App + TypeScriptで作ることにします。
npm install -g create-react-app
create-react-app my-app --scripts-version=react-scripts-ts
cd my-app/
npm start

FileSelect.tsx と Report.tsx というファイルを新規作成して、App.tsx からこれらを呼び出して使うようにします。

最初は FileSelect コンポーネントを表示してCSVファイルの選択を行い、ボタンがクリックされたらファイルの内容を読み取って、Report コンポーネントに渡します。




Report コンポーネントが表示されている時は FileSelect コンポーネントは非表示になるようにします。




2. JavaScriptでCSVファイルの内容を読み込む


HTML5のFile APIを使います。FileReaderでの読み込みは非同期処理なので、処理をラップしてPromiseを返す関数を作りました。


function readFileAsText(file: Blob): Promise<string> {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onerror = () => reject(reader.error);
reader.onload = () => resolve((reader.result as string) || '');
reader.readAsText(file);
});
}





3. ファイルの内容をパースしてオブジェクトの配列に変換する


上の readFileAsText() からファイル全体の内容が文字列として返ってくるので、それをパースしてオブジェクトの配列に変換します。

具体的には、まず改行文字で区切って行単位の配列に分け、さらにその各行について「,」で区切って列単位の配列に分けます。つまり最終的には string[][] (文字列の配列の配列)になります。


function mapCSVToArray(csv: string): string[][] {
return csv.split('\n').map((row) => row.split(','));
}


ここでは簡単に改行文字(\n)と「,」で区切っているだけですが、実際には改行文字が違う、値に「,」が含まれている、などさまざまなケースがあり得るのでCSVのパース処理はちゃんとやろうとすると実は結構大変です。

なので必要であれば下の記事で紹介されている csv-parser などを使った方が良いかもしれません。

ブラウザ上でCSVファイルをパースする
https://qiita.com/ledsun/items/e38ee0dff8f26bf8d930




その後、文字列の配列(の配列)から、今度は帳票を生成する時に扱いやすいように、WorkItemというクラスのインスタンスの配列に変換します。

この時に、文字列型から日付型への変換や、CSVファイルに無い項目の値(duration)の計算なども同時に行っています。


import * as moment from 'moment';
export interface WorkItem {
user: string;
client: string;
project: string;
description: string;
startDate: Date;
endDate: Date;
duration: number;
}
export function mapArrayToWorkItem(data: string[][]): WorkItem[] {
return data
.map((row) => {
const startDate = moment(`${row[4]} ${row[5]}`, 'YYYY-MM-DD HH:mm:ss');
const endDate = moment(`${row[6]} ${row[7]}`, 'YYYY-MM-DD HH:mm:ss');
const duration = moment.duration(endDate.diff(startDate));
return {
user: row[0],
client: row[1],
project: row[2],
description: row[3],
startDate: startDate.toDate(),
endDate: endDate.toDate(),
duration: duration.asHours()
};
})
.filter((i) => i.client !== 'Client' && i.client); // 先頭と末尾の行を除外。
}
view raw WorkItem.js hosted with ❤ by GitHub



4. HTMLで帳票を生成する


WorkItemオブジェクトの配列を受け取って、tableタグで表形式のHTMLに変換します。

本当は帳票のタイトル、見出し、明細行などをそれぞれ別のコンポーネントに分けた方が良いのだと思いますが、今回はあまり時間がなかったので1コンポーネントで作ってしまいました。


import * as React from 'react';
import * as moment from 'moment';
import { WorkItem } from './WorkItem';
import './Report.css';
interface Props {
items: WorkItem[];
}
export const Report = (props: Props) => {
const totalHours = props.items.reduce<number>(
(prev: number, cur: WorkItem) => prev + cur.duration,
0
);
const startOfMonth = moment(props.items[0].startDate).startOf('month');
const endOfMonth = moment(props.items[0].startDate).endOf('month');
return (
<div className="Report">
<h4>
{startOfMonth.format('YYYY-MM-DD')} ~ {endOfMonth.format('YYYY-MM-DD')}
</h4>
<table>
<thead>
<tr>
<th>Description</th>
<th>Date</th>
<th>Time</th>
<th>Hours</th>
</tr>
</thead>
<tbody>
{props.items.map((i, index) => (
<tr key={index}>
<td className="description">
<span className="project">{i.project}</span>
<br />
{i.description}
</td>
<td>{moment(i.startDate).format('MM/DD')}</td>
<td className="start_time">
{moment(i.startDate).format('HH:mm')}~{moment(i.endDate).format('HH:mm')}
</td>
<td className="number">{i.duration.toFixed(2)}h</td>
</tr>
))}
<tr>
<td>&nbsp;</td>
<td>&nbsp;</td>
<th>Total</th>
<th className="number">{totalHours.toFixed(2)}h</th>
</tr>
</tbody>
</table>
</div>
);
};
view raw Report.tsx hosted with ❤ by GitHub



あとは画面に表示された帳票をブラウザの印刷機能で印刷するだけです。デフォルトではおそらく余計なヘッダーやフッター(ページタイトルやURL)も印刷されてしまいますが、印刷時の設定画面でこれらをオフにしておけばきれいに出力されるはずです。


今回作ったアプリの全ソースコードは下記にあります。

https://github.com/mikehibm/react-csv-example







2018年8月8日

Create React App + TypeScript で最初につまずいたこと

ちょっとしたサンプルプログラム的なものを Create React App + TypeScript で作ろうと思ったのですが、思わぬところでいきなり詰まってしまいました。


Create React App(以下CRA)で TypeScript と言えば、

https://github.com/wmonk/create-react-app-typescript

が定番だと思います。


さて、下の通りにプロジェクトを作成して実行しました。

npm install -g create-react-app
create-react-app my-app --scripts-version=react-scripts-ts
cd my-app/
npm start


もちろんここまでは全く問題なし。早速VS Codeでプロジェクトのフォルダを開いて、アプリケーションを作り始めました。



ところが、あるタイミングでふと

console.log("hogehoge");

を入れて実行してみようと思ったところ、いきなりコンパイルエラーになってしまいました。


「Calls to 'console.log' are not allowed.」というエラーですが、これにはちょっと驚かされました。console.logを入れただけでアプリケーションが動かなくなるなんて。。。


これですが、デフォルトで設定されているTSLintのルールがめちゃくちゃ厳格なものになっているのが原因なようです。


厳格すぎてこれでは使いづらいという声も多く、GitHubのIssuesでも議論になっているようですね。


オリジナルのCRA作者のDan Abramov氏からも、「この厳しいルールはCRAを作った時の思想(初心者でもすぐに動くものを作り始められる。本当にクリティカルなもの以外はワーニングのみでエラーにはしない。)と合わないから、ゆるくした方が良いんじゃない?」 という趣旨のIssueをわざわざ上げられているぐらいです。


create-react-app-typescript リポジトリ作者のWill Monk氏によると、「最初は自分が職場で使っているルールをコピーして使っていたのだけれど、後で一般的に推奨されているプリセット("tslint:recommended", "tslint-react"など)を使うように変更した」との事。「それでもまだルールが厳しすぎるなら、誰かがオリジナルのCRAと同等のゆるいルールの設定を作ってPRしてくれたら喜んでそれをマージするよ。」とも言っているので、彼自身は特にどうしても厳しいルールを他の人に強制する気は無いようです。


その後このIssue上で議論は進んでいるようですが、このブログ執筆時点の最新版ではまだルールの変更には至っていないようです。


さて、なにはともあれとりあえず console.log を使えるようにする方法ですが、上のIssueでも言及されているのですが、Microsoftによる下記のページにわかりやすくはっきりと記載されていました。

https://github.com/Microsoft/TypeScript-React-Starter


プロジェクトのルートフォルダにある tslint.json ファイルを下のように書き換えればOKです。(赤字を削除、青字を追加。)

{
-  "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
+  "extends": [],
+  "defaultSeverity": "warning",

"linterOptions": {
     "exclude": [
       "config/**/*.js",
       "node_modules/**/*.ts"
     ]
   }
 }

これで問題なく開発が続けられるようになりました!