ヌメロン製作講座第18回:コール&ジャッジ処理
今回はコール&ジャッジ処理に対応し、コンピュータを相手にゲームができる状態にします。
但しコンピュータの思考ルーチンは完全ランダムなので初心者にはちょうど良いぐらいに弱いです。
- ゲーム画面
- 修正ソースファイル
- 修正個所
Numer0n.cpp
ヌメロンソースファイルにグローバル関数を追加しました。
- 追加関数
- Rank ColumnToRank( Column column );
- 桁数から位数を取得する
- Column RankToColumn( Rank rank );
- 位数から桁数を取得する
- VOID NumberToArray( Number number, Array array );
- ナンバーを番号配列に変換する
- Number ArrayToNumber( Array & array );
- 番号配列をナンバーに変換する
- VOID Judge( Number number, Call & call );
- コールナンバーをジャッジする
- Number GetLastCallNumer( MoveID move );
- 最後のコールナンバーを取得する
- Number ThinkSetNumber( MoveID move );
- 設定ナンバーを生成する
- Number ThinkCallNumber( MoveID move );
- コールナンバーを生成する
Rank ColumnToRank()
桁数を指定して位数を取得します。
Rank ColumnToRank( Column column ) { Rank rank = 0; if ( column >= 0 ) { for ( rank = 1; column > 0; column-- ) { rank *= MAX_DIGIT; } } return rank; }
桁数が負の場合は位数がゼロになるので結果をエラー判定に使用できます。
Column RankToColumn()
逆に位数を指定して桁数を取得します。
Column RankToColumn( Rank rank ) { Column column = -1; for ( ; rank >= 1; rank /= MAX_DIGIT ) { column++; } return column; }
位数が不正な場合は負数を返します。
VOID NumberToArray()
ナンバーから各桁ごとの番号配列を生成します。
VOID NumberToArray( Number number, Array array ) { for ( int i = 0; i < MAX_COLUMN; i++ ) { array[ i ] = ( DigitID ) ( number % MAX_DIGIT ); number /= MAX_DIGIT; } }
1桁目が配列の添え字のゼロになります。
Number ArrayToNumber()
逆に番号配列からナンバーを取得します。
Number ArrayToNumber( Array & array ) { Rank rank = 1; Number number = 0; for ( int n = 0; n < maxColumn; n++ ) { number += rank * array[ n ]; rank *= MAX_DIGIT; } return number; }
VOID Judge()
設定ナンバーとコールナンバーを与えてイート数とバイト数を取得します。
VOID Judge( Number number, Call & call ) { Array array; Array array2; NumberToArray( number, array ); NumberToArray( call.number, array2 ); call.eat = 0; call.byte = 0; for ( int column = 0; column < maxColumn; column++ ) { for ( int column2 = 0; column2 < maxColumn; column2++ ) { if ( array2[ column ] == array[ column2 ] ) { if ( column == column2 ) { call.eat++; } else { call.byte++; } } } } }
number が設定ナンバーです。
call.number がコールナンバーです。
Number GetLastCallNumer()
指定した手番が最後にコールしたナンバーを取得します。
Number GetLastCallNumer( MoveID move ) { for ( int n = maxRecord - 1; n >= 0; n-- ) { Record & record = records[ n ]; if ( record.move == move && record.did == DATA_CALL ) { return record.call.number; } } return anals[ move ][ 0 ].candids[ 0 ]; }
棋譜データを最後尾から検索して指定された手番のコールナンバーを返します。
存在しない場合は最初の正解候補ナンバーを返します。
Number ThinkSetNumber()
指定手番の設定ナンバーをランダムで生成します。
Number ThinkSetNumber( MoveID move ) { int maxList = 0; DigitID list[ MAX_DIGIT ]; Array array; int n; for ( int digit = 0; digit < MAX_DIGIT; digit++ ) { if ( players[ move ].cards[ digit ] > 0 ) { list[ maxList++ ] = ( DigitID ) digit; } } NumberToArray( 0, array ); for ( int column = 0; column < maxColumn; column++ ) { n = rand() % maxList; array[ column ] = list[ n ]; for ( int i = n; i < maxList - 1; i++ ) { list[ i ] = list[ i + 1 ]; } maxList--; } return ArrayToNumber( array ); }
ロストカードは含まれないようにチェックしています。
Number ThinkCallNumber()
指定手番のコールナンバーをランダムで生成します。
Number ThinkCallNumber( MoveID move ) { int n = rand() % anals[ move ][ maxRecord ].maxCandid; return anals[ move ][ maxRecord ].candids[ n ]; }
分析データの正解候補ナンバー配列からランダムで選択しています。
struct Analyze
コール&ジャッジに対応するためのメソッドを分析データ構造体に追加しました。
- 追加メソッド
- VOID CandidateByValid( BOOL * buffer, Number maxNumber );
- 有効番号をもとに正解候補を絞り込む
- VOID ValidateByCandidate( BOOL * buffer, Number maxNumber );
- 正解候補をもとに有効番号を絞り込む
- VOID CheckDecide( BOOL * buffer, Number maxNumber );
- 確定番号をチェックする
- VOID AddData( BOOL * buffer, Number maxNumber, MoveID move, const Record & record );
- 棋譜データを追加して分析データを更新する
VOID CandidateByValid()
有効番号データをもとに正解候補を絞り込みます。
VOID Analyze :: CandidateByValid( BOOL * buffer, Number maxNumber ) { Array array; BOOL result; for ( int number = 0; number < maxNumber; number++ ) { NumberToArray( number, array ); result = TRUE; for ( int column = 0; result && column < maxColumn; column++ ) { if ( ! valids[ column ][ array[ column ] ] ) { result = FALSE; } } if ( ! result ) { buffer[ number ] = FALSE; maxCandid--; } } }
正解候補ナンバーに無効番号が含まれているかチェックしてはじいています。
VOID ValidateByCandidate()
すべての正解候補ナンバーをもとに有効番号を絞り込みます。
VOID Analyze :: ValidateByCandidate( BOOL * buffer, Number maxNumber ) { Array array; ZeroMemory( valids, sizeof valids ); for ( int number = 0; number < maxNumber; number++ ) { if ( buffer[ number ] ) { NumberToArray( number, array ); for ( int column = 0; column < maxColumn; column++ ) { valids[ column ][ array[ column ] ] = TRUE; } } } }
各桁ごとに正解候補ナンバーに含まれる番号のみを有効番号として抽出しています。
VOID CheckDecide()
確定番号をチェックします。
VOID Analyze :: CheckDecide( BOOL * buffer, Number maxNumber ) { int count; Array array; for ( int digit = 0; digit < MAX_DIGIT; digit++ ) { count = 0; for ( int number = 0; number < maxNumber; number++ ) { if ( buffer[ number ] ) { NumberToArray( number, array ); for ( int column = 0; column < maxColumn; column++ ) { if ( array[ column ] == digit ) { count++; } } } } decides[ digit ] = ( count == maxCandid ); } }
その番号がすべての正解候補ナンバーに含まれていれば確定番号となります。
VOID AddData()
追加された棋譜データをもとに分析データを更新します。
VOID Analyze :: AddData( BOOL * buffer, Number maxNumber, MoveID move, const Record & record ) { Call call2; if ( record.move == move ) { if ( record.did == DATA_CALL ) { call2.number = record.call.number; for ( int number = 0; number < maxNumber; number++ ) { if ( buffer[ number ] ) { Judge( number, call2 ); if ( call2.eat != record.call.eat || call2.byte != record.call.byte ) { buffer[ number ] = FALSE; } else if ( number == record.call.number && record.call.eat != maxColumn ) { buffer[ number ] = FALSE; } } } ValidateByCandidate( buffer, maxNumber ); } } }
追加されたジャッジ結果と一致しない正解候補ナンバーを除外します。
またフルイートでないコールナンバーも除外します。
class Numer0n
棋譜データ削除メソッドを追加しました。
- 追加メソッド
- VOID DeleteRecord()
- 最後の棋譜データを削除する
VOID DeleteRecord()
最後尾の棋譜データを削除します。 VOID Numer0n :: DeleteRecord() { if ( maxRecord > 0 ) { maxRecord--; } }
コールにしか対応してないのでこれだけです。
アイテム使用の削除に対応する場合はいろいろやることがあります。
main.cpp
分析データの表示と非公開ナンバーの公開コマンドに対応するためグローバル変数を追加しました。
それぞれのコマンドボタン処理も実装しました。
- 追加変数
- BOOL analMode;
- 分析データ表示フラグ
- BOOL openMode;
- 設定ナンバー公開フラグ
struct Cell
分析データ表示で必要になる固定幅フォント表示と複数行表示に対応しました。
- 修正メソッド
- BOOL DrawContent( HDC hdc, RECT rc );
- 固定幅フォント表示、マルチライン表示処理対応
- BOOL GetAnalText( PTCHAR text );
- 正式分析データ表示文字列取得処理実装
- BOOL GetRecordEditParam( EditParam & edit );
- コンピュータ手番処理実装など
class Grid
グリッドクラスも多少修正しました。
- 追加変数&メソッド
- BOOL loop;
- カーソルループ許可フラグ
- virtual VOID SelectByParam( ParamID param );
- パラメータ識別子によるセル選択
VOID SelectByParam()
パラメータ識別子を指定してカーソルを選択します。
VOID Grid :: SelectByParam( ParamID pid ) { int select2 = GetCellNumber( pid ); if ( select2 >= 0 ) { Select( select2 ); } }
- 修正メソッド
- virtual VOID MakeGrid();
- 固定幅フォント、マルチライン表示対応
- virtual VOID OnKeyDown( UINT vkey );
- ループ処理有無対応など
class SetGrid
設定画面グリッドクラス
- 追加メソッド
- VOID OnKeyDown( UINT vkey );
- キー入力処理をオーバーライド
VOID OnKeyDown()
基底クラスのキー入力処理を単純化して設定画面のみに必要なキー入力処理をこっちに移動しました。
VOID SetGrid :: OnKeyDown( UINT vkey ) { Cell * cells = GetCells(); Cell & cell = cells[ select ]; Cell & cellLR = cells[ selectLR ]; int select2 = select; int select3 = select; int r2; int r3; if ( ! IsCapture() && cursor ) { switch ( vkey ) { case VK_ESCAPE : if ( GetCells()[ select ].GetParamID() == PARAM_START_BUTTON ) { SelectByParam( PARAM_END_BUTTON ); } else { SelectByParam( PARAM_START_BUTTON ); } return; case VK_UP : case VK_DOWN : for ( int i = 0; i < maxCell - 1; i++ ) { if ( vkey == VK_DOWN ) { select2 = ( select2 + 1 ) % maxCell; if ( select2 < select && ! loop ) { break; } } else if ( vkey == VK_UP ) { select2 = ( select2 + maxCell - 1 ) % maxCell; if ( select2 > select && ! loop ) { break; } } Cell & cell2 = cells[ select2 ]; Cell & cell3 = cells[ select3 ]; if ( select3 != select && cell2.top != cell3.top ) { break; } else if ( CanSelectCell( cell2 ) ) { if ( abs( cell.top - cell2.top ) >= ( cell.height + cell2.height ) / 2 ) { if ( select3 == select ) { select3 = select2; } else { r2 = abs( ( cellLR.left + cellLR.width / 2 ) - ( cell2.left + cell2.width / 2 ) ); r3 = abs( ( cellLR.left + cellLR.width / 2 ) - ( cell3.left + cell3.width / 2 ) ); if ( r2 < r3 ) { select3 = select2; } } } } } if ( select3 != select ) { Select( select3 ); } return; } } Grid :: OnKeyDown( vkey ); }
ついでに以前のカーソル上下処理に多少の不具合があったので修正しました。
class RecGrid
棋譜データグリッドクラスは主に表示入力関係の修正です。
- 追加メソッド
- VOID Invalidate();
- 再描画メソッドのオーバーライド
- VOID Select( int select2 );
- セル選択メソッドのオーバーライド
- VOID SelectLastRecord();
- 最後の棋譜データセルを選択
- VOID OnKeyDown( UINT vkey );
- キー入力処理のオーバーライド
VOID Invalidate()
画面再描画メソッドです。
VOID RecGrid :: Invalidate() { HDC hdc = GetDC( hwnd ); RECT rc; Grid :: Invalidate(); GetClientRect( hwnd, & rc ); rc.top = rect.bottom + offset.y; if ( analMode ) { rc.bottom = analGrid.rect.top + analGrid.offset.y; } FillRect( hdc, & rc, hbrLightGray ); ReleaseDC( hwnd, hdc ); }
グリッドサイズが動的に変化するので、縮小した場合に未使用部分を消去するよう対応しました。
VOID Select()
分析データ表示と連動するためセル選択メソッドをオーバーライドしました。
VOID RecGrid :: Select( int select2 ) { int select0 = select; Grid :: Select( select2 ); if ( select != select0 && analMode ) { analGrid.Invalidate(); } }
選択セルが変更されたら分析データを再表示します。
VOID SelectLastRecord()
最後の棋譜データセルを選択します。
VOID RecGrid :: SelectLastRecord() { Cell * cells = GetCells(); for ( int select2 = maxCell - 1; select2 >= 0; select2-- ) { Cell & cell = cells[ select2 ]; if ( cell.cid == CELL_RECORD ) { Select( select2 ); selectLR = select2; return; } } SelectLast(); }
VOID OnKeyDown()
棋譜データの削除処理のためキー入力処理をオーバーライドしました。
VOID RecGrid :: OnKeyDown( UINT vkey ) { Cell * cells = GetCells(); Cell & cell = cells[ select ]; if ( ! IsCapture() && cursor ) { switch ( vkey ) { case VK_BACK : if ( cell.cid == CELL_CALL ) { if ( maxRecord > 0 ) { numer0n.DeleteRecord(); MakeGrid(); SelectLast(); } } else if ( cell.cid == CELL_RECORD && cell.data == maxRecord - 1 ) { numer0n.DeleteRecord(); MakeGrid(); SelectLastRecord(); } else if ( maxRecord > 0 ) { Message( _T( "削除できるのは最後の棋譜データのみです。" ) ); } break; default : Grid :: OnKeyDown( vkey ); break; } } }
バックスペースキーを押すと最後の棋譜データを削除します。