グループで 「じゃんけん大会」

最大16名が参加して、一斉に「じゃんけん大会」をします。
参加者全員の同期を取らなければならず、Server 側のプログラムは最もやっかいかも知れません。

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

ゲームの進行

ゲームの進行状態を管理するために、次の Stage を設定します。
  1. Login Stage(STG=0)
    Login を待っている状態で、最大16人の Client が Login してゲームに参加できます。
    参加者が揃えば Start ボタンで STG=1 へ移行します。
  2. Start Stage(STG=1, 4, 7, 10, ...)
    STG>=3 から移行してきたときは、ゲームの敗者/勝者を確定します。
    勝ち残っている Client を確定して次のゲームの準備をします。
    準備が終われば自動的に STG=2 に移行します。
    当初はボタン操作を予定していたので Stage を分けています。
  3. Play Stage(STG=2, 5, 8, 11, ...)
    Server は、勝ち残っている Client に対して「グー/チョキ/パー」を要求します。
    全員の手が揃えば Disp ボタンで STG=3 へ移行します。
  4. Display Stage(STG=3, 6, 9, 12, ...)
    参加者全員の手(グー/チョキ/パー)を公開して Client の確認を取ります。
    全員の確認が終われば Start ボタンで STG=4(二回戦) へ移行します。
    三回戦は STG=7, 四回戦は STG=10 ... と進んで行きます。
  5. GameOver Stage(STG=99)
    優勝者が決まってゲームが終了した状態です。
    Reset ボタンで次のゲーム(STG=0) へ移行します。
Server 側のボタン操作です。
  1. START Button
    セッションを開設して Client の参加を待ち、人数が揃ったときにクリックします。
    または、ゲーム参加者の手を公開して、全員の確認が終わったときにクリックします。
    一回戦(STG=1)では参加者全員の flg を ON に、画像を1に設定してゲーム開始です。
    二回戦(STG>=4)以降では、敗者の flg を OFF に、画像を0にします。
    勝者は flg を ON のままで、画像を1に設定します。
  2. DISP Button
    ゲームに参加している Client の手が揃えば Disp ボタンをクリックして全参加者の手を公開します。
    Client は「グー/チョキ/パー」の結果を確認して確認ボタンをクリックします。
  3. RESET Button
    強制的に Login 状態に切り替えます。
  4. END Button
    Srever のプログラムを終了します。
Client 側のボタン操作です。
  1. LOGIN Button
    ID と URL を設定してゲームに Login します。
  2. グー Button
    「グー」の手を送信します。
  3. チョキ Button
    「チョキ」の手を送信します。
  4. パー Button
    「パー」の手を送信します。
  5. 確認 Button
    公開された全員の手の確認が終わったことを通知します。
  6. END Button
    Client のプログラムを終了します。
Server が管理する各 Client の情報です。
  1. char id[18];
    Client を管理するIDを格納します。
    "?" のときは Login されていません。
  2. short gra;
    表示する画像の番号(0 ~ 5)を格納します。

    1. 0番
      空き状態を示す模様の無い画像です。
    2. 1番
      Login を示す「にこマーク」の画像
    3. 2番
      出す手を決めたときの、?付きの「にこマーク」の画像
    4. 3番
      「グー」の画像
    5. 4番
      「チョキ」の画像
    6. 5番
      「パー」の画像
  3. short flg;
    1: 勝ち残ってゲームに参加している Player
    0: 負けてゲームに参加していない Player
  4. short ans;
    Client からの手の送信と勝負の結果の応答確認を制御します。
    2: 応答を確認する必要がある Client
    1: 応答要求のメッセージを送信
    0: 通常のモード
  5. short ply;
    Client から送信された手を格納します。
    3: グー
    4: チョキ
    5: パー

GServer Object Class の説明

  1. Wsock Object Class を継承して GServer Object Class を作成します。
    GServer Object Class は Srever Program と Client Program で共通に使用しています。
    GTBL は Client 毎の情報を管理する領域です。
    画像を切り出すために Sprite を使っています。
    Sprite Class の詳細は Sprite Class で美人のアニメーション を参照して下さい。
    SetStr() は、送られてきた COMMAND から ID とプレイ(手)を調べる関数で、stw がその作業領域です。
    Client にメッセージを送信する SendMsg() をオーバーライドします。
    オーバーライドの説明は Server Program を Windows で作成する を参照して下さい。
        //★ Group Jyanken Class Header File  2005/08/01  Ver 1.0  前田 稔
        #ifndef     _GServer
        #define     _GServer
        #include    "Wsock.h"
        #define     GTSIZ       18
    
        #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);
    
        //☆BLOCK 構造体
        typedef struct _GTBL
        {   char    id[18];     //ID
            short   gra;        //表示する画像の番号
            short   flg;        //1:勝者、0:敗者
            short   ans;        //勝敗の確認
            short   ply;        //グー/チョキ/パー
        }   GTBL;
    
        class GServer : public Wsock
        { protected:
            HWND        hWnd;
            HDC         hMdc;
            WORD        WNum;               //横方向の SPrite 個数
            WORD        HNum;               //縦方向の SPrite 個数
            WORD        SWidth;             //Sprite の幅
            WORD        SHeight;            //Sprite の高さ
            WORD        xp,yp;
            char        ID[18];             //ID Work(MAX 16 Byte)
            int         POS;                //GT[] Position
            char        msg[1024];          //送信メッセージ
    
            int         SetStr(char buf[])  {  return SetStr(buf,stw);  }
    
          public:
            GServer(HWND hWnd);             //Constructor
            ~GServer();                     //Destructor
            WORD        Width;              //画像の幅
            WORD        Height;             //画像の高さ
            char        szFile[MAX_PATH];   //オープンするファイル名(パス付き)
            GTBL        GT[GTSIZ];          //各 Client の情報
            int         CNT;                //参加人数
            int         ANS;                //応答カウント
            int         WIN;                //勝者の番号
            char        stw[6][18];         //String Work Table
            int         STG;                //Game Stage
    
            int         SetStr(char buf[], char t[][18]);
            int         SendMsg(char *com); //送信メッセージ
            int         Search(char *id);   //ID の Client を検索
            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);  }
        };
    
        #endif
        
  2. Client からのメッセージを解析して送信する SendMsg() 関数です。
    Client から送られてくるメッセージ(COMMAND)は、先頭の一文字で識別しています。
    Client から送られてくる COMMAND の仕様です。
    COMMAND 引数-1 引数-2 説明
    LOGIN ID ゲームに参加する
    PLAY ID じゃんけんする
    OK ID 確認する
    END ID ゲームを終了する
    GET ID 現在の状態を調べる
    GET COMMAND では、通常は全員の状態を Client に送り返すのですが ans と STG の設定によってはメッセージを送信します。
    ans, STG 説明
    ans==2 現在の状態を送信した後で、メッセージを送信したいとき
    ans==1 ans==2 から移行してきて、メッセージ(プレイの促進、プレイの確認)を送信する
    STG==99 ゲームの終了です。RESET Button で次のゲームを開始
        //★ COMMAND を解析して Send Message を作成
        int  GServer::SendMsg(char *com)
        {   int     n,i;
            char    wk[8];
    
            switch(com[0])
            {   case 'L': case 'l': //LOGIN ID
                    if (STG!=0)
                    {   wsprintf(msg,"Stage 0 で Login して下さい。 Stage=%d",STG);
                        break;
                    }
                    SetStr(com);
                    if (POS=GTSIZ)
                    {   strcpy(msg,"現在空きがありません");
                        break;
                    }
                    ZeroMemory((void *)GT[POS].id,sizeof(_GTBL));
                    strcpy(GT[POS].id,ID);
                    strcpy(msg,"Login OK");
                    GT[POS].gra= 1;
                    CNT++;
                    break;
                case 'P': case 'p': //PLAY ID 0|1|2
                    if ((STG-1)%3!=1)
                    {   wsprintf(msg,"Stage が違います。 Stage=%d",STG);
                        break;
                    }
                    SetStr(com);
                    if (POS>=GTSIZ)
                    {   wsprintf(msg,"%s は Login されていません",ID);
                        break;
                    }
                    if (GT[POS].flg==0)
                    {   wsprintf(msg,"%s 参加できません",ID);
                        break;
                    }
                    if (GT[POS].gra==1)
                    {   ANS++;
                        GT[POS].gra= 2;
                        GT[POS].ans= 0;
                    }
                    GT[POS].ply= atoi(stw[2])+3;
                    break;
                case 'O': case 'o': //OK ID
                    if ((STG-1)%3!=2)
                    {   wsprintf(msg,"Stage が違います。 Stage=%d",STG);
                        break;
                    }
                    SetStr(com);
                    if (POS>=GTSIZ)
                    {   wsprintf(msg,"%s は Login されていません",ID);
                        break;
                    }
                    if (GT[POS].ans==1)
                    {   GT[POS].ans= 0;
                        ANS++;
                    }
                    break;
                case 'E': case 'e': //END ID
                    SetStr(com);
                    if (POS>=GTSIZ)
                    {   wsprintf(msg,"%s は Login されていません",ID);
                        break;
                    }
                    ZeroMemory((void *)GT[POS].id,sizeof(_GTBL));
                    GT[POS].id[0]= '?';
                    strcpy(msg,"BYE");
                    CNT--;
                    break;
                default:            //GET ID
                    SetStr(com);
                    if (POS>=GTSIZ)
                    {   wsprintf(msg,"%s は Login されていません",ID);
                        break;
                    }
                    if (GT[POS].ans==2)
                    {   //ID と画像番号を送信
                        wsprintf(msg,"= %d ",CNT);
                        for(i=0; i<GTSIZ; i++)
                        {   strcat(msg,GT[i].id);
                            wsprintf(wk," %d ",GT[i].gra);
                            strcat(msg,wk);
                        }
                        GT[POS].ans= 1;
                        break;
                    }
                    if (STG==99)
                    {   wsprintf(msg,"優勝者は %s で GAME OVER です",GT[WIN].id);
                        break;
                    }
                    //Play Stage(2,5,8...) Client は手を送信
                    if ((STG-1)%3==1 && GT[POS].flg==1 && GT[POS].ans==1)
                    {   strcpy(msg,"「グー/チョキ/パー」を送信して下さい");
                        break;
                    }
                    //Display Stage(3,6,9...) Client は全員の手を確認して OK を送信
                    if ((STG-1)%3==2 && GT[POS].flg==1 && GT[POS].ans==1)
                    {   strcpy(msg,"結果を確認してボタンを押して下さい");
                        break;
                    }
                    //ID と画像番号を送信
                    wsprintf(msg,"= %d ",CNT);
                    for(i=0; i<GTSIZ; i++)
                    {   strcat(msg,GT[i].id);
                        wsprintf(wk," %d ",GT[i].gra);
                        strcat(msg,wk);
                    }
                    break;
            }
            // メッセージを送信します
            n = send(sockw,msg,strlen(msg),0);
            if (n < 1)
            {   strcpy(szMSG,"send に失敗しました");
                return -1;
            }
            strcpy(szMSG,"Client に送信しました");
            return 0;
        }
        

Server Main Program の説明

  1. Server のプログラムは GServer Object Class を使って Windows モードで作成しています。
    Edit Box と Button のハンドルとIDを定義します。
    Wsock Object Class を継承します。
    App は GServer Object Class の定義です。
        #define     NAME    "Group Jyanken"
        #include    <windows.h>
        #include    "GServer.h"
        #pragma     once
        #pragma     comment(lib,"ws2_32.lib")
        #pragma     comment(lib,"GServer.lib")
        #define     ID_TIMER    32767
    
        #define     IDM_START   1001
        #define     IDM_DISP    1002
        #define     IDM_RESET   1004
        #define     IDM_END     1005
    
        GServer     *App= NULL;
        HINSTANCE   g_hInst;
        HWND        g_hSTS,g_hMSG;      // Edit Handle
        HWND        g_hStart,g_hDisp,g_hReset,g_hEnd;
        int         PORT= 12345;
        char        buf[1024];
        
  2. CALLBACK 関数です。
    Init() 関数でアプリケーションの初期化を行います。
    WM_PAINT: ではゲームに参加する16人分の画像を表示します。
    表示の形式はページ先頭の画像を参照して下さい。
        LRESULT  CALLBACK  WndProc(HWND hWnd,UINT msg,WPARAM wParam,LPARAM lParam)
        {
                       :
            switch(msg)
            {   case WM_CREATE:
                    Init(hWnd);
                    break;
                case WM_PAINT:
                    hdc= BeginPaint(hWnd, &ps);
                    wsprintf(wk,"STG=%d  CNT=%d  ANS=%d",App->STG,App->CNT,App->ANS);
                    SetWindowText(g_hSTS,wk);
                    for(n=0; n<18; n++)
                    {   x= (n%6)*128+10;
                        y= (n/6)*90+20;
                        App->Show(App->GT[n].gra,x,y);
                        TextOut(hdc,x,y+60,App->GT[n].id,strlen(App->GT[n].id));
                    }
                    EndPaint(hWnd, &ps);
                    break;
        
  3. Server 側のボタン操作を検出して STAGE を設定します。
    ボタン操作で設定する情報は、ボタンの説明と Client 情報を参照して下さい。
                case WM_COMMAND:
                    switch(LOWORD(wParam))
                    {   case IDM_RESET:         //Reset して LOGIN Mode
                            for(n=0; n<18; n++)
                            {   App->GT[n].gra= 0;
                                App->GT[n].flg= 0;
                                App->GT[n].ans= 0;
                                App->GT[n].ply= 0;
                                if (App->GT[n].id[0]!='?')  App->GT[n].gra= 1;
                            }
                            App->STG= 0;
                            break;
                        case IDM_START:         //勝敗を判定して START Mode
                            if (App->STG==99)   break;
                            if (App->STG!=0 && (App->STG-1)%3!=2)   break;
                            if (App->STG==0)    //一回戦
                            {   for(n=0; n<18; n++)
                                    if (App->GT[n].id[0]!='?')  App->GT[n].flg= 1;
                            }
                            else                //勝敗の判定
                            {   gu=ti=pa= 0;
                                for(n=0; n<18; n++)
                                {   if (App->GT[n].ply==3)          gu++;
                                    else    if (App->GT[n].ply==4)  ti++;
                                    else    if (App->GT[n].ply==5)  pa++;
                                }
                                if (gu==0 || ti==0 || pa==0)
                                {   if (gu==0)          rc= 5;
                                    else    if (ti==0)  rc= 3;
                                    else    if (pa==0)  rc= 4;
                                    for(n=0; n<18; n++)
                                        if (App->GT[n].ply==rc) App->GT[n].flg= 0;
                                }
                            }
                            App->STG= (App->STG/3)*3+1; //STG=1, 4, 7, 10, ...
                            //Client Table の設定
                            App->ANS= 0;
                            for(App->CNT=n=0; n<18; n++)
                                if (App->GT[n].flg==1)
                                {   App->CNT++;
                                    App->WIN= n;
                                }
                            if (App->CNT==1)            //Game Over
                            {   App->STG= 99;
                                for(n=0; n<18; n++) App->GT[n].ans= 2;
                                break;
                            }
                            for(n=0; n<18; n++)         //Next Play
                            {   if (App->GT[n].flg==0)  App->GT[n].gra= 0;
                                else                    App->GT[n].gra= 1;
                                App->GT[n].ans= 2;
                                App->GT[n].ply= 0;
                            }
                            App->STG= (App->STG/3)*3+2; //STG=2, 5, 8, 11, ...
                            break;
                        case IDM_DISP:          //全員の手を送信して確認
                            if (App->STG==99)   break;
                            if ((App->STG-1)%3!=1)  break;
                            for(n=0; n<18; n++)
                            {   if (App->GT[n].flg==1)
                                {   App->GT[n].gra= App->GT[n].ply;
                                    App->GT[n].ans= 2;
                                }
                            }
                            App->STG= (App->STG/3)*3+3; //STG=3, 6, 9, 12, ...
                            App->ANS= 0;
                            break;
                        case IDM_END:
                            SendMessage(hWnd,WM_CLOSE,0,0L);
                            break;
                    }
                    break;
        
  4. Timer 割り込みで Client からの COMMAND を管理します。
    App->CheckSrv() で受信したメッセージ(COMMAND)は SendMsg() で解析されます。
                case WM_TIMER:
                    KillTimer(hWnd, ID_TIMER);
                    rc= App->CheckSrv(buf,1024);
                    if (rc==0)
                    {   if (buf[0]=='\0')   strcat(buf,"NULL");
                        SetWindowText(g_hMSG,buf);
                        InvalidateRect(hWnd,NULL,FALSE);
                    }
                    SetTimer(hWnd, ID_TIMER, 300, NULL);
                    break;
        

Client Main Program の説明

  1. Client のプログラムは Server と同じ GServer Object Class を使って Windows モードで作成しています。
    Edit Box と Button のハンドルとIDを定義します。
    App は GServer Object Class の定義です。
        #include    <windows.h>
        #include    "GServer.h"
        #pragma     once
        #pragma     comment(lib,"ws2_32.lib")
        #pragma     comment(lib,"GServer.lib")
        #define     ID_TIMER    32767
    
        #define     IDM_LOGIN   1001
        #define     IDM_GU      1002
        #define     IDM_TYOKI   1003
        #define     IDM_PA      1004
        #define     IDM_OK      1005
        #define     IDM_END     1006
    
        GServer     *App= NULL;
        HINSTANCE   g_hInst;
        HWND        g_hMSG,g_hID,g_hURL;            //EditBox Handle
        HWND        g_hLogin,g_hGu,g_hTyoki,g_hPa,g_hOk,g_hEnd;
        char        URL[MAX_PATH]= "127.0.0.1";     //送信先 URL
        int         PORT= 12345;                    //PORT は 12345 に固定
        char        ID[18];
        char        buf[1024],old[1024];
        char        wk[40][18];
        
  2. CALLBACK 関数です。
    Client のプログラムは Server が全て処理してくれるので、Server からのメッセージを受信して表示するだけです。
    WM_PAINT: では App->GT[] に保存されている全クライアントの状態を表示します。
    表示の形式はページ先頭の Server 画面に比べて、ボタンの設定が異なるだけです。
        LRESULT  CALLBACK  WndProc(HWND hWnd,UINT msg,WPARAM wParam,LPARAM lParam)
        {
                :
            switch(msg)
            {   case WM_CREATE:
                    Init(hWnd);
                    break;
                case WM_PAINT:
                    hdc= BeginPaint(hWnd, &ps);
                    for(n=0; n<18; n++)
                    {   x= (n%6)*128+10;
                        y= (n/6)*90+20;
                        App->Show(App->GT[n].gra,x,y);
                        TextOut(hdc,x,y+60,App->GT[n].id,strlen(App->GT[n].id));
                    }
                    EndPaint(hWnd, &ps);
                    break;
        
  3. ボタンがクリックされたときの処理です。
    ボタン操作で処理する内容は、Client 側のボタン操作を参照して下さい。
                case WM_COMMAND:
                    switch(LOWORD(wParam))
                    {
                        case IDM_LOGIN:
                            GetWindowText(g_hURL,URL,MAX_PATH);
                            Play("LOGIN ",NULL);
                            break;
                        case IDM_GU:
                            Play("PLAY "," 0 ");
                            break;
                        case IDM_TYOKI:
                            Play("PLAY "," 1 ");
                            break;
                        case IDM_PA:
                            Play("PLAY "," 2 ");
                            break;
                        case IDM_OK:
                            Play("OK ",NULL);
                            break;
                        case IDM_END:
                            Play("END ",NULL);
                            SendMessage(hWnd,WM_CLOSE,0,0L);
                            break;
                    }
                    break;
        
  4. Timer 割り込みで定期的に Server にアクセスして現在の状態を取得するために Play() 関数を呼び出します。
    buf の先頭が '=' のときは参加者全員の最新の情報です。
    前回の情報を old に保存しておいて、更新されていればウインドウを再描画します。
    buf に格納されている参加者全員の情報は次のようになっています。
    = 識別情報
    CNT 参加者の人数
    ID-1 参加者のID-1
    gra-1 画像の番号-1
    ID-2 参加者のID-2
    gra-2 画像の番号-2
    ID-n gra-n以下 ID と gra が18回繰り返されます
                case WM_TIMER:
                    KillTimer(hWnd, ID_TIMER);
                    rc= Play("GET ",NULL);
                    if (rc==0 && buf[0]=='=' && strcmp(buf,old)!=0)
                    {   strcpy(old,buf);
                        SetWindowText(g_hMSG,buf);
                        App->SetStr(buf,wk);
                        for(n=0; n<18; n++)
                        {   strcpy(App->GT[n].id,wk[n*2+2]);
                            App->GT[n].gra= atoi(wk[n*2+3]);
                        }
                        InvalidateRect(hWnd,NULL,FALSE);
                    }
                    SetTimer(hWnd, ID_TIMER, 300, NULL);
                    break;
        
  5. Server に COMMAND を送る関数です。
    Server から送られてきたメッセージは buf[] に格納されます。
    buf[] の先頭が '=' のときは全 Client の現在の情報です。
        // Server に COMMAND を送る
        int  Play(char *cmd, char *ply)
        {   int     rc;
            strcpy(buf,cmd);
            GetWindowText(g_hID,ID,17);
            strcat(buf,ID);
            if (ply!=NULL)  strcat(buf,ply);
            rc= App->Connect(URL,PORT,buf,buf,1024);
            if (buf[0]!='=' && buf[0]!=0)   SetWindowText(g_hMSG,buf);
            return rc;
        }
        

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