オブジェクト指向と await の相性は良いのか?の落とし穴を考える

で、めでたく await を使ったモーダルダイアログができたわけですが、これが花札ゲームのロジックにあてはめられるかを試してみます。本来ならば「あてはめられるかどうか?」を UML なりを使ってシーケンス的にチェックする訳ですが、どうやら async/await の組み合わせ…というか、WinRT の UI の限界(あるいは、従来からある Windows フォーム系のイベント指向からのパラダイムシフト)の違いが顕著に表れる例で、どうも UML のシーケンス部分と(あるいは関数呼び出しとしてのシーケンス)との相性が悪いような気がしています。まぁ、IO 系を中心としたドライバーの非同期の呼び出し(call した後に return を非同期で受けるようなコード)が慣れている場合はいいのですが、もともと UML 自体に非同期のロジックが不足していた歴史的経緯を思い出し、Java を発祥とするオブジェクト指向記述型の UML であったことをあわせると、UI とロジックの分離のところで、UI とロジックが密接になってしまうというジレンマに陥るのでは?ってことです。

■発端は、UI なしで考えることから。

花札ゲームの場合、ゲームロジックを共通化することを優先したので、データ構造とロジックから作りはじめています。なので、「UI は飾りであって、取り換え可能である」という論理のもとに UI とロジックを分離させて、UI をチープなものからリッチなものへ変化させる、あるいはアニメーション部分は UI の担当なので後から変更できるようにする、というのが最初の思惑です。

Game クラスがゲームのロジック全般をつかさどるところ、Player クラスがユーザーのクラスですね。Player 自体はコンピュータの AI と切り替えを可能にするので(CPU 対戦というやつ)、ユーザーに問い合わせをする UI を使いません。インターフェースだけを使って、プレイヤーの思考プログラムを組むというやり方は、もとはシミュレーションの「エージェント指向」、近いところでは「アスペクト指向」って方法です。最近では、その手の「○○指向」的な話が沈静化しているので(フレームワークの話とか、DI の話とか)どういう動向で作っているのかが不明なところのなのですが、「温故知新」ってことで掘り出してみれば、20年前のオブジェクト指向ロジックですね。

さて、Pleyer がユーザーの場合には、ユーザーの選択を確かめるために「問い合わせ」を行います。これが、モーダルダイアログで、シーケンスの中に UI を挿入してユーザーの選択を待つ、という仕掛けです。このモーダルダイアログってのはウィンドウシステムの鬼っ子なところがあって、ユーザーから見れば「どこからでもアプリの制御ができる」というモードレスな画面に対して、どちらかを選択しないといけないモードがある画面が突如として出てくる、というシーケンスです。まあ、今となっては当たり前なのですが、コマンドラインで順番にコマンドを流す、という「対話形式」の入力方式から、Excel のようにどこからで操作ができるというウィンドウへのジャンプは結構大きかったのですよ。が、ジャンプが大きかったものの、YES/NO だけを選択させる画面が出るというのはシーケンスを阻害する意味もあり、しかしロジック的にはどちらかを選択しないと次のロジックに進めないというジレンマもあり、ってことで、モーダルダイアログ(問い合わせダイアログ)ってのは、コード的にも貴重な「鬼っ子」であるのです。

そんな訳で、場に花札を出したときに、マッチする札が2枚ある場合には、どちらを選択するかユーザーに問い合わせをするのです。ええ、ユーザーインターフェース的には、札を出すときにマッチする札へドロップさせる、という方法もあるわけで、実は「この時点でロジックが UI を制限している」ってのを意識しないといけないのですが…従来の流れもあってあまり意識せずにつくってしまいます…というのが今回の落とし穴ですね。

画面から選択させるのであれば、モーダルダイアログを出して「どちらを選択しますか?」で OK と考えるわけえで、シーケンス的には、Player から UI に SelCard 呼び出しをして、UI に問い合わせダイアログを開くのが簡単な方法です。
ユーザーが選択したら SelCard 関数の戻り値が返ってくるので、めでたく SelCard の戻り値で Card が選択されるので処理を続けられるというフローになります。ええ、わかりやすいですよね。

■イベントドリブンってことで、最初の一発は UI から始まる

この設計で OK ってことで、Game クラスと Player クラスを組むわけです。最初はチープな画面を作ってテストをしたり、ロジック部分を NUnit を使ってテストしたりして、そこそこ強固なロジックができあがります。

さて、先のシーケンス図では、Player への呼び出しはどこから始まるのか?を明確にしていませんが、実際のところ「場に札を捨てる」という動作は、ユーザーが UI で「手札をクリックする」という動作からはじまります。ここで注意したいのは UI を持っている場合は、ユーザーのアクションからロジックがスタートするってところです。なので、VBの時代には、Click イベントのがしがしとロジックを書いてスパゲッティになってしまったり、逆にロジックを無理矢理分離させるためじ Click イベントにロジックのワンメソッドが書いてあったりするわけですが、UI とロジックの分離は実は、もうちょっと複雑な関係なってきています。

最初の左矢印の putBa が UI から呼び出されるところ、その後に SelCard のところで再び UI に戻ってきて問い合わせダイアログ。札を選択したら山から1枚撮ってくるので、再び SelCard を呼び出して UI に表示して…ってな感じで、putBa の呼び出しから、内部で再帰的に UI が呼び出されます。実は、これは「状態遷移」の問題で、シーケンス的には関数呼び出しなのか、状態遷移なのか(メッセージングなのか)がわかりづらいのですが、これをクラス化して、シーケンスで動かそうとするといきおい、最初のメソッドが内部のメソッドを呼び出して、さらに内部のメソッドが内部の内部のメソッドを呼び出す…これを何回か繰り返した後に戻り値で戻ってくるという「構造化プログラミング」になっています。関数呼び出し≒スタック積み上げの方式は、これはこれでよいのですが、あまり入れ子になってしまうと昔はスタックの問題があったので、これを一度 main ルーチンに戻してから次のステップへ移動させる、っていう「状態遷移」の方法を取ります。この場合、状態を持たなければボタンクリックなどの「イベント駆動」のことになって、ある状態を持って開始から終了まで遷移をすれば、yacc とかなんらかのステートマネージャーを用意することになります。

■ブロッキングを使うか、状態遷移を使うかの選択

ロジックのシーケンスを作るときに UI に問い合わせが発生した場合には、ブロッキング(モーダルダイアログ)にするか、ノンブロッキング(状態遷移)にするかが悩ましいところです…といいますか、花札の2枚選択のようなユーザーに問い合わせをしてしまうパターンの場合には、「モーダルダイアログ」で OK ですよね?「はい」「いいえ」だけとか、場から札を選択させるだけなので、状態遷移なんて考えたこともありませんでした…ってのが罠なんですよ~。

先に書いた通り、UI のイベント駆動の場合には、シーケンスのスタートは UI になります。なので、ロジックの間に再び UI が挟まるという形になります。この部分、MessageBox.Show とか、普通に使っていますが、内部的に、Application.DoEvent を使うとか、別の WinProc を使っているとか、実は問い合わせを出しているダイアログでは、ちょっと複雑なことになっています。といいますか、DoEvent の場合はメッセージループを元に戻すとか、MessageBox の場合は別スレッド(別ウィンドウ)にするとか、それぞれ UI の実装の仕方が違います。

■WinRT はひとつの UI スレッドしかない

問い合わせダイアログを出すときは、従来の Windows フォームの場合には MessageBox を使って別のウィンドウに出ますが、Windows ストア アプリの場合には MessageDialog の ShowAsync メソッドを使って一見、Windows フォームように使えますが…実は大幅に実装が異なります。

private async void Button_Click_DeleteItem(object sender, RoutedEventArgs e)
{
    // 問い合わせを開く
    var dlg = new MessageDialog("アイテムを削除しますか?");
    dlg.Commands.Add(new UICommand("はい"));		
    dlg.Commands.Add(new UICommand("いいえ"));			
    dlg.DefaultCommandIndex = 1; // 「いいえ」をデフォルトボタンにする
    var cmd = await dlg.ShowAsync();				
    // キャンセルの場合
    if (cmd == dlgCommands[1] )		//(*7)
    {
        return;
    }
    // アイテムを削除
    var item = (SampleDataItem)this.flipView.SelectedItem;
    SampleDataSource.DeleteItem(item);
    // メインページに戻る
    this.Frame.GoBack();
}

一見、ShowAsync メソッドで「待ち」をしているように見えるのですが、await がついて Sync がついているのでこれは非同期メソッドです。なので、実はメソッドを呼び出したときにはこの Button_Click_DeleteItem メソッドは一度終了していて、ShowAsync メソッド内のタスクが終了した後に(戻り値が確定したときに)再び Button_Click_DeleteItem が呼び出されて、次の if (cmd == dlgCommands[1] ) の行から実行されるように「状態遷移」が隠されているのですね。

なるほど。WinRT の場合には、UI をブロックさせないようにするために、時間がかかかるロジックは非同期メソッドが用意されている、かつ、それのコードを軽減させるために async/await が用意されている…っていう流れで、これらを UI のイベントから呼び出す場合には、async をメソッドに付ければよいのでめでたしめでたし、ってな訳なのですが、いえいえ、話をシーケンスに戻すと、かなり複雑な現象に陥ります。

■async メソッドを呼び出すメソッドも async を付ける

2枚から選択するときに SelCard メソッド内で問い合わせダイアログを出します。ここでは、コードを簡単にするために MessageDialog.ShowAsync を使うのですが…このメソッドは非同期メソッドなので await を使うわけですね。で、いきおい、SelCard メソッドに async を付けないといけないわけです。

public async Card SelCard(Card c1, Card c2)
{

    // 問い合わせを開く
    var dlg = new MessageDialog("札を選択してください);
    dlg.Commands.Add(new UICommand(c1.ID));		
    dlg.Commands.Add(new UICommand(c2.ID));			
    dlg.DefaultCommandIndex = 1; 
    var cmd = await dlg.ShowAsync();				
    // キャンセルの場合
    if (cmd == dlgCommands[0] ) {
		return c1;
	} else {
		return c2;
	}
}

ここで、問題が発生します。SelCard はロジックなので、非同期を想定していないし、メソッド呼び出しとして同期的に戻り値が取りたい。また SelCard に async を付けると、実は状態遷移になっているので、先のシーケンス図とは動作が異なってしまう。という罠です。UI が非同期なので、ロジック部分も非同期にしないといけないのは当然なのですが、このゲームロジックを作る前提として、「UI の動作には関係なくロジックを流用する」というのがスタートだったので、後から UI の動作によってロジックの動きを変更する、っていうのは未来予測的に違反な訳です…っていうか、落とし穴ですね。フレームワークの調査不足。

なので、Windows フォームの場合には、DoEvent を使って回避することが可能なのですが、WinRT の場合は回避できません。いや、できるとしても、なんかかなり妙なコードを書かないとダメな気が。

また、この ShowAsync メソッドで await を使ってしまうと、内部ロジックにも await を使わないといけなくて、さらに呼び出し元の呼び出し元も await を付けるという await 変更地獄に陥ります。UI から Async を呼び出すときにはいいのですが、ロジック内では、await を使うとかなり妙なことになりそう。

■なので、素直に非同期を考慮したシーケンスを作る

なので、WinRT では UI 待ちは非同期に必ず直す、っていう方針に変えてしまって、await を使わないようにします。UI のところでは、コールバック関数を用意しておいて、結果が出たときのメソッドを別にしておきます。これはあらかじめデリゲートで用意してもよいし、ラムダ式で用意しても OK。
実は、見た目上は、中身が同期なのか非同期なのかはわかりません。というのも Player クラスを AI にした場合には非同期ではない(問い合わせダイアログが必要ない)ので、同期メソッド的に動かないとダメなのです。このあたり、悩ましいところですが、WinRT の async/await に関係なく、同期/非同期を同じようにコードで扱う場合には、この方法がよいかと。

void Player1_EventSelCard(Card c1, Card c2)
{
	// 場コントロールで選択する
	baControl1.SelCard(c1, c2, SelCardResult);
}
void Player2_EventSelCard(Card c1, Card c2)
{
	baControl1.SelCard(c1, c2, SelCardResult);
}
void SelCardResult(Card card)
{
	// 選択後の処理
	var lst = _board.Game.SelCard( card );
	_board.Ba.GetCard(lst);
	_curPlayer.GetCard(lst);
	// 画面の更新
	ScrUpdate();
}

それで、非同期型の UI は、状態遷移型にしておいて、ユーザーのボタン押下待ちになります。AI の場合には、結果のコールバック関数を直接呼ぶロジックに変えてもよいし、そもそも Player クラスの実装が別になるので、この UI 問い合わせの SelCard は呼び出されません。

public void SelCard(Card c1, Card c2, Action<Card> result)
{
	// 指定の2枚に枠を付ける
	var pic1 = _pics.Find(p => p.Source == CardUI.GetResName(c1.ID));
	var pic2 = _pics.Find(p => p.Source == CardUI.GetResName(c2.ID));
	this.rectangleShape1.Margin = pic1.Margin;
	this.rectangleShape2.Margin = pic2.Margin;
	this.rectangleShape1.Visibility = Windows.UI.Xaml.Visibility.Visible;
	this.rectangleShape2.Visibility = Windows.UI.Xaml.Visibility.Visible;

	pic1.Tapped += selPic_Click;
	pic2.Tapped += selPic_Click;

	_selPic1 = pic1;
	_selPic2 = pic2;
	_selCard1 = c1;
	_selCard2 = c2;

	// 選択待ち
	_selCard = null;
	_result = result;	// 結果用のコールバック
}
private void selPic_Click(object sender, TappedRoutedEventArgs e)
{
	_selPic1.Tapped -= selPic_Click;
	_selPic2.Tapped -= selPic_Click;
	this.rectangleShape1.Visibility = Windows.UI.Xaml.Visibility.Collapsed;
	this.rectangleShape2.Visibility = Windows.UI.Xaml.Visibility.Collapsed;

	Image pic = (Image)sender;
	if (pic == _selPic1)
	{
		_selCard = _selCard1;
	}
	else
	{
		_selCard = _selCard2;
	}
	_result(_selCard);
}

■あらためて非同期を考慮してシーケンスを書く

そんな訳で、あらためて非同期を考慮してシーケンスを書き直します…というか、元の画像と同じですね。

右の SelCard のところで「非同期」ってのが明記されているのがミソです。この result が、SelCard メソッドの戻り値ではなくて、Result というメッセージが Player に流れるという感じなんですよね…実はややこしい。

なので、WinRT の場合には UI を操作する部分は「必ず非同期」を考慮しないといけない、ってのがスムースに流れます(awit 無しのモーダルダイアログが出てくるまでは)。シーケンスとしては、簡単な問い合わせをするためだけに非同期シーケンスを考慮しないといけないのが、面倒なところなのですが、そのあたりは必ず状態遷移を使うってことで了解しておけばよいかと。シーケンス的に、こんなに複雑ではない場合(UI から問い合わせダイアログを出すだけの場合)には、await を使えば Ok だし、先の自作モーダルダイアログのようなものを使えば ok です。要は、呼び出しメソッドをさかのぼって async 付けまくりのメソッド直しだけは避けたいかという話です。

■そういえば、オブジェクト指向のシーケンスは元はメッセージングだった

で、最初のオブジェクト指向のシーケンスの話に戻るのですが、オブジェクト指向の発端は、クラス図とかではなくて、メッセージングだよって話があります。メッセージングってのは、データを関数で伝えるのではなくて、イベントキューとして伝える方式ですね。MFC でいえば、SendMessage(即時処理) と PostMessage(遅延処理) の違いです。

クラスメソッドの場合、引数を渡して、戻り値で処理の結果を得るという形で関数(メソッド)呼び出しをしますが、クラスとクラスの間のデータ受け渡しには、元のクラスからデータを渡して、先のクラスでデータを受け取る、という一方向のメッセージングにするパターンがあります。この場合、処理の結果は、先のクラスから元のクラスに再びメッセージが来る、という流れになるわけで、関数呼び出しとはかなり違います。戻り値がないので常に void method を呼び出している感じですね。

で、最初のシーケンス図は、メッセージのやり取りを規定するものであったので、メソッドの戻り値のところはなくて、これを Java で実装するときにクラスメソッドの戻り値が導入されて、戻り値で処理結果を得ることが主流になって、その後に非同期処理というのが明示的に導入された、という歴史的な経緯があるので、ええ、そうなると実は、シーケンス図は、非同期→同期→非同期 っていう歴史的な経緯がある訳です。シーケンス図が状態遷移図をワンセットになるので、シーケンスの矢印が戻り値を必要としない一方向だったってのはうなずける話で、そういう経緯を考えると、WinRT の非同期 UI フレームワークと、オブジェクト指向型のクラス構造との受け渡しは「メッセージング方式」にするのがベターかなと思い直しています。

ちなみに、メッセージング方式は、メッセージを送るときのコストが高くて、正確なコーディングは、キューに設定した後に受け側がメッセージを受け取るという部分を実装しないといけません。単なるクラス間のデータのやり取りにいちいちキューを使うのもコストがかかるので、通常は同期的な処理でよいのですが、状態遷移の場合にはキューを使うしかないんですかね…。事前の策としては、状態遷移を持っているステータスマネージャのクラスを使って、そのクラスから処理を振り分けるという WinProc 的なことをやることになるのですが、これはこれで悩ましいところ。

カテゴリー: C#, WinRT | 1件のコメント

[win8] Windows ストア アプリでモーダルダイアログを作る

[win8] Windows ストア アプリでモーダルダイアログは作れるのか? | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/4030

の続きで、カスタムのモーダルダイアログができたので、晒しておきます。難点としては、ちょっとコードが強引なのと、リサイズに対応していない(縦横にしたときとか)ので、後でライブラリ的に修正するのがベターかと。

■使い方

private async void ModalClick2(object sender, RoutedEventArgs e)
{
	ModalDialog dlg = new ModalDialog();
	int n = await dlg.ShowAsync();
	textResult.Text = n.ToString();
}

MessageDialog 風に await で待ちができます。これでシーケンスが簡単になるかなと。

モーダルダイアログ自体は、ユーザーコントロールで作っておいて、画面に表示します。ダイアログが表示している間は、他のボタンが押せないので、実質モーダルダイアログとして働きます。

■モーダルダイアログを作る

背景をかぶせる部分と、ダイアログとして表示する部分の2つの grid を作ります。

<UserControl
    x:Class="SampleModal.ModalControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:SampleModal"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="341"
    d:DesignWidth="610">
    <Grid x:Name="grid">
        <Rectangle x:Name="rect" Fill="black" Opacity=".2" />
        <Grid
          x:Name="msg"
            Background="Green"
          Width="380"
          Height="140"
          >
            <TextBlock 
            FontSize="32"
            HorizontalAlignment="Left" Margin="33,27,0,0" TextWrapping="Wrap" Text="こいこいしますか?" VerticalAlignment="Top"/>
            <Button
            Click="YesClick"
            Content="こいこい" HorizontalAlignment="Left" Margin="33,89,0,0" VerticalAlignment="Top"/>
            <Button 
            Click="NoClick"
            Content="勝負" HorizontalAlignment="Left" Margin="254,89,0,0" VerticalAlignment="Top" Width="87"/>

        </Grid>
    </Grid>
</UserControl>

背景のところは grid という名前を付けて、ダイアログ自身は msg という名前を付けておきます。grid のほうは、画面いっぱいに広げて他のボタンを押せなくするようにするために使います。

■モーダルダイアログのコード

モーダルダイアログ自身の ModalControl と、それを表示するための ModalDialog クラスを作っておきます。

public sealed partial class ModalControl : UserControl
{
	public ModalControl()
	{
		this.InitializeComponent();
	}

	public Action<int> OnButtonClick;
	public int Result;

	private void YesClick(object sender, RoutedEventArgs e)
	{
		if (OnButtonClick != null)
		{
			this.IsOpen = false;
			Result = 0;
			OnButtonClick(0);
		}
	}

	private void NoClick(object sender, RoutedEventArgs e)
	{
		if (OnButtonClick != null)
		{
			this.IsOpen = false;
			Result = 1;
			OnButtonClick(1);
		}
	}

	private bool _isOpen;
	public bool IsOpen
	{
		get { return _isOpen; }
		set
		{
			_isOpen = value;
			if (value)
			{
				this.Visibility = Windows.UI.Xaml.Visibility.Visible;
			}
			else
			{
				this.Visibility = Windows.UI.Xaml.Visibility.Collapsed;
			}
		}
	}
}

public class ModalDialog
{
	public ModalControl _dlg;

	public ModalDialog()
	{
		_dlg = new ModalControl();
		_dlg.OnButtonClick += OnButtonClick;

		var frame = Window.Current.Content as Frame;
		var page = frame.Content as Page;
		// ここでは Grid 固定
		// 親が Canvas の場合にも対応するべき
		var root = page.Content as Grid;
		root.Children.Add(_dlg);

		// 画面いっぱいに表示する
		double sw = Window.Current.Bounds.Width;
		double sh = Window.Current.Bounds.Height;
		_dlg.HorizontalAlignment = HorizontalAlignment.Left;
		_dlg.VerticalAlignment = VerticalAlignment.Top;
		_dlg.Margin = new Thickness(0, 0, -sw, -sh);
		_dlg.Width = sw;
		_dlg.Height = sh;
	}

	/// <summary>
	/// 通常の表示
	/// OnResult イベントをフックする
	/// </summary>
	public void Show()
	{
		_dlg.IsOpen = true;
	}

	/// <summary>
	/// 非同期に表示する
	/// </summary>
	/// <returns></returns>
	public async Task<int> ShowAsync()
	{
		_dlg.IsOpen = true;
		var n = await Task.Run( async () =>
		{
			while ( _dlg.IsOpen == true ) {
				// イベント待ち
				await Task.Delay(100);
			}
			return _dlg.Result;
		});
		return n;
	}

	public event Action<int> OnResult;
	private void OnButtonClick(int btn)
	{
		if (OnResult != null)
		{
			OnResult(btn);
		}
	}
}

ボタンのイベント待ちのところは、Awaitlbe パターン

非同期メソッドの内部実装 (C# によるプログラミング入門)
http://ufcpp.net/study/csharp/sp5_awaitable.html

を使えばいいのでしょうが、なんかうまく作れなかったので、await Task.Delay() でキーボード待ちをします。Applecation.DoEvents() と同じことができるので、まあ、これはこれでよいかと。100 msec で待てばさほど CPU に負担をかけずに済みます。ちなみに、ここを while ループだけにしてしまうと、CPU を 100% 喰ってしまいます。

■モーダルダイアログとシーケンス図の関係

で、モーダルダイアログの実装ができた訳ですが、果たして

無理矢理流れに乗せてみると意外とうまくいって無理矢理ではなかったというオチ | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/4022

のようにフォームアプリの Applcation.DoEvent の問題とか、ロジックとUIのシーケンスの問題(UMLで設計をするときの罠)が解決しているかどうかはさだかではないのですよ。ってなわけで、さらに寄り道をしてお次はこれを検証するということで。

カテゴリー: C# | [win8] Windows ストア アプリでモーダルダイアログを作る はコメントを受け付けていません

[win8] Windows ストア アプリでモーダルダイアログは作れるのか?

無理矢理流れに乗せてみると意外とうまくいって無理矢理ではなかったというオチ | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/4022/comment-page-1#comment-26496

なところで、花札の2枚選択を作ってみたのですが、さて、これが WinRT の場合にはどうなるのか?ってのが疑問でありました。ダイアログの出し方が、非同期になってしまっているので、Game -> Player -> UI の流れで Modal ダイアログが出せないとシーケンス上困るわけです…が、実際 WinRT に直してみると、これができないということがわかり、シーケンスを直す破目に。これって、普通の Windows フォームのロジックを、Windows ストア アプリに直そうとしたときに UML ベースでハマる罠で、ちょっと問題かなあと。

■ポップアップ画面を表示する

まずは、WinRT でポップアップ画面を出すにはどうするのか?ってのサンプル的に。メッセージダイアログを表示する場合には、MessageDialog というクラスがあって、

    var dlg = new MessageDialog("アイテムを削除しますか?");
    dlg.Commands.Add(new UICommand("はい"));
    dlg.Commands.Add(new UICommand("いいえ"));
    dlg.DefaultCommandIndex = 1; // 「いいえ」をデフォルトボタンにする
    var cmd = await dlg.ShowAsync();

な感じで書いておいて、フォームアプリと同じく MessageBox.Show のように使えます。

しかし、MessageDialog の場合には、画像を張り付けたりとかできないわけで「こいこいしますか?」みたいなゲーム画面の場合はどうするかというと、(たぶん)Popup コントロールを使います。が、この Popup コントロールの使い方がちょっと厄介で、Popup をデザイナ上に置いただけだと、左上に引っ付くんですよね。

これを適当な位置に移動させるには、Popup タグのところをつかんで移動しないとダメという罠があります。

<Popup x:Name="pop1" Margin="184,230,-184,258" Grid.Row="1">
    <Grid Background="red" Width="373" Height="161">
        <TextBlock
            FontSize="32"
            HorizontalAlignment="Left" Margin="23,24,0,0" TextWrapping="Wrap" Text="ここにメッセージを書きます" VerticalAlignment="Top"/>
        <Button
            Click="OkClick"
            Content="OK" HorizontalAlignment="Left" Margin="23,106,0,0" VerticalAlignment="Top" Width="131"/>
        <Button
            Click="CancelClick"
            Content="Cancel" HorizontalAlignment="Left" Margin="212,106,0,0" VerticalAlignment="Top" Width="131"/>
    </Grid>
</Popup>

まあ、Windows ストア アプリの流儀としては、別の画面に遷移させるとか、チャームの画面を利用するとかいう話もあるのですが、ポップアップだけを「簡単に出したい」というのが無理な構造になっているのは、かなり問題かなと思ってます。

/// <summary>
/// Popup を開く
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Open_Click(object sender, RoutedEventArgs e)
{
	// Popup を表示する
	pop1.IsOpen = true;
}

/// <summary>
/// OKボタンイベント
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OkClick(object sender, RoutedEventArgs e)
{
	pop1.IsOpen = false;
	textResult.Text = "OKをクリックした";
}

private void CancelClick(object sender, RoutedEventArgs e)
{
	pop1.IsOpen = false;
	textResult.Text = "Cancelをクリックした";
}

Popup の表示自体は、IsOpen プロパティを操作します。Ok ボタンとか Cancel ボタンのイベントは、画面のほうに返ってくるので、ここでボタンイベントの処理をします。非同期そのままの実装がでてくるので、モーダルダイアログっぽいものを出したい場合には、結構手間かなぁと。

で、この Popup の場合には、画面に表示させているときに他のボタンなどをガードしているわけではありません。いわゆるモードレスダイアログのような振る舞いになっているわけで、Popup を表示している間に、裏にあるボタンとかテキストボックスをクリックできます。確かに、フライアウト的な表示をする場合にはいいのですが、従来通り、「はい」「いいえ」をユーザーに選択させている間、他のボタンが押せない状態にしておく、モーダルダイアログの場合にはどうしたらいいのか?ってのが問題ですね。

■モーダルダイアログ風にPopupを使う

もうちょっと、検討しないとダメなんですが、ひとまずアイデアとして、こんな風にするのがコーディング上、手軽ではないかという例を示しておきます。

まずは、Popup コントロールを画面に張り付けて、画面いっぱいに grid を広げます。
広げた grid に半透明の rectange とダイアログを表示する grid を追加します。そう、画面いっぱいに rectangle を広げてしまって、後ろのクリックイベントなどをガードしてしまうという古い方法ですね。
でも、結構有効に働きます。

<Popup x:Name="pop2">
    <Grid Height="2000" Width="2000" >
        <Rectangle Fill="Black" Opacity=".2" />
        <Grid Background="Green" Margin="329,272,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" Height="224" Width="570">
            <TextBlock
                FontSize="32"
                HorizontalAlignment="Left" Margin="23,24,0,0" TextWrapping="Wrap" Text="こいこいしますか?" VerticalAlignment="Top"/>
                        <Button
                Click="YesClick"
                Content="こいこい" HorizontalAlignment="Left" Margin="26,155,0,0" VerticalAlignment="Top" Width="131"/>
                        <Button
                Click="NoClick"
                Content="勝負" HorizontalAlignment="Left" Margin="215,155,0,0" VerticalAlignment="Top" Width="131"/>
            <Image
                Source="/images/FC001.png"
                HorizontalAlignment="Left" Height="169" Margin="393,24,0,0" VerticalAlignment="Top" Width="111"/>
        </Grid>
    </Grid>
</Popup>

半透明のほうは Opacity の値で指定します。ここでは、0.2 にしていますが、0 にして完全に透明にしても後ろのボタンをクリックできません。なので事実上、モーダルダイアログにできます。
まぁ、AppMenu を張り付けるときは、たぶんスライドで出てしまうので完全に、という訳ではないでしょうが。

ボタンイベントは普通の Popup と同じです。

/// &lt;summary&gt;
/// モーダル風に開く
/// &lt;/summary&gt;
/// &lt;param name="sender"&gt;&lt;/param&gt;
/// &lt;param name="e"&gt;&lt;/param&gt;
private void Open2_Click(object sender, RoutedEventArgs e)
{
	pop2.IsOpen = true;
}
private void YesClick(object sender, RoutedEventArgs e)
{
	pop2.IsOpen = false;
	textResult.Text = "こいこいします";
}
private void NoClick(object sender, RoutedEventArgs e)
{
	pop2.IsOpen = false;
	textResult.Text = "勝負します";
}

■シーケンス上、なにが問題になるのか?

さて、これをシーケンス図にして考えてみると、通常のモーダルダイアログの場合には logic -> UI -> logic の場合で、UI の中で待ってくれるのだが、Popup コントロールの場合は、logic -> UI … UI -> loigc な漢字で、非同期に UI から結果が飛んでくる。最初の logic -> UI のところで UI の結果が取れる手続き風に書きたいところなのだが、非同期なので一度関数が戻ってきた後に、戻り値が非同期で呼び出されるということになりちょっと複雑。

なので、この Popup の非同期な部分を、同期っぽくみせるために、

    var cmd = await pop1.ShowAsync();

な方法で組みたいわけなのだが、これがなかなかうまくいかない。ってなわけで、ひとまず Popup はモーダル風にする、非同期に呼び出されるので花札のシーケンスを書き換える、ってな方向で考えている途中。MessageDialog ができているので、できないことはないと思うんだが…ひとまず。

カテゴリー: C# | [win8] Windows ストア アプリでモーダルダイアログは作れるのか? はコメントを受け付けていません

無理矢理流れに乗せてみると意外とうまくいって無理矢理ではなかったというオチ

あの日はこう考えていたけど、今はこう考えていて、それを実装しようとするときに、以前の他人の自分を「他人」として捉えるのも良し。いやいや、同一人物なんだから同じ考えでオーケーでは?と再び考えてみて、それなりにリスペクトしたコードを書いてみれば、なるほど、そう考えていたのか、と自分に納得する、という現象なのか。

■2枚を選択させるUIを場コントロールに追加する

2つの選択肢をしめすために、RectangleShape を使って枠線を描画する、ってのはいいとして、もともとのメイン画面からの呼び出しが、こんな風にコールバックになっているわけです。

/// <summary>
/// 2枚から選択する
/// </summary>
/// <param name="c1"></param>
/// <param name="c2"></param>
/// <returns></returns>
Card Player1_EventSelCard(Card c1, Card c2)
{
	string msg = string.Format(
		"{0} と {1} があります。{2} を選択しますか?",
		c1.ID, c2.ID, c1.ID);
	var btn = MessageBox.Show(msg, "", MessageBoxButtons.YesNo);
	if (btn == System.Windows.Forms.DialogResult.Yes)
	{
		return c1;
	}
	else
	{
		return c2;
	}
}

これを場コントロールで選択できる、という風に仮定すると、こんな感じなるのが理想的…つーか、最初はコールバック関数を作るとか、モロモロ変な感じなコードになっていたのですが、Player1_EventSelCard メソッド自体が、コールバック関数なので、この中で処理をしないといけないんですよね。非同期の async/await が使えればそれはそれでいいのだけど、今回の場合は Windows フォームなので、それを使うのも変な話だし。

/// <summary>
/// 2枚から選択する
/// </summary>
/// <param name="c1"></param>
/// <param name="c2"></param>
/// <returns></returns>
Card Player1_EventSelCard(Card c1, Card c2)
{
	return baControl1.SelCard(c1, c2);
}

という訳で、場コントロールには、次のロジックを追加。

PictureBox _selPic1;
PictureBox _selPic2;
Card _selCard1;
Card _selCard2;
Card _selCard;

/// <summary>
/// 2枚から選択する
/// </summary>
/// <param name="c1"></param>
/// <param name="c2"></param>
/// <returns></returns>
public Card SelCard(Card c1, Card c2)
{
	// 指定の2枚に枠を付ける
	var pic1 = _pics.Find( p => p.Image == CardUI.GetResName(c1.ID));
	var pic2 = _pics.Find( p => p.Image == CardUI.GetResName(c2.ID));
	this.rectangleShape1.Location = new Point(pic1.Location.X - 3, pic1.Location.Y - 3);
	this.rectangleShape2.Location = new Point(pic2.Location.X - 3, pic2.Location.Y - 3);
	this.rectangleShape1.Visible = true;
	this.rectangleShape2.Visible = true;

	pic1.Click += selPic_Click;
	pic2.Click += selPic_Click;

	_selPic1 = pic1;
	_selPic2 = pic2;
	_selCard1 = c1;
	_selCard2 = c2;

	// 選択待ち
	_selCard = null;
	while (_selCard == null)
	{
		Application.DoEvents();
	}
	return _selCard;
}

private void selPic_Click(object sender, EventArgs e)
{
	_selPic1.Click -= selPic_Click;
	_selPic2.Click -= selPic_Click;
	this.rectangleShape1.Visible = false;
	this.rectangleShape2.Visible = false;

	PictureBox pic = (PictureBox)sender;
	if (pic == _selPic1)
	{
		_selCard = _selCard1;
	} else {
		_selCard = _selCard2;
	}
}

メソッド内でユーザーの選択待ちの動作が入ってしまうので、Application.DoEvents() でクリック待ちをするという荒業です。これを使わない場合には、メイン画面のほうにコールバック関数を作って、という具合になるわけですが、そうなるとロジックと UI の部分の分離がうまくできない。ここは鬼っ子なのか?というのは後で検証するとして、まあ、これで2枚の花札を選択するという動作ができます。

■お次は「動的なデータ表現」

大体、静的なデータ表現ができたので、今度は動的なデータ表現を作ります。「動的な」ってのは、アニメーションとか、一連の動作のところですね。手札がから1枚場に捨てる場合、一連の動作としては、

  1. 手札から場に1枚出す。
  2. 場にマッチさせる。
  3. 山から1枚引いて、場に出す。
  4. 場にマッチさせる。
  5. マッチした札を取り札にする。

というところまで1連の動作なわけで、これを「静的なデータ」だけで表現すると、1の手順を行ったときに、一瞬で5になってしまって「何が起こったのかさっぱりわからない」状況になります。なので、このあたりを「時間」を差し込んで、アニメーションで表す、ってところですね。この時間を差し込むところは、業務アプリではほとんど使わないかと。強いて言えば、ドラッグ&ドロップのところぐらいでしょうか。リストから選んだり、テキストを入力してボタンを押した直後に、何か結果が出てくるのが業務アプリの特徴。ゲームの場合は、この間にアクションを差し込んで「わかりやすく」≒「エンターテインメント性を持たせる」ってことになります。

Windows メトロアプリの場合、XAML を使って storyboard を使うと、このゲーム性の部分が比較的簡単にできるのでは?と思っています。このあたりの実験も兼ねて。そう、まずは、Windows フォーム版を、XAML 版に移植するところから始めます。WPF を作ってから Windows メトロのほうがいいのかな?ちょっと思案中。

カテゴリー: C#, 花札ゲーム | 1件のコメント

IT技術を周回遅れで追ってみれば、枯れた温故知新が見つかるのか、単なる当時の幻想が見つかるのか

花札ゲームの対象は、最終的に Windows 8 のメトロアプリ(XAMLアプリ)と iPad のアプリな訳で、データ構造やゲームの内部構造を共通化すると同時に、表面的には、XAML の部分と、xcode の storyboard のところ(たぶん、ユーザーコントロールを自作)を作ることになるハズ。その前提として、データ指向的なデータバインドの方法と、少し古めの MVVM の方法(observerパターンを使うという点で)を組み入れる予定です。なので、View からデータへの連結はできるだけ簡単な機構を使いたい、ってのがある。
ゲームロジックと画面への描画をべたべたに書くほうが楽だとは思うのだけど、腐心しているのは「後から修正可能にする」ということと、同時に「後から修正を短時間で行えること」を目標とするわけで。このとき「後から」ってのは、今から未来を見通すことになるわけで、その昔…というかオブジェクト指向的にいうと拡張可能にしたり、あらかじえインターフェースを用意したりしようとする方法に陥るのだが、いやいや、そのような「未来予想のコスト」は、変更時のコストがかなり安くなてしまったので(というか、作り直すというコストがかなり安くなってきている)、枠というか「考え方」だけ残してざっくりと作り直したほうが、バグ修正も含めて早かったりする。
となると、今度は「作り直すコスト」を下げるために、今のコードを作る、という不思議なコーディングスタイルがあっても良いかなと思ったり。

■山のコントロールを作る

山のコントロールは作る必要もないのだけど、山から花札を取るという動作をチープな画面では「get」ボタンに割り当てていたので、同じように山のイメージだけ作っておきます。
他のコントロールと同じように、DataSource プロパティがあるわけですが、中身が空っぽです。花札の画像をクリックしたときに、クリックイベントを画面のほうに投げるだけです。正確に作るならば、DataSource に山用の List<Card> を渡してやって、クリック時に1枚メイン画面に渡す、ってコードになるわけですが、1枚山のリストから引っ張ってくるというコードはメイン画面に書いてあるし、ってことでそのまま。

public partial class YamaControl : UserControl
{
	public YamaControl()
	{
		InitializeComponent();
	}
	protected List<Card> _data;
	public List<Card> DataSource
	{
		get { return _data; }
		set
		{
			_data = value;
			UIUpdate();
		}
	}
	protected void UIUpdate()
	{
	}

	public event Action<Card> ClickCard;
	/// <summary>
	/// 山をクリック
	/// </summary>
	/// <param name="sender"></param>
	/// <param name="e"></param>
	private void pict_Click(object sender, EventArgs e)
	{
		if (ClickCard != null)
		{
			ClickCard(null);
		}
	}
}

さらに理屈を加えると、大抵の花札のゲームでは山から1枚取るのは自動なんですよね。ゲームの動きとしては、

  1. 手札から1枚場に出す。
  2. 場とマッチングさせる。
  3. 山から1枚引く。
  4. 場とマッチングさせる。
  5. 役があれば紹介アニメを表示する。
  6. マッチした札を取り札に移動する。

まで一連の流れになるわけで(実際には、2枚マッチがあった場合にはユーザーに選択させるわけですが)、ここで使っているテスト用の「チープな画面」では、このあたりの操作はボタンを押して遷移させています。
まあ、そうしないと一瞬で画面から札を取ってしまって途中経過がわからないと、この「途中経過」の部分をどうやって MVVM なのか、データバインドなのか、データ指向なのか、に仕立てるということで、外してあります….というのは半分嘘で、アニメーションのほうを先に考えたら失敗したので、「静的データの表現」から「動的データの表現」へと開発の向きを変えてみたわけです。

■場に札を出したときの処理を加える

場に一枚、札を出したときのコントロールを別に作ろうかと思ってもみたのですが、「場に札を出す」という動作から考えて、場コントロールを拡張させることにします。

ここでちょっと悩んだのが、データバインドの DataSource プロパティをどう拡張しようか?ってことなのですが、いやいや、そんなデータ構造を複雑にせずに、バインドするプロパティを DataSource と PutCard という2つにすれば良いのだ、ということに気づきました。そうそう、データバインドって、なんとなくひとつだけというイメージがあったのですが、別に複数あっても構いません。ただし、あまり多くなってしまうと、どれをどのようにバインドしたらよいのかと、コントロールを使う側が迷ってしまうので、少な目にしておきます。ってのがポイントですね。

このあたり、16駒パズルを作ったときに失敗したのですが、枠線やら画像やら位置やらのひとつひとつにバインドをさせるのは、面倒…と言いますか、バインドの指向的に間違っています。「設定する値」というデータの意味的な部分と、「視覚的に表現する形」という UI 的な部分とは、別々にするとコードが簡単になるし、簡単になりますね。そのあたりの反省もあって、今回は「ユーザーコントロール」を多用しています。下手に、標準的なコントロール(この場合は PictureBox とか)を画面から直接扱うようにするとデータとの連携が複雑になってしまいますが、DataSource プロパティというデータバインドという「インターフェース」に絞ってしまうことで、コードの修正がかなり楽になっています。このあたりは別途機会を作ってまとめるということで。

public partial class BaControl : UserControl
{
	public BaControl()
	{
		InitializeComponent();

		_pics = new List<PictureBox>();
		_pics.Add(this.pictureBox1);
		_pics.Add(this.pictureBox2);
		_pics.Add(this.pictureBox3);
		_pics.Add(this.pictureBox4);
		_pics.Add(this.pictureBox5);
		_pics.Add(this.pictureBox6);
		_pics.Add(this.pictureBox7);
		_pics.Add(this.pictureBox8);
		_pics.Add(this.pictureBox9);
		_pics.Add(this.pictureBox10);
		_pics.Add(this.pictureBox11);
		_pics.Add(this.pictureBox12);
	}
	protected List<PictureBox> _pics;

	// 場のカード
	protected List<Card> _data;
	public List<Card> DataSource
	{
		get { return _data; }
		set
		{
			_data = value;
			UIUpdate();
		}
	}
	// 山や手札から出したカード
	protected Card _putCard;
	public Card PutCard
	{
		get { return _putCard; }
		set
		{
			_putCard = value;
			UIUpdate();
		}
	}

	protected void UIUpdate()
	{
		int i = 0;
		if (this.DataSource != null)
		{
			foreach (var c in this.DataSource)
			{
				_pics[i++].Image = CardUI.GetResName(c.ID);
			}
		}
		for (; i < _pics.Count; ++i)
		{
			_pics[i].Image = CardUI.GetUra();
		}
		if (_putCard != null)
		{
			this.pictureBoxPut.Image = CardUI.GetResName(_putCard.ID);
		}
		else
		{
			this.pictureBoxPut.Image = CardUI.GetUra();
		}
	}
	public event Action<Card> ClickPutCard;
	/// <summary>
	/// 場をクリック
	/// </summary>
	/// <param name="sender"></param>
	/// <param name="e"></param>
	private void pict_Click(object sender, EventArgs e)
	{
		if (ClickPutCard != null)
		{
			ClickPutCard(null);
		}
	}
}

■表示位置を調節してゲームらしく

フォームの位置を変えて、チープ画面用の ListBox などを隠すと、ほら花札ゲームのようになります。
リッチ UI 絡みのほうはだいたいできたのですが、もうひとつ問題があって、場に2枚マッチしたときに、どちらかを選ばさせないといけません。いままのままだと無骨なダイアログがでるので、これを場コントロールを使ってなんとか表現させたいのです。

方法はいくつかって、他の花札ゲームのように場の札を枠線であらわして選択させるとか、札を点滅させるとか、別の画像つきのダイアログを出して選択するとか、まあ、いろいろ。ただ、win タブレットとか iphone を考えると、ダイアログが出るよりは、枠を光らせて選択させる、ってのが「自然」ではありますね。

という訳で、お次は、2枚選択の部分を「場コントロール」にフィードバックして表現させる、ってところを実装しています。

カテゴリー: C#, 花札ゲーム | IT技術を周回遅れで追ってみれば、枯れた温故知新が見つかるのか、単なる当時の幻想が見つかるのか はコメントを受け付けていません

優先順序が決まったら一手動いて再び優先順位を見直す、場合はコストを考える

そういえば、PPCM の法則に優先順位を決めたら優先順位が変わらない間はそのままの実行順序で行く。その間に優先順位が変わるような状況があったら、再び優先順位を見直す、てのがある。PDCA の基本でもあるし、アジャイルのアジャイルたるところでもあるし、手順を作って突っ走るほうのが一番早いウォーターフォール開発ってのもその意味では正しい。が、これらの「優先順位づけ」が「コストが掛かる」≒「時間がかかる」てのが前提条件になっていて、実際いろいろな状況を鑑みて計画を練り直す(Excel シートに書き付けた計画シートを書き直す、という作業的なものも含めて)のはコストがかかるわけで、ある程度の期間は計画はそのままというのがベター。となると、逆に計画変更のコストが安くなれば?ってのが plan-language の発端だったなぁ、と思い返してみたり。いやいや、ひとりでやっているときは、タイムボックス的なところの計画と、詳細のタスク分割とは切り離して考えるのがベターかなと。

■取り札のコントロールを作る(その1)

昨日の続きで、取った札をユーザーコントロールに

最初は、動的に PictureBox を増減させていたのですが、簡単のため≒バグを少なくするために、あらかじめ花札の枚数分だけ用意しておきます。デザイナでペタペタと張り付けて…というのは大変なので、コントロールのコンストラクタで作ります。このあたり、最終的には XAML にするパターンになるのでかなり違ってしまうのが難点ですが、思想は同じようにできる感じで。
なぜか Controls に逆順で挿入しないとダメなのと(デザイナ的には、順方向でいいはずなんですが)、一度 PictureBox を表示しておかないと、上下がおかしくなるというバグ的な動きが。まあ、これはプロトタイプなので、そのままにしておきます。

public partial class GainControl : UserControl
{
	public GainControl()
	{
		InitializeComponent();

		// あらかじめ 48 枚分用意しておく
		this.pictureBox1.Visible = false;
		_pics = new List<PictureBox>();
		int x = this.pictureBox1.Location.X;
		int y = this.pictureBox1.Location.Y;
		for (int i = 0; i < 48; i++)
		{
			var pic = new PictureBox();
			pic.Size = this.pictureBox1.Size;
			pic.Location = new Point(x, y);
			pic.SizeMode = this.pictureBox1.SizeMode;
			pic.Visible = true;
			pic.Image = CardUI.GetUra();
			x += 20;
			_pics.Add(pic);
		}
		// 逆順で Controls に追加
		_pics.Reverse();
		foreach (var p in _pics)
		{
			this.Controls.Add(p);
		}
		// 元に戻す
		_pics.Reverse();

	}
	protected List<PictureBox> _pics;
	protected List<Card> _data;
	public List<Card> DataSource
	{
		get { return _data; }
		set
		{
			_data = value;
			UIUpdate();
		}
	}

	protected void UIUpdate()
	{
		if (_data == null)
			return;

		foreach (var p in _pics)
		{
			p.Visible = false;
		}
		int i = 0;
		foreach (var c in _data)
		{
			_pics[i].Image = CardUI.GetResName(c.ID);
			_pics[i].Visible = true;
			i++;
		}
	}
}

画面のほうからは DataSource プロパティでバインド。

	gainControl2.DataSource = _board.Player2.TakenCards;

まあ、これで動くといえば動くのですが、花札の場合、タネとか短冊とかの枚数がわからないと次の手を打ちにくいので、これを「普通の花札ゲーム」のように4つに分けます。

■取り札のコントロールを作る(その2)

まずは、上記のように4つの置き場所を作ります。これにデータバンドをしたいわけですが、さて、それぞれのリストに対して DataSource プロパティを作るべきか?と悩んだのですが…やめました。ユーザーコントロールから設定する場合は、DataSource プロパティひとつに設定しておいて、内部的にリストに設定すれば OK なのですね。なんか、ListBox などのバインドイメージがあったので、List のコレクションをそのままバインドしないといけない気になっていた訳です。

public partial class TakenControl : UserControl
{
	public TakenControl()
	{
		InitializeComponent();

		// あらかじめ20,10,5,1札の場所を用意しておく
		this.pictureBox1.Visible = false;
		this.pictureBox2.Visible = false;
		this.pictureBox3.Visible = false;
		this.pictureBox4.Visible = false;

		_pics20 = InitPics(this.pictureBox1, 5);
		_pics10 = InitPics(this.pictureBox2, 10);
		_pics5 = InitPics(this.pictureBox3, 10);
		_pics1 = InitPics(this.pictureBox4, 20);
	}

	private List<PictureBox> InitPics(PictureBox ptemp, int num)
	{
		var pics = new List<PictureBox>();
		ptemp.Visible = false;
		int x = ptemp.Location.X;
		int y = ptemp.Location.Y;
		for (int i = 0; i < num; i++)
		{
			var pic = new PictureBox();
			pic.Size = ptemp.Size;
			pic.Location = new Point(x, y);
			pic.SizeMode = ptemp.SizeMode;
			pic.Visible = true;
			pic.Image = CardUI.GetUra();
			x += 20;
			pics.Add(pic);
			// this.Controls.Add(pic);
		}
		// 逆順で Controls に追加
		pics.Reverse();
		foreach (var p in pics)
		{
			this.Controls.Add(p);
		}
		// 元に戻す
		pics.Reverse();
		return pics;
	}

	protected List<PictureBox> _pics20, _pics10, _pics5, _pics1;

	protected List<Card> _data;
	public List<Card> DataSource
	{
		get { return _data; }
		set
		{
			_data = value;
			UIUpdate();
		}
	}

	protected void UIUpdate()
	{
		if (_data == null)
			return;
		foreach (var p in _pics20) { p.Visible = false; }
		foreach (var p in _pics10) { p.Visible = false; }
		foreach (var p in _pics5) { p.Visible = false; }
		foreach (var p in _pics1) { p.Visible = false; }

		// 点数ごとに分けて表示する
		int i20 = 0;
		int i10 = 0;
		int i5 = 0;
		int i1 = 0;
		foreach (var c in _data)
		{
			switch (c.Ten)
			{
				case 20:
					_pics20[i20].Image = CardUI.GetResName(c.ID);
					_pics20[i20].Visible = true;
					i20++;
					break;
				case 10:
					_pics10[i10].Image = CardUI.GetResName(c.ID);
					_pics10[i10].Visible = true;
					i10++;
					break;
				case 5:
					_pics5 [i5 ].Image = CardUI.GetResName(c.ID);
					_pics5 [i5 ].Visible = true;
					i5++;
					break;
				case 1:
					_pics1 [i1 ].Image = CardUI.GetResName(c.ID);
					_pics1 [i1 ].Visible = true;
					i1++;
					break;
			}
		}
	}
}

置き場所にあらかじめ PictureBox を作っておくのは同じで、画面を更新するための UIUpdate メソッドで DataSource プロパティの値(内部的には _data)のカードを振り分けます。こうすると、インターフェース的には、DataSource プロパティひとつでいける訳で、コードが簡単になる、ってことで。

これでずいぶん花札ゲームらしくなったということで。

お次は、「山のコントロール」と「手札や山から場に1枚だしたときのコントロール」。場に出したときのコントロールは、別のコントロールにしようと思ったのですが、場コントロールに含めてみた、という話を。

カテゴリー: C#, 花札ゲーム | 優先順序が決まったら一手動いて再び優先順位を見直す、場合はコストを考える はコメントを受け付けていません

脳内補完を現実にすることで物理的制約により準優先が決まる

想像とか机上でなんとなくできているような感じがして、それで満足したり試行錯誤したりするわけですが、現実に当てはめれてみればなんということはない「物理的な制約」が先にあって、実現不可能な選択肢が削られて話が簡単になること、というのはプログラムの話。いや、煮詰めすぎると無理が来るってことか、あるいは時間的な制約ということか。

チープな画面を

少しリッチな画面に

変えていきます。

花札の画像は、Windows ストア アプリのサンプル画像からピックアップ。

■UIのプロジェクトを分ける

チープな画面プロジェクト(Hanahuda)とは別にリッチなUI用の HanahudaUI プロジェクトを作ります。

.NET のライブラリ参照が便利なのは、元の Hanahuda アセンブリのクラスを UI プロジェクトでも簡単に参照できるところですね。簡単すぎて、先行き複雑になったとき(特に複数人数で開発になったとき)には困ることが多いのですが、ロジックと UI を強引に分離させたいときは、こういう風にプロジェクトを分けてしまいます。と云いますか、元のチープな画面はロジックテスト用に残しておいて、リッチな UI チェック用は別に作る、って方法ですね。

■花札のリソースを追加

となると、花札の画像はどちらに置くかというと、UI プロジェクトに置くわけで、このリソースの名前と花札の ID とをどうやって結び付けるのかが問題になるのです。同じプロジェクトに置けば、リソースを参照するのも楽なのですが、ここは UI を別にしておきたいし。そうなると、UI の方に花札のリソースが含まれるのは当然なわけで。そうなると、プロジェクトは HanahuhaUI から Hanhuda を参照する一方向しかできないわけで、さて、ID とリソースの関係はどうするのか?ってのが問題で悩みます。

が、現実的なところ

  • ひとつにはまとめたくない(まとめない)
  • 循環参照はできない。

という制約のなかで、以下のような対比表が書かれることになるわけです。
“A1” というのは花札の ID なので、Hanahuda プロジェクトの ID クラスとダブって書かれることになります。C 言語の場合は、インクルードファイルってことになるのですが、果たして .NET の場合はどうするべきなのか?ってところですね。業務的には、別の ID 定義用のプロジェクトをひとつ作って、enum か static を使って Card.A1 とか、ちまちまと定義することになるのですが…まあ、無駄なプロジェクトを増やすよりは、ダブって書くということで、今回はあきらめ。

class CardUI
{
	protected static Dictionary<string, Bitmap> _resNames;
	public static Bitmap GetUra()
	{
		return Properties.Resources.FC097_2;
	}
	public static Bitmap GetResName(string id) 
	{
		if (CardUI._resNames == null)
		{
			CardUI._resNames = new Dictionary<string, Bitmap>();
			CardUI._resNames["A4"] = Properties.Resources.FC001;
			CardUI._resNames["A3"] = Properties.Resources.FC002;
			CardUI._resNames["A2"] = Properties.Resources.FC003;
			CardUI._resNames["A1"] = Properties.Resources.FC004;
			CardUI._resNames["B4"] = Properties.Resources.FC005;
			CardUI._resNames["B3"] = Properties.Resources.FC006;
			CardUI._resNames["B2"] = Properties.Resources.FC007;
			CardUI._resNames["B1"] = Properties.Resources.FC008;
			CardUI._resNames["C4"] = Properties.Resources.FC009;
			CardUI._resNames["C3"] = Properties.Resources.FC010;
			CardUI._resNames["C2"] = Properties.Resources.FC011;
			CardUI._resNames["C1"] = Properties.Resources.FC012;
			CardUI._resNames["D4"] = Properties.Resources.FC013;
			CardUI._resNames["D3"] = Properties.Resources.FC014;
			CardUI._resNames["D2"] = Properties.Resources.FC015;
			CardUI._resNames["D1"] = Properties.Resources.FC016;
			CardUI._resNames["E4"] = Properties.Resources.FC017;
			CardUI._resNames["E3"] = Properties.Resources.FC018;
			CardUI._resNames["E2"] = Properties.Resources.FC019;
			CardUI._resNames["E1"] = Properties.Resources.FC020;
			CardUI._resNames["F4"] = Properties.Resources.FC021;
			CardUI._resNames["F3"] = Properties.Resources.FC022;
			CardUI._resNames["F2"] = Properties.Resources.FC023;
			CardUI._resNames["F1"] = Properties.Resources.FC024;
			CardUI._resNames["G4"] = Properties.Resources.FC025;
			CardUI._resNames["G3"] = Properties.Resources.FC026;
			CardUI._resNames["G2"] = Properties.Resources.FC027;
			CardUI._resNames["G1"] = Properties.Resources.FC028;
			CardUI._resNames["H4"] = Properties.Resources.FC029;
			CardUI._resNames["H3"] = Properties.Resources.FC030;
			CardUI._resNames["H2"] = Properties.Resources.FC031;
			CardUI._resNames["H1"] = Properties.Resources.FC032;
			CardUI._resNames["I4"] = Properties.Resources.FC033;
			CardUI._resNames["I3"] = Properties.Resources.FC034;
			CardUI._resNames["I2"] = Properties.Resources.FC035;
			CardUI._resNames["I1"] = Properties.Resources.FC036;
			CardUI._resNames["J4"] = Properties.Resources.FC037;
			CardUI._resNames["J3"] = Properties.Resources.FC038;
			CardUI._resNames["J2"] = Properties.Resources.FC039;
			CardUI._resNames["J1"] = Properties.Resources.FC040;
			CardUI._resNames["K4"] = Properties.Resources.FC041;
			CardUI._resNames["K3"] = Properties.Resources.FC042;
			CardUI._resNames["K2"] = Properties.Resources.FC043;
			CardUI._resNames["K1"] = Properties.Resources.FC044;
			CardUI._resNames["L4"] = Properties.Resources.FC045;
			CardUI._resNames["L3"] = Properties.Resources.FC046;
			CardUI._resNames["L2"] = Properties.Resources.FC047;
			CardUI._resNames["L1"] = Properties.Resources.FC048;
		}
		return _resNames[id];
	}
}

■場と手札を表示するためのユーザーコントロールを作る

チープな画面では、場と手札は ListBox を使っているので、これに対応するようにリストを扱えるようなユーザーコントロールを作ります。

場には最大12枚、手札は最大8枚なので、PictureBox をペタペタと張り付けてリスト化しておきます。可搬性はないのですが、このぐらいのユーザーコントロールならばペタペタ作ったほうがよいかと。
データバインドするために、DataSource プロパティを作っておくのがミソです。

public partial class BaControl : UserControl
{
	public BaControl()
	{
		InitializeComponent();

		_pics = new List<PictureBox>();
		_pics.Add(this.pictureBox1);
		_pics.Add(this.pictureBox2);
		_pics.Add(this.pictureBox3);
		_pics.Add(this.pictureBox4);
		_pics.Add(this.pictureBox5);
		_pics.Add(this.pictureBox6);
		_pics.Add(this.pictureBox7);
		_pics.Add(this.pictureBox8);
		_pics.Add(this.pictureBox9);
		_pics.Add(this.pictureBox10);
		_pics.Add(this.pictureBox11);
		_pics.Add(this.pictureBox12);
	}
	protected List<PictureBox> _pics;

	protected List<Card> _data;
	public List<Card> DataSource
	{
		get { return _data; }
		set
		{
			_data = value;
			UIUpdate();
		}
	}

	protected void UIUpdate()
	{
		int i = 0;
		if (this.DataSource != null)
		{
			foreach (var c in this.DataSource)
			{
				_pics[i++].Image = CardUI.GetResName(c.ID);
			}
		}
		for (; i < _pics.Count; ++i)
		{
			_pics[i].Image = CardUI.GetUra();
		}
	}
}

手札のほうも似た感じなのですが、PictureBox をクリックしたときのイベントを追加しておきます。

public partial class TefudaControl : UserControl
{
	public TefudaControl()
	{
		InitializeComponent();

		_pics = new List<PictureBox>();
		_pics.Add(this.pictureBox1);
		_pics.Add(this.pictureBox2);
		_pics.Add(this.pictureBox3);
		_pics.Add(this.pictureBox4);
		_pics.Add(this.pictureBox5);
		_pics.Add(this.pictureBox6);
		_pics.Add(this.pictureBox7);
		_pics.Add(this.pictureBox8);
	}
	protected List<PictureBox> _pics;

	protected List<Card> _data;
	public List<Card> DataSource
	{
		get { return _data; }
		set
		{
			_data = value;
			UIUpdate();
		}
	}

	protected void UIUpdate()
	{
		int i = 0;
		if (this.DataSource != null)
		{
			foreach (var c in this.DataSource)
			{
				_pics[i++].Image = CardUI.GetResName(c.ID);
			}
		}
		for (; i < _pics.Count; ++i)
		{
			_pics[i].Image = CardUI.GetUra();
		}
	}

	private void pict_Click(object sender, EventArgs e)
	{
		int i=0;
		foreach (var p in _pics)
		{
			if (p == sender)
			{
				if (p.Image != CardUI.GetUra())
				{
					if (ClickCard != null)
					{
						ClickCard(_data[i]);
						break;
					}
				}
			}
			i++;
		}
	}
	public event Action<Card> ClickCard;
}

■リッチな画面のコードを追加する

手札でカードをクリックしたときのイベントと、

/// <summary>
/// player1のカードを場に出す
/// </summary>
/// <param name="card"></param>
void tefudaControl1_ClickCard(Card c)
{
	_board.Player1.IntoBa(c);
	_board.Ba.PutCard(c);
	_curPlayer = _board.Player1;
	// 画面の更新
	ScrUpdate();
}

画面を更新するところのコードを少しだけ修正します。

/// <summary>
/// 画面の更新
/// </summary>
void ScrUpdate()
{
	listBox1.Items.Clear();
	listBox2.Items.Clear();
	listBox3.Items.Clear();
	listBox4.Items.Clear();
	listBox5.Items.Clear();
	listBox1.Items.AddRange(_board.Ba.Cards.ToArray());
	listBox2.Items.AddRange(_board.Player1.MyCards.ToArray());
	listBox3.Items.AddRange(_board.Player1.TakenCards.ToArray());
	listBox4.Items.AddRange(_board.Player2.MyCards.ToArray());
	listBox5.Items.AddRange(_board.Player2.TakenCards.ToArray());

	label7.Text = _board.Ba.PlayerCard.ToString();

	// 点を表示
	Yaku yaku1 = _board.Game.CalcYaku(_board.Player1.TakenCards);
	Yaku yaku2 = _board.Game.CalcYaku(_board.Player2.TakenCards);
	int ten1 = _board.Game.CalcTen(yaku1);
	int ten2 = _board.Game.CalcTen(yaku2);
	label10.Text = ten1.ToString();
	label11.Text = ten2.ToString();
	// ここだけ追加
	baControl1.DataSource = _board.Ba.Cards; 
	tefudaControl1.DataSource = _board.Player1.MyCards;
	tefudaControl2.DataSource = _board.Player2.MyCards;
}

こうするだけで、いや「だけ」ってほどではないですが、あまりコードを修正をせずに「ゲームらしい画面」ができがあるということで。

あと、追加するのは、

  • 手札や山から場に1枚だしたときのコントロール(忘れてた)
  • 持ち札のコントロール
  • 役ができたときのコントロール(必要?)

なところです。これを追加すると「静的なデータの表現」が終わるわけで、アニメーションとかありませんが、画面キャプチャ的にはそれなりが画面ができます。そのあとに「動的なデータの表現」ってプロセスが待っていて、ゲームらしい「動き」を追加します。

カテゴリー: C# | 脳内補完を現実にすることで物理的制約により準優先が決まる はコメントを受け付けていません

消えてしまった時間が消えてしまわないように足跡を残す

正月を潰して仕事、そのあた気疲れ(?)とインフルエンザの打撃…ってな感じで1月があっという間に過ぎてしまったわけですが。「あっという間」かどうかは、過去を振り替えてみるとわかるかも、ということで、しばらくは、日記風に blog を書き綴るということで、調子を戻しましょう > 自分、うんうん。

最初はチープな画面を作ってロジックを確認を | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/3982

の続きです。

  1. データクラスを作成する
  2. チープな画面を作成する

ってことで、Model と View が揃ったので、ゲームロジックを作っていきます。データ形式の持ち方はこれでいいのか?ってのを確かめるためでもあるし、ざっとゲームロジックを作ることで、それが「遊べるものかどうか?」ってのが確認できます。ええ、「遊べなそう」ってのが分かったら清く捨ててしまえばいいのです。そのために、この手のデータクラスとか、チープな画面をさっくりと作るのは大切かと。
この方法は、業務アプリでもよくやる方法で、「捨て去ってもいい時間」を使ってプロトタイプ的なものを作ります。世の中のプロトタイプだとお客に見せるために、結構手を込んだものを作る場合がありますが(プレゼンテーション的な意味があるので、それはそれで重要なのです)、自分で確認するプロトタイプ≒これが本当に動くのかどうかのプロトタイプの場合は、「捨てても惜しくない時間を割り当てます」。2,3時間というところでしょうか。長くて1日ですね。ただ、最近気づいたのは、1日でさっくりできるようになるための「実力」を付けるのは結構鍛錬がいるのではないかな、ということ。20年前のパソコン通信の頃の「この人の開発スピードってすごいな!」って程度には、やっとこさ、達することができたような気が…するんだけど、どうでしょう?いやいや、本当のところが、がっと集中して作れる程度が2,3時間ってところなんですけどね。ってことかも。

■データを作成する

さて、通常はそれらしいサンプルを作成するとこから(データクラスの操作性の確認も兼ねて)始めるわけですが、花札の場合は枚数が限られているので、データも作ってしまいます。
Card クラスに持たせてしまうのは異論がある!(私もある!)のですが、ざっと作ったということで。ゲームとしては「こいこい」を作る予定なので、点数自体には意味がないのですが、カス札とか短冊とかを区別するために必要ってことです。このあたりはユーティリティクラスを作っても同じですね。データ量が多い場合は、検索時の効率を考えて、ってのもあるのですが、このぐらいならば全件検索しても特に問題ないし。

/// <summary>
/// カード
/// </summary>
public class Card
{
	string _id;
...
	static protected Dictionary<string,int> _tens;
	/// <summary>
	/// 点数を取得
	/// </summary>
	public int Ten
	{
		get
		{
			if (Card._tens == null)
			{
				Card._tens = new Dictionary<string, int>();
				Card._tens["A1"] = 20; // 松に鶴
				Card._tens["A2"] = 5;
				Card._tens["A3"] = 1;
				Card._tens["A4"] = 1;
				Card._tens["B1"] = 10; // 梅に鶯
				Card._tens["B2"] = 5;
				Card._tens["B3"] = 1;
				Card._tens["B4"] = 1;
				Card._tens["C1"] = 20; // 桜に幕
				Card._tens["C2"] = 5;
				Card._tens["C3"] = 1;
				Card._tens["C4"] = 1;
				Card._tens["D1"] = 10; // 藤にホトトギス
				Card._tens["D2"] = 5;
				Card._tens["D3"] = 1;
				Card._tens["D4"] = 1;
				Card._tens["E1"] = 10; // 菖蒲に八つ橋
				Card._tens["E2"] = 5;
				Card._tens["E3"] = 1;
				Card._tens["E4"] = 1;
				Card._tens["F1"] = 10; // 牡丹に蝶
				Card._tens["F2"] = 5;
				Card._tens["F3"] = 1;
				Card._tens["F4"] = 1;
				Card._tens["G1"] = 10; // 萩に猪
				Card._tens["G2"] = 5;
				Card._tens["G3"] = 1;
				Card._tens["G4"] = 1;
				Card._tens["H1"] = 20; // ススキに月
				Card._tens["H2"] = 10;
				Card._tens["H3"] = 1;
				Card._tens["H4"] = 1;
				Card._tens["I1"] = 10; // 菊に盃
				Card._tens["I2"] = 5;
				Card._tens["I3"] = 1;
				Card._tens["I4"] = 1;
				Card._tens["J1"] = 10; // 銀杏に鹿
				Card._tens["J2"] = 5;
				Card._tens["J3"] = 1;
				Card._tens["J4"] = 1;
				Card._tens["K1"] = 20; // 小野道風に蛙
				Card._tens["K2"] = 10;
				Card._tens["K3"] = 5;
				Card._tens["K4"] = 1;
				Card._tens["L1"] = 50; // 桐に鳳凰
				Card._tens["L2"] = 1;
				Card._tens["L3"] = 1;
				Card._tens["L4"] = 1;
			}
			return Card._tens[_id];
		}
	}
	public override string ToString()
	{
		return _id;
	}
}

■役を作る

ざっくりとデータができたら、役を作ります。いわゆるカードゲームの場合は、同じパターンで作るのではないかな、と。最初は「役を計算する」部分と「点数を計算する」部分とを独立させて作っていたのですが、結局のところ「役」を使って「点数」を計算することになるので、CalcYaku と CalcTen は無関係になっていません。まあ、これはこれでよいかと。

/// <summary>
/// 役クラス
/// </summary>
public class Yaku
{
	public List<Card> GoKou;
	public List<Card> ShiKou;
	public List<Card> AmeShiKou;
	public List<Card> SanKou;
	public List<Card> InoShika;
	public List<Card> AkaTan;
	public List<Card> AoTan;
	public List<Card> Hanami;
	public List<Card> Tukimi;
	public List<Card> Tane;
	public List<Card> Tan;
	public List<Card> Kasu;
}

/// <summary>
/// ゲームルールライブラリ
/// </summary>
public class Game
{
...
	/// <summary>
	/// 役を計算する
	/// </summary>
	/// <param name="cards"></param>
	/// <returns></returns>
	public Yaku CalcYaku(List<Card> cards)
	{
		Yaku yaku = new Yaku();
		// 菊に盃の有無をチェック
		bool I1 = cards.Find(n => n.ID == "I1") != null;

		// 20点札をチェック
		var cs = cards.FindAll(n => n.Ten == 20);
		if (cs.Count == 5)
			yaku.GoKou = new List<Card>( cs );
		else if ( cs.Count == 4 ) 
			if (cs.Find(n => n.ID == "K1") != null ) 
 					yaku.AmeShiKou = new List<Card>( cs );
			else 
				yaku.ShiKou = new List<Card>( cs );
		else if ( cs.Count == 3 ) 
			yaku.SanKou = new List<Card>( cs );

		// 花見、月見
		cs = cards.FindAll(n => n.ID == "I1" || n.ID == "C1");
		if ( cs.Count == 2 )
			yaku.Hanami = new List<Card>(cs);
		cs = cards.FindAll(n => n.ID == "I1" || n.ID == "H1");
		if (cs.Count == 2)
			yaku.Hanami = new List<Card>(cs);

		// 猪鹿蝶
		cs = cards.FindAll(n => n.ID == "G1" || n.ID == "J1" || n.ID == "F1");
		if (cs.Count == 3)
		{
			yaku.InoShika = new List<Card>(cs);
			// タネに追加
			yaku.Tane = cards.FindAll(n => n.Ten == 10);
		}
		// 赤短
		cs = cards.FindAll(n => n.ID == "A2" || n.ID == "B2" || n.ID == "C2");
		if ( cs.Count == 3 ) 
			yaku.AkaTan = new List<Card>(cs);
		// 青短
		cs = cards.FindAll(n => n.ID == "I2" || n.ID == "J2" || n.ID == "F2");
		if ( cs.Count == 3 )
			yaku.AoTan = new List<Card>(cs);
		if (yaku.AkaTan != null || yaku.AoTan != null)
		{
			// タンに追加
			yaku.Tan = cards.FindAll(n => n.Ten == 5); 
		}
		// タネ
		if ( yaku.Tane == null ) {
			cs = cards.FindAll(n => n.Ten == 10);
			if ( cs.Count >= 5 ) 
				yaku.Tane = new List<Card>(cs);
		}
		// タン
		if ( yaku.Tan == null ) {
			cs = cards.FindAll(n => n.Ten == 5);
			if ( cs.Count >= 5 ) 
				yaku.Tane = new List<Card>(cs);
		}
		// カス
		cs = cards.FindAll(n => n.Ten == 1);
		if ( I1 == true && yaku.Tane.Find(n=>n.ID== "I1") == null ) 
			cs.Add( cards.Find( n=>n.ID == "I1" ));
		if ( cs.Count >= 10 ) 
			yaku.Kasu = new List<Card>(cs);

		return yaku;
	}

	/// <summary>
	/// 点数を計算する
	/// </summary>
	/// <param name="yaku"></param>
	/// <returns></returns>
	public int CalcTen( Yaku yaku )
	{
		int ten = 0;
		// 20点札をチェック
		if (yaku.GoKou != null) ten += 10;	// 五光
		if (yaku.SanKou != null) ten += 8;	// 四光
		if (yaku.AmeShiKou != null) ten += 7;	// 雨四光
		if (yaku.SanKou != null) ten += 5;	// 三光
		if (yaku.Hanami != null) ten += 5;	// 花見
		if (yaku.Tukimi != null) ten += 5;	// 月見
		if (yaku.InoShika != null)	// 猪鹿蝶
		{
			ten += 5;
			// タネ分を加算
			if (yaku.Tane != null)
				ten += yaku.Tane.Count - 3;
		}
		if (yaku.AkaTan != null) ten += 5;	// 赤短
		if (yaku.AoTan != null) ten += 5;	// 青短
		if (yaku.AoTan != null || yaku.AkaTan != null)
		{
			// タン分を加算
			if (yaku.Tan != null)
				ten += yaku.Tan.Count - yaku.AkaTan.Count - yaku.AoTan.Count;
		}
		// タネ
		if (yaku.Tane != null && yaku.InoShika == null)
			ten += yaku.Tane.Count - 4;
		// タン
		if (yaku.Tan != null && yaku.AkaTan == null && yaku.AoTan == null)
			ten += yaku.Tan.Count - 4;
		// カス
		if (yaku.Kasu != null)
			ten += yaku.Kasu.Count - 9;
		return ten;
	}
}

で、これを組み込んで、チープな画面を使ってテストをすると、そこそこ「遊べる」ことがわかります。昔ならばコマンドラインでぽちぽちとキーボードでテストをしていたのですが、Windows フォームが簡単にできるようになったので、これはこれでよいかと。

■お次は

  1. データクラスを作成する
  2. チープな画面を作成する
  3. ゲームロジックを作成する

ってところまでできたので、この度は View の部分を少しリッチにしてみます。
リッチにするところは、

  1. 花札の画像を使って表示する。
  2. 花札ゲームのアクションを作る。

ってところです。花札の画像自体は、Windows ストア アプリのゲーム画像を流用するとして、アクションの部分は、MVVM とユーザーコントロールとの兼ね合いもあるので、ちょっと試行中。

カテゴリー: 雑談 | 2件のコメント

最初はチープな画面を作ってロジックを確認を

今更ながら年始の雑文で穴埋めを | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/3978

の続き。

ロジックの確認のために、下記のようなチープな画面を作成する。

この手のロジックをチェックするのは、UnitTest を使うのがいいのですが、今回は UI のチェックも兼ねるので、Windows フォームを使っています。最初は、いきなり Windows ストア アプリにしようかと思ったのですが、UI のコーディングがバインドと混ざってややこしくなるので、フォームアプリで作成ということで。

フォームに書いたコードは使い捨てになるので、できるだけ簡潔に…と云いますか、フォームでもWindows ストア アプリでもそれなりに動くようにしたいので(コードレベルでは無理なので、ロジックの手続きレベルで)、ロジックの呼び出しテストも兼ねて。

public partial class Form1 : Form
{
	public Form1()
	{
		InitializeComponent();

		_board.Player1.EventSelCard += Player1_EventSelCard;

		listBox1.Sorted = true;
		listBox2.Sorted = true;
		listBox3.Sorted = true;
		listBox4.Sorted = true;
		listBox5.Sorted = true;

	}

	/// <summary>
	/// 2枚から選択する
	/// </summary>
	/// <param name="c1"></param>
	/// <param name="c2"></param>
	/// <returns></returns>
	Card Player1_EventSelCard(Card c1, Card c2)
	{

		string msg = string.Format(
			"{0} と {1} があります。{2} を選択しますか?",
			c1.ID, c2.ID, c1.ID);
		var btn = MessageBox.Show(msg, "", MessageBoxButtons.YesNo);
		if (btn == System.Windows.Forms.DialogResult.Yes)
		{
			return c1;
		}
		else
		{
			return c2;
		}
	}

	GameBoard _board = new GameBoard();
	Player _curPlayer;
	/// <summary>
	/// 山から1枚取得
	/// </summary>
	/// <param name="sender"></param>
	/// <param name="e"></param>
	private void button1_Click(object sender, EventArgs e)
	{
		Card c = _board.Yama.GetCard();
		_board.Ba.PutCard(c);
		// 画面の更新
		ScrUpdate();
	}

	/// <summary>
	/// マッチしたカードを場から取得
	/// </summary>
	/// <param name="sender"></param>
	/// <param name="e"></param>
	private void button2_Click(object sender, EventArgs e)
	{
		var lst = _board.Game.MatchBa(_board.Ba, _board.Ba.PlayerCard, _curPlayer);
		_board.Ba.GetCard(lst);
		_curPlayer.GetCard(lst);
		// 画面の更新
		ScrUpdate();
	}

	/// <summary>
	/// 花札を配る
	/// </summary>
	/// <param name="sender"></param>
	/// <param name="e"></param>
	private void button5_Click(object sender, EventArgs e)
	{
		// 花札を配る
		_board.Reset();
		_curPlayer = _board.Player1;
		// 画面の更新
		ScrUpdate();
	}

	/// <summary>
	/// player1のカードを場に出す
	/// </summary>
	/// <param name="sender"></param>
	/// <param name="e"></param>
	private void button3_Click(object sender, EventArgs e)
	{
		if (listBox2.SelectedIndex == -1) 
			return;

		var c = (Card)listBox2.SelectedItem;
		_board.Player1.IntoBa(c);
		_board.Ba.PutCard(c);
		_curPlayer = _board.Player1;
		// 画面の更新
		ScrUpdate();
	}

	/// <summary>
	/// player2のカードを場に出す
	/// </summary>
	/// <param name="sender"></param>
	/// <param name="e"></param>
	private void button4_Click(object sender, EventArgs e)
	{

		if (listBox4.SelectedIndex == -1)
			return;

		var c = (Card)listBox4.SelectedItem;
		_board.Player2.IntoBa(c);
		_board.Ba.PutCard(c);
		_curPlayer = _board.Player2;
		// 画面の更新
		ScrUpdate();
	}

	/// <summary>
	/// 画面の更新
	/// </summary>
	void ScrUpdate()
	{
		listBox1.Items.Clear();
		listBox2.Items.Clear();
		listBox3.Items.Clear();
		listBox4.Items.Clear();
		listBox5.Items.Clear();
		listBox1.Items.AddRange(_board.Ba.Cards.ToArray());
		listBox2.Items.AddRange(_board.Player1.MyCards.ToArray());
		listBox3.Items.AddRange(_board.Player1.TakenCards.ToArray());
		listBox4.Items.AddRange(_board.Player2.MyCards.ToArray());
		listBox5.Items.AddRange(_board.Player2.TakenCards.ToArray());

		label7.Text = _board.Ba.PlayerCard.ToString();
	}
}

画面のロジックは基本はユーザーからのイベントドリブンで、ユーザーがなんらかの選択をした時に発生します。昔は業務ロジックと画面からのイベントを分離させるために、「イベントごとに関数を作る」ってなことをやってたりしますが、煩雑なので、最近はパスですね。ですが、NUnit などの自動テストを有効に働かせるためには、画面のほうに業務ロジックを入れないようにするのがベター…なのですが、いまのところこれといった定番がありません。
業務アプリのような簡単な(?)画面の場合には、MVVM か MVC で分離させて、業務ロジックを NUnit でテストして、くっつけた上でアプリ上で再テストって手順がよいのですが、ゲームの場合はどうなんでしょうね?ってのが、今の私の(解消しなければいけない)疑問点です。

そんな訳で、ユーザーがアクションをするボタンの類と、画面更新 ScrUpdate という簡単な組み合わせにしています。このボタンイベントのところが長くなったら、適宜ロジックのほうへ移行ってな雰囲気で組み立てるとよいかと。

カテゴリー: 雑談 | 最初はチープな画面を作ってロジックを確認を はコメントを受け付けていません

今更ながら年始の雑文で穴埋めを

年末年始も二週間が過ぎて皆様お仕事ご学業お試験などは如何でございましょうか…などと年始の挨拶をしたいわけでもなく、なんとなく過ぎてしまった2週間ではありますが、いえ、なんか、だらだと仕事をしていただけなんですけどね。やっぱり休むときは休まないと、捗るものも捗らないのかと思い至ったり。

ひとまず無事、Microsoft MVP の Visual C#(今回はC#)を頂き、その MSDN は引き続き使えるわけで、その先に Windows ストア アプリがありーのという訳で、さて、去年の「親馬鹿アプリ」にするのか、「化粧男子シリーズ」にするのか、それとも何かきちっとしたものを?と考えてあぐねたまま数日が過ぎ、という具合で今日に至っております。

で、なんか手頃なゲーム用コンポーネント作り、というか、Windows ストア アプリのゲームを作る中でゲームらしいコンポーネントの組み合わせを作れないかと思案していたのですが、結局のところ「花札」を作ってみようかという路線で落ち着いております。ええ、MVVM というか、MVC というか、ゲームロジックの部分をきちんと分類させておいて、UI の部分は切り替えられるように、かつ、ゲームロジックを差し替え可能なように(ネットワーク対戦とか、コンピュータ対戦とか)という実用的なサンプルですね。ねらいとしては、

  • ゲームロジックの使い回しが可能(Win8, iPhoneで)
  • UI の切り替えが可能
  • UI のアクション部分が切り替えが可能

ってな風にしたいわけです。
「ゲームロジックの使いまわし」ってのは、誰もが考えることなのですが、これはゲームアプリを発表した後にも「定期的にバージョンアップ」できるようにするためです。ええ、Windows ストア アプリとiPadアプリの両方に対応するという意味もあります。
ハードルを高くして、C# と Objective-C のコード共有…というか、C++で書けば、両方で共有できるわけですが、それはせずに、UML レベルで共有という感じで。そもそも「ロジック」部分というのは、それほど言語に差がないわけで(関数型言語を使うと、かなり違いますが)、C#, Objective-C, Java あたりをターゲットにしている間は、どれも UML レベルで統括が可能でしょう。

そのなかで「定期的なバージョンアップ」ってのがあって、昨今のゲームアプリなりを発表した後で、そのままってのはちょっと難しいかなと思っています。かといって、次々と別のゲームロジックを考えるのも大変で(いろいろあるにはあるんでしょうが)、そうなると、なんらかのスライドしたアイデアを盛り込めるような作りをしておく、いや、アイデアをコードレベルで追加しやすいようにしておくというのがポイントかと思います。ええ、少なくとも自分には。

UI の切り離しってのは、定番ではありますが、札のデザインもそうなのですが、

  • 札の並び方(ばらばらっと置くような感じとか)
  • なんとなく 3D っぽく置くような感じとか
  • ルールが変わっても、あまり UI 部分で苦労しないような作りにするとか

ってのを目指します。このあたり、XAML ならば、かなり手軽に作れそうな感じがしています。手札なり場なりをコンポーネントで作っておいて、それぞれを storyboard でアニメーションさせるとか、そんな感じですね。

で、ゲームロジックのほうなのですが、いわゆる「いかさまロジック」をきっちりと入れるのが目標です。「いかさまロジック」をわざわざ入れるというのも変ですが、コンピューター対戦の場合は、それなりの「人格」みたいなのがあったほうがおもしろいですよね。で、たいていの場合はランダム値を上げるとか、ユーザーの引きを弱くするとか、という不当な方法に走ってしまうわけですが、そういう「納得いかーん」というものではなくて、「自摸、嶺上開花」とかそいういうやつです。まあ、最終的には麻雀ロジックのいかさまバージョンを作りたいのですが、ちょっと今の私には難しいので、手始めに花札から、という具合。花札アプリを作っている間に、なんらかの UI コンポーネントも徐々に揃えられるかなと。

ひとまず、任天堂の花札のページを参考替わりに。

花札の歴史・遊び方
http://www.nintendo.co.jp/n09/hana-kabu_games/index.html

■ゲームロジックを作ってみる

「手四」の場合、配り直しになっていますが、まあこんな感じで。

  • Card
  • Player
  • Yama
  • Ba

というクラスを作っておきます。これはいわゆる Value Class ですね。Player クラスが、思考ロジックを含んでいるので、ちょっと妙ですが、このあたりは実際に XAML にバインドをさせたときに工夫していきましょう。

GameBoard は、Player, Yama, Ba をまとめたクラスです。これは、UI のほうに持っていくとコードが煩雑になる…というか分離できなくなるので、コントロール系のクラスとして使います。あと、ゲームのルール自体を Game クラスが受け持つ感じ。これは、ライブラリ的に使います。

namespace Hanahuda
{
	/// <summary>
	/// カード
	/// </summary>
	public class Card
	{
		string _id;

		protected Card() { }
		/// <summary>
		/// コンストラクタ
		/// </summary>
		/// <param name="id"></param>
		public Card(string id)
		{
			_id = id;
		}

		public string ID
		{
			get { return _id; }
		}
		/// <summary>
		/// 月を取得
		/// </summary>
		public string Month
		{
			get { return _id.Substring(0, 1); }
		}
		/// <summary>
		/// 番号を取得
		/// </summary>
		public string Num
		{
			get { return _id.Substring(1, 1); }
		}

		public override string ToString()
		{
			return _id;
		}
	}

	/// <summary>
	/// 山
	/// </summary>
	public class Yama
	{
		/// <summary>
		/// 花札のコレクション
		/// </summary>
		public List<Card> Cards = new List<Card>();

		/// <summary>
		/// カードを山から取る
		/// </summary>
		/// <returns></returns>
		public Card GetCard()
		{
			if (Cards.Count == 0)
				return null;
			Card c = Cards[0];
			Cards.RemoveAt(0);
			return c;
		}
	}
	/// <summary>
	/// 場
	/// </summary>
	public class Ba
	{
		/// <summary>
		/// 花札のコレクション
		/// </summary>
		public List<Card> Cards = new List<Card>();

		// プレイヤーが出したカード
		public Card PlayerCard = new Card("");

		/// <summary>
		/// 場に1枚カードを出す
		/// </summary>
		/// <param name="c"></param>
		public void PutCard(Card c)
		{
			PlayerCard = c;
		}

		/// <summary>
		/// マッチしたカードを取り除く
		/// </summary>
		/// <param name="cs"></param>
		public void GetCard(IEnumerable<Card> cs)
		{
			if (cs.Count() == 0)
			{
				Cards.Add(PlayerCard);
			}
			else
			{
				foreach (var c in cs)
				{
					this.Cards.Remove(c);
				}
			}
			PlayerCard = new Card("");

		}
	}

	/// <summary>
	/// プレイヤー
	/// </summary>
	public class Player
	{
		// 手持ちのカード
		public List<Card> MyCards = new List<Card>();
		// 取ったカード
		public List<Card> TakenCards = new List<Card>();

		/// <summary>
		/// 手持ちにカードを加える
		/// </summary>
		/// <param name="c"></param>
		public void GetCard(Card c)
		{
			MyCards.Add(c);
		}
		public void GetCard(IEnumerable<Card> cs)
		{
			TakenCards.AddRange(cs);
		}

		/// <summary>
		/// カードを場に捨てる
		/// </summary>
		/// <param name="c"></param>
		/// <returns></returns>
		public Card IntoBa(Card c)
		{
			MyCards.Remove(c);
			return c;
		}

		public delegate Card HandlerSelCard( Card c1, Card c2 );
		public event HandlerSelCard EventSelCard;
		/// <summary>
		/// 場に2枚あるときに選択する
		/// </summary>
		/// <param name="c1"></param>
		/// <param name="c2"></param>
		/// <returns></returns>
		public Card SelCard(Card c1, Card c2)
		{
			if (EventSelCard != null)
			{
				return EventSelCard(c1, c2);
			} else {
				return c1;
			}
		}

	}

	/// <summary>
	/// ゲームルールライブラリ
	/// </summary>
	public class Game
	{
		/// <summary>
		/// 場と1枚のカードをチェックする
		/// </summary>
		/// <param name="ba"></param>
		/// <param name="card"></param>
		/// <returns></returns>
		public List<Card> MatchBa(Ba ba, Card card, Player pl)
		{
			var lst = new List<Card>();

			var cs = ba.Cards.Where(n => n.Month == card.Month);
			if (cs.Count() == 0)
				return lst;
			lst.Add(card);

			if (cs.Count() == 1) {
				lst.Add(cs.First());
			} else if ( cs.Count() == 2 ) {
				// どちらを取るか選択させる
				// lst.Add(cs.First());
				Card c1 = cs.First();
				Card c2 = cs.Last();
				Card c = pl.SelCard(c1, c2);
				lst.Add(c);
			} else if ( cs.Count() == 3 ) {
				lst.AddRange(cs);
			}
			return lst;
		}
	}

	/// <summary>
	/// ゲームボード
	/// </summary>
	public class GameBoard
	{
		Player _player1 = new Player();
		Player _player2 = new Player();
		Ba _ba = new Ba();
		Yama _yama = new Yama();
		Game _game = new Game();

		public Player Player1 { get { return _player1; } }
		public Player Player2 { get { return _player2; } }
		public Ba Ba { get { return _ba; } }
		public Yama Yama { get { return _yama; } }
		public Game Game { get { return _game; } }

		/// <summary>
		/// コンストラクタ
		/// </summary>
		public GameBoard()
		{
		}

		/// <summary>
		/// 花札を配る
		/// </summary>
		public void Reset()
		{
			Yama.Cards.Clear();
			Ba.Cards.Clear();
			Player1.MyCards.Clear();
			Player1.TakenCards.Clear();
			Player2.MyCards.Clear();
			Player2.TakenCards.Clear();

			// カードを作成
			List<Card> cards = new List<Card>();
			for (int m = 1; m <= 12; m++)
			{
				for (int n = 1; n <= 4; n++)
				{
					Card c = new Card(string.Format("{0}{1}", Convert.ToChar('A'+m-1) , n));
					cards.Add(c);
				}
			}
			while (true)
			{
				// カードをシャッフル
				Random rnd = new Random();
				while (cards.Count > 0)
				{
					int n = rnd.Next(cards.Count);
					Yama.Cards.Add(cards[n]);
					cards.RemoveAt(n);
				}
				// player1, player2, ba の順に8枚ずつ配る
				for (int i = 0; i < 4; i++)
				{
					Player1.GetCard(Yama.GetCard());
					Player1.GetCard(Yama.GetCard());
					Player2.GetCard(Yama.GetCard());
					Player2.GetCard(Yama.GetCard());
					Ba.Cards.Add(Yama.GetCard());
					Ba.Cards.Add(Yama.GetCard());
				}
				// 4枚まとまっていないかチェック
				bool flag1 = Check4Cards(Player1.MyCards);
				bool flag2 = Check4Cards(Player2.MyCards);
				bool flag3 = Check4Cards(Ba.Cards);
				if (flag1 == false && flag2 == false & flag3 == false)
					break;
			}
		}

		bool Check4Cards(List<Card> cs)
		{
			var mon = new Dictionary<string, int>();
			foreach (var c in Player1.MyCards)
			{
				if (mon.ContainsKey(c.Month) == false)
				{
					mon[c.Month] = 1;
				}
				else
				{
					mon[c.Month]++;
				}
			}
			bool flag = false;
			foreach (var v in mon.Values)
			{
				if (v == 4)
				{
					flag = true;
					break;
				}
			}
			return flag;
		}
	}
}

ひとつ悩みどころが、Player.SelCard メソッドです。これは、いわゆる「ユーザーに問い合わせ」をするところで、MVVM, MVC のロジックでも悩みどころですね。コンピュータ思考ルーチンの場合は、直接結果を返せばよいのですが、ユーザーが担当している場合は問い合わせのダイアログを出して、応答待ちになります。この問いあわせの部分をどう作るか、です。

特に分離を考えなければ、UI のほうでチェックしてもよいのですが、そうなると思考ルーチンのほうが変なことになるし…ということで、EventSelCard というイベントを使っています。これが定番なのかどうかは微妙なのですが、

  1. 手札を場に出すアクション
  2. 出した札に対して、2枚マッチすることを判断
  3. ユーザーに問い合わせをする。
  4. ユーザーが選択した札とマッチさせる。
  5. Model で Player.MyCards と Ba.Cards 差し替え
  6. 手札と場の UI を更新

という手順になるので、

  1. UI
  2. Logic
  3. UI
  4. Logic
  5. Model
  6. UI

という流れで UI が間に挟まるパターンですね。この手のユーザー問い合わせはゲームでは頻繁にあるので、ここをうまくクリアに作れないと、MVVM なり MVC なりがうまく組み込めないわけです。業務ロジックの場合は、それほど複雑ではないのと場所が限ら得ているので、そこだけ特殊に作ってしまいますが、ゲームの場合は頻繁にありそう。
それに、コンピュータの思考ルーチンであっても、2枚のマッチを判断するのは Logic の問い合わせになるわけで、この「思考ルーチン」の差し替えを有効にするために、ここはきちんと UI と分離させておかないと困るわけです。そんなわけで、わざわざイベントハンドラにしています…と云いますか、イベントハンドラにしてみました。

これが有効に働くかどうかは、後日。

カテゴリー: C# | 今更ながら年始の雑文で穴埋めを はコメントを受け付けていません