Client 同士が対戦する「三山くずしゲーム」

一組の Client が Server を介して「三山くずしゲーム」で対戦します。

前田稔(Maeda Minoru)の超初心者のプログラム入門

プロジェクトの概要

  1. 一組の Client が Server を介して「三山くずしゲーム」で対戦します。
    もちろん複数の組を同時に制御することもできるのですが、今回は「プログラムを理解できること」に主眼を置いて、 補助的な処理は省いています。
    複数の Client を制御するプログラムは 複数の Client を管理する などを参考にして下さい。
  2. 一組の Client を管理して「三山くずしゲーム」で対戦する Server Program を DOS モードで作成します。
    1. Server が立ち上がるとゲームに参加する Client からの接続待ちで待機します。
    2. 最初に接続してきた Client は「親」として参加できます。
    3. 親が参加した状態で接続要求をすると「子」として参加できます。
      親と子が揃うと子の先手でゲームを開始します。
    4. Server は子と親に対して交互にプレイを促すメッセージを送信します。
    5. 三個の山から石を交互に取り除き、最後の石を取らされた方が負けです。
    6. 再挑戦するときには、どちらかが一度 Logout して、再度 Login して下さい。
  3. Client のプログラムは Windows モードで作成します。
    1. Server が立ち上がっていることを確認して Login します。
    2. Login が成功すると「親/子」の種別と山の石が送られてきます。
    3. ゲームの進行や山の管理は、全て Server 側で行うので Client は Server の指示に従って画面を描画します。
    4. 手番になるとプレイを促すメッセージが送られて来るので、マウスのクリックで取り除く石を指示します。
    5. 最後の石を取らされた方が負けで、再挑戦するときには一度 Logout してから、再度 Login して下さい。

【課題】

  1. 勝負が決まっているのに「最後の一個」まで取らすのは失礼です。
    この段階で勝敗を判定して下さい。
  2. Logout しなくても連続してゲームができるようにして下さい。
  3. 先手と後手が交互にプレイ(または選択)できるようにして下さい。
  4. 対戦成績を記録して Client の画面に表示しましょう。
  5. 手番になったら音声で知らせて下さい。
  6. 複数の組が同時に対戦できるようにして下さい。

Server Program の説明

  1. Server のプログラムは、画像の表示にこだわらないので DOS モードで作成しました。
    Miyama Server Object Class の Header です。
    Wsock Object Class を継承します。
    T[3]; は三個の山に積まれている石の数です。
    PID[18] が親のIDを、CID[18] が子のIDを保存する領域です。
    TEBAN は親と子の手番を制御する領域で、次のようになっています。
    TEBAN 説明
    0 Client の接続待ち
    1 子の手番
    2 親の手番
    9 子の勝ち
    10 親の勝ち
    Client にメッセージを送信する SendMsg() をオーバーライドします。
    オーバーライドの説明は Server Program を Windows で作成する を参照して下さい。
    SetStr() は、送られてきた COMMAND から ID とプレイ(手)を調べる関数で、WT がその作業領域です。
    Settbl() は、T[3] に乱数で石を設定する関数です。
    Play() は、送られてきた Play COMMAND に従ってプレイする関数です。
        //★ MiyamaServer Object Class Header   前田 稔
        #include    <stdio.h>
        #include    <winsock2.h>
        #include    "Wsock.h"
    
        //★ Miyama Server Object Class
        class MiyamaSvr : public Wsock
        { protected:
            int     T[3];           //山の石
            char    PID[18];        //Parent ID
            char    CID[18];        //Child ID
            int     TEBAN;          //手番(0:待ち 1:子 2:親)
            char    msg[1024];      //送信メッセージ
    
          public:
            char    WT[6][18];      //Work Table
    
            MiyamaSvr();            //Constructor
            ~MiyamaSvr();           //Destructor
            int     SendMsg(char *com);
            int     SetStr(char buf[], char t[][18]);
            void    Settbl();
            void    Play();
        };
        
  2. Client から送られてくるメッセージ(COMMAND)は、先頭の一文字で識別しています。
    Client から送られてくる COMMAND の仕様です。
    COMMAND 引数-1 引数-2 引数-3 説明
    LOGIN ID ゲームに参加する
    END ID ゲームを終了する
    PLAY ID 山番号 石数 山番号=石数
    GET ID 山の状態を送信する
    GET COMMAND では、通常は山の状態を Client に送り返すのですが、勝敗が決まったときには勝った側のIDを送信します。
    また手番になると「プレイを促すメッセージ」と「山の状態」を乱数を使って交互に送信します。
        //★ COMMAND を解析して Send Message を作成
        int  MiyamaSvr::SendMsg(char com[])
        {   int     n;
    
            if (com[0]!='G')    printf("[%s]\n",com);
            switch(com[0])
            {   case 'L': case 'l':         //LOGIN ID
                    SetStr(com,WT);
                    if (PID[0]==0)
                    {   sprintf(msg,"%s さんは親で Login されました",WT[1]);
                        strcpy(PID,WT[1]);
                        if (CID[0]!=0)  TEBAN= 1;
                        break;
                    }
                    if (CID[0]==0)
                    {   sprintf(msg,"%s さんは子で Login されました",WT[1]);
                        strcpy(CID,WT[1]);
                        if (PID[0]!=0)  TEBAN= 1;
                        break;
                    }
                    strcpy(msg,"現在ゲーム中で Login できません");
                    break;
                case 'P': case 'p':         //PLAY ID YAMA NUM
                    SetStr(com,WT);
                    if (strcmp(CID,WT[1])==0 && TEBAN==1)  {  Play();  break;  }
                    if (strcmp(PID,WT[1])==0 && TEBAN==2)  {  Play();  break;  }
                    strcpy(msg,"もう少しお待ち下さい");
                    break;
                case 'E': case 'e':         //END ID
                    SetStr(com,WT);
                    if (strcmp(PID,WT[1])==0)
                    {   PID[0]= 0;
                        sprintf(msg,"%s さんが Logout されました",WT[1]);
                        Settbl();
                        TEBAN= 0;
                        break;
                    }
                    if (strcmp(CID,WT[1])==0)
                    {   CID[0]= 0;
                        sprintf(msg,"%s さんが Logout されました",WT[1]);
                        Settbl();
                        TEBAN= 0;
                        break;
                    }
                    sprintf(msg,"%s が見つかりません",WT[1]);
                    break;
                default:                    //GET ID
                    if (TEBAN==9)
                    {   sprintf(msg,"%s さんの勝ちです",CID);
                        break;
                    }
                    if (TEBAN==10)
                    {   sprintf(msg,"%s さんの勝ちです",PID);
                        break;
                    }
                    if (TEBAN && rand()%2==1)
                    {   if (TEBAN==1)   wsprintf(msg,"%s の手番です",CID);
                        if (TEBAN==2)   wsprintf(msg,"%s の手番です",PID);
                        break;
                    }
                    wsprintf(msg,"= %d %d %d",T[0],T[1],T[2]);
            }
            // メッセージを送信します
            n = send(sockw,msg,strlen(msg),0);
            if (n < 1)
            {   strcpy(szMSG,"send に失敗しました");
                return -1;
            }
            strcpy(szMSG,msg);
            return 0;
        }
        
  3. Miyama Server の Main Program です。
    Main Program は Miyama Server Object Class を使うと簡単です。
        /****************************************/
        /*★ Miyama Server Program    前田 稔 ★*/
        /****************************************/
        #include    <stdio.h>
        #include    <conio.h>
        #include    <winsock2.h>
        #include    "MiyamaSvr.h"
        #pragma     once
        #pragma     comment(lib,"ws2_32.lib")
    
        MiyamaSvr   *App= NULL;     //Jyanken Object Class
        int         PORT= 12345;
        char        buf[1024];
    
        int main()
        {   int     rc;
    
            App= new MiyamaSvr();
            // winsock2の初期化
            rc= App->Startup();
            puts(App->szMSG);
            if (rc==-1) {  getch(); return -1;  }
            // ソケットの作成
            rc= App->StartSrv(PORT);
            puts(App->szMSG);
            if (rc==-1) {  getch(); return -1;  }
            puts("セッションを開始しました");
            while(1)
            {   rc= App->CheckSrv(buf,1024);
                switch(rc)
                {   case 0:
                        //puts(App->szMSG);
                        break;
                    case 1:
                        Sleep(300);
                        continue;
                    case -1:
                        puts("CheckSrv に失敗しました");
                        break;
                }
            }
            SAFE_DELETE(App);
            return 0;
        }
        

Client Program の説明

  1. Client のプログラムは画像を使うので Windows モードで作成します。
    Miyama Client Object Class の Header です。
    Wsock Object Class を継承します。
    Client のプログラムは、基本的に Server とのメッセージの送受信と画像を表示するだけです。
        //★ Miyama Client Object Header File   Ver 1.0  前田 稔
        #ifndef     _MiyamaCli
        #define     _MiyamaCli
        #include    <commdlg.h>
        #include    "Wsock.h"
    
        #define  SAFE_DELETE(p)  { if (p) { delete (p);     (p)=NULL; } }
        #define  SAFE_RELEASE(p) { if (p) { (p)->Release(); (p)=NULL; } }
        #define  EMSG(x)     MessageBox(NULL,x,"Windows App",MB_OK);
    
        // Sprite Object Header File  2004-01-05
        class MiyamaCli : public Wsock
        { protected:
            HWND        hWnd;
            HDC         hMdc;
            WORD        WNum;               //横方向の SPrite 個数
            WORD        HNum;               //縦方向の SPrite 個数
            WORD        SWidth;             //Sprite の幅
            WORD        SHeight;            //Sprite の高さ
            WORD        xp,yp;
    
          public:
            MiyamaCli(HWND hWnd);           //Constructor
            ~MiyamaCli();                   //Destructor
            WORD        Width;              //画像の幅
            WORD        Height;             //画像の高さ
            char        szFile[MAX_PATH];   //オープンするファイル名(パス付き)
    
            HRESULT     Load(LPSTR szBitmap, WORD WN, WORD HN);
            HRESULT     Show(WORD n,WORD x,WORD y,WORD w,WORD h,WORD xoff,WORD yoff,DWORD rop=SRCCOPY);
            HRESULT     Show(WORD n,WORD x,WORD y,DWORD rop=SRCCOPY)
            {  return Show(n,x,y,SWidth,SHeight,0,0,rop);  }
            HFONT       SetMyFont(LPCTSTR face,int h,int angle);
            int         SetStr(char buf[], char t[][18]);
        };
    
        #endif
        
  2. Miyama Client の WinMain Program です。
    シンプルなプログラムを目指して作成したのですが、ウインドウの描画や Button 操作のために多少ソースコードが 長くなりましたが、基本的には Server に接続してメッセージを送受信して表示するだけです。
    ウインドウの表示は マウスのクリックで石を取り除く や他のページを参照して下さい。
        /********************************************************/
        /*★ Client 同士が対戦する三山くずしゲーム    前田 稔 ★*/
        /********************************************************/
        #define     NAME    "Miyama"
        #include    <windows.h>
        #include    "MiyamaCli.h"
        #pragma     once
        #pragma     comment(lib,"ws2_32.lib")
        #pragma     comment(lib,"MiyamaCli.lib")
        #define     ID_TIMER    32767
    
        #define     IDM_LOGIN   1001
        #define     IDM_END     1002
    
        MiyamaCli   *App= NULL;
        HINSTANCE   g_hInst;
        HWND        g_hID,g_hURL,g_hPORT,g_hMSG;    //Edit Handle
        HWND        g_hLogin,g_hEnd;
        char        URL[MAX_PATH]= "127.0.0.1";     //送信先 URL
        int         PORT= 12345;                    //送信先 PORT
        char        ID[18];                         //Player ID
        int         T[3]= { 15, 10, 12 };           //三個の山
        char        buf[1024],old[80];
        char        WK[6][18];
        
  3. Server に送信する COMMAND を編集する Play() 関数です。
    xp, yp はクリックされたマウスの座標です。
    *com と ID と山と石を組み合わせて buf に COMMAND を編集して送信します。
    Server では COMMAND を解析して、対応するメッセージを buf に送り返してきます。
    buf の先頭が '=' のときは、山の情報です。
        // Server に COMMAND を送る
        int  Play(char *cmd, int xp, int yp)
        {   int     rc,yama,num;
            char    wk[40];
    
            strcpy(buf,cmd);
            GetWindowText(g_hID,ID,17);
            strcat(buf,ID);
            if (xp>0)
            {   yama= (yp-20)/60;
                num= (xp-20)/50;
                wsprintf(wk," %d %d",yama,num);
                strcat(buf,wk);
            }
            rc= App->Connect(URL,PORT,buf,buf,1024);
            if (buf[0]!='=')	SetWindowText(g_hMSG,buf);
            return rc;
        }
        
  4. CALLBACK 関数です。
    ゲームのプレイは、マウスでクリックした石から右側の石が取り除かれます。
    Server から送られてきた山の情報を T[3] に保存しておいて描画します。
        //★ イベントで起動するウインドウプロシージャ
        LRESULT  CALLBACK  WndProc(HWND hWnd,UINT msg,WPARAM wParam,LPARAM lParam)
        {   PAINTSTRUCT ps;
            HDC         hdc;
            POINT       pt;
            int         rc,n,x,y;
            char        wk[24];
    
            switch(msg)
            {   case WM_CREATE:
                    Init(hWnd);
                    break;
                case WM_LBUTTONDOWN:
                    KillTimer(hWnd, ID_TIMER);
                    pt.x= LOWORD(lParam);
                    pt.y= HIWORD(lParam);
                    Play("PLAY ",pt.x,pt.y);
                    return TRUE;
                case WM_PAINT:
                    hdc= BeginPaint(hWnd, &ps);
                    for(y=0; y<3; y++)
                        for(x=0; x<T[y]; x++)
                            App->Show(0,x*50+20,y*60+20);
                    EndPaint(hWnd, &ps);
                    break;
        
  5. 接続ボタンがクリックされると IDM_LOGIN が呼ばれます。
    終了ボタンがクリックされると IDM_END が呼ばれます。
                case WM_COMMAND:
                    KillTimer(hWnd, ID_TIMER);
                    switch(LOWORD(wParam))
                    {   case IDM_LOGIN:
                            if (App->Startup()==-1)
                            {   SetWindowText(g_hMSG,App->szMSG);
                                break;
                            }
                            strcpy(buf,"LOGIN ");
                            GetWindowText(g_hID,ID,16);
                            strcat(buf,ID);
                            GetWindowText(g_hURL,URL,MAX_PATH);
                            GetWindowText(g_hPORT,wk,23);
                            PORT= atoi(wk);
                            App->Connect(URL,PORT,buf,buf,64);
                            SetWindowText(g_hMSG,buf);
                            break;
                        case IDM_END:
                            SendMessage(hWnd,WM_CLOSE,0,0L);
                            break;
                    }
                    SetTimer(hWnd, ID_TIMER, 500, NULL);
                    break;
        
  6. WM_TIMER: で定期的に GET COMMAND をサーバーに送信して情報を受け取ります。
    GET COMMAND で送られてくる山の情報です。
    先頭が '=' 以外は Client に対するメッセージです。
    山の情報は前回の情報を old に保存しておいて、状態が変化したときに InvalidateRect() で描画します。
    【例】= 2 5 4説明
    = 識別情報として "=" が格納されています
    2 1番の山の個数
    5 2番の山の個数
    4 3番の山の個数
                case WM_TIMER:
                    KillTimer(hWnd, ID_TIMER);
                    rc= Play("GET ",0,0);
                    if (rc==0 && buf[0]=='=' && strcmp(buf,old)!=0)
                    {   strcpy(old,buf);
                        SetWindowText(g_hMSG,buf);
                        App->SetStr(buf,WK);
                        for(n=0; n<3; n++)  T[n]= atoi(WK[n+1]);
                        InvalidateRect(hWnd,NULL,TRUE);
                    }
                    SetTimer(hWnd, ID_TIMER, 500, NULL);
                    break;
        
  7. C++ Windows の三山くずし ゲームです。

[Next Chapter ↓] ネット対戦ゲームを作成する

超初心者のプログラム入門(Win32API C++)