年末年始も二週間が過ぎて皆様お仕事ご学業お試験などは如何でございましょうか…などと年始の挨拶をしたいわけでもなく、なんとなく過ぎてしまった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 というイベントを使っています。これが定番なのかどうかは微妙なのですが、
- 手札を場に出すアクション
- 出した札に対して、2枚マッチすることを判断
- ユーザーに問い合わせをする。
- ユーザーが選択した札とマッチさせる。
- Model で Player.MyCards と Ba.Cards 差し替え
- 手札と場の UI を更新
という手順になるので、
- UI
- Logic
- UI
- Logic
- Model
- UI
という流れで UI が間に挟まるパターンですね。この手のユーザー問い合わせはゲームでは頻繁にあるので、ここをうまくクリアに作れないと、MVVM なり MVC なりがうまく組み込めないわけです。業務ロジックの場合は、それほど複雑ではないのと場所が限ら得ているので、そこだけ特殊に作ってしまいますが、ゲームの場合は頻繁にありそう。
それに、コンピュータの思考ルーチンであっても、2枚のマッチを判断するのは Logic の問い合わせになるわけで、この「思考ルーチン」の差し替えを有効にするために、ここはきちんと UI と分離させておかないと困るわけです。そんなわけで、わざわざイベントハンドラにしています…と云いますか、イベントハンドラにしてみました。
これが有効に働くかどうかは、後日。