複数の Client を管理する

複数の Client が同時に接続して Server を相手に「じゃんけんゲーム」をします。
Server 側で複数の Client を管理する基本的なゲームプログラムです。

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

プロジェクトの概要

  1. 複数の Client を相手にして「じゃんけんゲーム」をする Game Program を作成します。
    今回の「じゃんけんゲーム」は勝敗の率では無く、得点を競います。
    「グー、チョキ、パー」の組み合わせにより得点が異なります。
    得点表は次のとおりです。
    勝ち 負け あいこ
    グー(0) 5点 -5点 3点
    チョキ(1) 2点 0点 1点
    パー(2) 15点 0点 0点
  2. Srever は Client (現在は5名以下)ごとに次の情報を記録します。
    TSIZE 5 の値を変えれば Client 数は自由に設定できます。
        #define     TSIZE   5
    
        //☆BLOCK 構造体
        typedef struct _TABLE
        {   char    id[24];     //ID (id[0]==0 のときは空き)
            short   cli;        //直前の Client の手
            short   svr;        //直前の Srever の手
            short   cliv;       //Client のポイント
            short   svrv;       //Srever のポイント
            short   ply;        //打つ手を解析する情報
            short   val;        //打つ手を解析する情報
        }   TABLE;
    
        TABLE   TBL[TSIZE];     //各 Client の情報
        
  3. 同時に複数の Client を相手にして「じゃんけんゲーム」をする Server Program を DOS モードで作成します。
    1. 最初に Server を起動してセッションを開設して下さい。
      Server は「ページ先頭左側の画像のように」DOS モードで起動します。
    2. Server が立ち上がるとゲームに参加する Client からの接続待ちで待機します。
    3. Client が Login してくると n 番目の TBL[n] を初期化してゲームを開始します。
      n は Login した Client の番号です。
    4. Client からの COMMAND を受けて「じゃんけんゲーム」を進めます。
    5. 勝敗の判定や得点の計算は Server 側で管理します。
  4. Server を相手にして「じゃんけんゲーム」する Client Program を Windows モードで作成します。
    グー/チョキ/パーのボタンをクリックして Server に送るだけで、勝敗や得点は Server が計算してくれます。
    1. Server が立ち上がっていることを確認して Login します。
      ID にプレイヤの識別名を指定して接続ボタンで Server に接続します。
    2. 送受信メッセージが Server のウインドウに表示されるので確認して下さい。
      ページ先頭の画像では PlayerA と PlayerB がゲームに参加しています。
    3. Login が成功すると Login OK のメッセージが送られてきます。
    4. グー/チョキ/パーのいずれかのボタンをクリックすると、ID と手が Server に送られます。
    5. Server では「双方の手と得点」を計算して Client に送り返します。
    6. Client では Server から送り返された情報を表示するだけです。

Server Program の説明

  1. Server のプログラムは、画像の表示にこだわらないので DOS モードで作成しました。
    Jyanken Server Object Class の Header です。
    Wsock Object Class を継承します。
    TABLE TBL[TSIZE]; は、各 Client ごとの情報を保存するテーブルです。
    WID[24] に接続してきた Client の ID を保存します。
    short POS; が ID の Client 情報が保存されている TBL[] の番号です。
    Client にメッセージを送信する SendMsg() をオーバーライドします。
    オーバーライドの説明は Server Program を Windows で作成する を参照して下さい。
    SelID() 関数は、送られてきた COMMAND から ID とプレイ(手)を調べる関数です。
    Search() は ID が Login している TBL[] を調べて、その番号を POS に格納する関数です。
        //★ MultiClient  Jyanken Server  Header   前田 稔
        #include    <stdio.h>
        #include    <winsock2.h>
        #include    "Wsock.h"
        #define     TSIZE   5
    
        //☆BLOCK 構造体
        typedef struct _TABLE
        {   char    id[24];
                :
                :
        }   TABLE;
    
        //★ Jyanken Server Object Class
        class JyanSvr : public Wsock
        { protected:
            TABLE   TBL[TSIZE];     //各 Client の情報
            char    WID[24];
            char    PLY[8];
            short   POS;            //Client の TBL 番号
            char    msg[80];
    
            void    SelID(char str[], char id[], char ply[]);
    
          public:
            JyanSvr();                      //Constructor
            ~JyanSvr();                     //Destructor
            int     SendMsg(char *com);
            int     Search(char *id);
        };
        
  2. Client から送られてくるメッセージ(COMMAND)は、先頭の一文字で識別しています。
    Server 側は原則として、理論的な確率を元に乱数で「グー、チョキ、パー」を決めています。
    同じ手が続くと「その手に勝つ手を出す」のですが、その時「ちょっとズル」をしているのですがわかりますか?(^_^;
    Client から送られてくる COMMAND の仕様です。
    COMMAND 引数-1 引数-2 説明
    LOGIN ID ゲームに参加する
    END ID ゲームを終了する
    PLAY ID じゃんけんする
    GET ID 双方の手と得点を調べる
        //★ MultiClient  Jyanken Server  Object Class   前田 稔
        #include    "JyanSvr.h"
        short   VT[3][3] = { { 3, 5, -5 }, { 0, 1, 2 }, { 15, 0, 0 } };
                     :
        //★ COMMAND を解析して Send Message を作成
        int  JyanSvr::SendMsg(char com[])
        {   int     n,ans;
    
            printf("[%s]\n",com);
            switch(com[0])
            {   case 'L': case 'l':         //LOGIN ID
                    SelID(com,WID,PLY);
                    POS= Search(WID);
                    if (POS<TSIZE)
                    {   sprintf(msg,"%s は既に Login しています",WID);
                        break;
                    }
                    for(POS=0; POS<TSIZE && TBL[POS].id[0]!='\0'; POS++);
                    if (POS>=TSIZE)
                    {   strcpy(msg,"現在空きがありません");
                        break;
                    }
                    ZeroMemory((void *)TBL[POS].id,sizeof(_TABLE));
                    strcpy(TBL[POS].id,WID);
                    strcpy(msg,"Login OK");
                    break;
                case 'P': case 'p':         //PLAY ID PLY
                    SelID(com,WID,PLY);
                    POS= Search(WID);
                    if (POS>=TSIZE)
                    {   sprintf(msg,"%s が見つかりません",WID);
                        break;
                    }
                    //理論的な確率は(5,33,21)
                    n= rand()%59;
                    if (n<5)            ans= 0;
                    else  if (n<38)     ans= 1;
                    else                ans= 2;
                    //7手を超えて同じ手が連続するときは、その手に勝つ手
                    TBL[POS].cli= atoi(PLY);
                    if (TBL[POS].ply != TBL[POS].cli)
                    {   TBL[POS].ply= TBL[POS].cli;
                        TBL[POS].val= 0;
                    }
                    TBL[POS].val++;
                    if (TBL[POS].val>7) ans= (TBL[POS].ply+2)%3;
                    wsprintf(msg,"Client=%s Server=%d",PLY,ans);
                    TBL[POS].svr= ans;
                    TBL[POS].cliv+= VT[TBL[POS].cli][TBL[POS].svr];
                    TBL[POS].svrv+= VT[TBL[POS].svr][TBL[POS].cli];
                    break;
                case 'G': case 'g':         //GET ID
                    SelID(com,WID,PLY);
                    POS= Search(WID);
                    if (POS>=TSIZE)
                    {   sprintf(msg,"%s が見つかりません",WID);
                        break;
                    }
                    wsprintf(msg,"= %d %d %d %d",TBL[POS].cli,TBL[POS].svr,TBL[POS].cliv,TBL[POS].svrv);
                    break;
                case 'E': case 'e':         //END ID
                    SelID(com,WID,PLY);
                    POS= Search(WID);
                    if (POS>=TSIZE)
                    {   sprintf(msg,"%s が見つかりません",WID);
                        break;
                    }
                    TBL[POS].id[0]= '\0';
                    strcpy(msg,"BYE");
                    break;
                default:
                    strcpy(msg,"ERROR");
            }
            // メッセージを送信します
            n = send(sockw,msg,strlen(msg),0);
            if (n < 1)
            {   strcpy(szMSG,"send に失敗しました");
                return -1;
            }
            strcpy(szMSG,msg);
            return 0;
        }
        
  3. Jyanken Server の Main Program です。
    Main Program は Jyanken Server Object Class を使うと簡単です。
        /******************************************************/
        /*★ MultiClient  Jyanken Server Program    前田 稔 ★*/
        /******************************************************/
        #include    <stdio.h>
        #include    <conio.h>
        #include    <winsock2.h>
        #include    "JyanSvr.h"
        #pragma     once
        #pragma     comment(lib,"ws2_32.lib")
    
        JyanSvr     *App= NULL;     //Jyanken Object Class
        int         PORT= 12345;
        char        buf[1024];
    
        int main()
        {   int     rc;
    
            App= new JyanSvr();
            // 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 モードで作成します。
    Jyanken Object Class の Header です。
    Wsock Object Class を継承します。
    Client のプログラムは、基本的に Server とのメッセージの送受信と画像を表示するだけです。
        //★ Jyanken Object Header File  2005/08/01  Ver 1.0  前田 稔
        #ifndef     _JyanLib
        #define     _JyanLib
        #include    "Wsock.h"
    
        #define  SAFE_DELETE(p)  { if (p) { delete (p);     (p)=NULL; } }
        #define  SAFE_RELEASE(p) { if (p) { (p)->Release(); (p)=NULL; } }
    
        // Sprite Object Header File  2004-01-05
        class JYAN : public Wsock
        { protected:
            HWND        hWnd;
            HDC         hMdc;
            WORD        WNum;               //横方向の SPrite 個数
            WORD        HNum;               //縦方向の SPrite 個数
            WORD        SWidth;             //Sprite の幅
            WORD        SHeight;            //Sprite の高さ
            WORD        xp,yp;
    
          public:
            JYAN(HWND hWnd);                //Constructor
            ~JYAN();                        //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);  }
        };
    
        #endif
        
  2. Jyanken Client の WinMain Program です。
    ソースコードは長いのですが、画面に貼り付けた EditBox や Button を操作するためで、基本的には Server に接続してメッセージを送受信して表示するだけです。
    ウインドウの表示は じゃんけんゲーム や他のページを参照して下さい。
        /****************************************************/
        /*★ Server に接続してジャンケンをする    前田 稔 ★*/
        /****************************************************/
        #define     NAME    "Jyanken"
        #include    <windows.h>
        #include    "Jyan.h"
        #pragma     once
        #pragma     comment(lib,"ws2_32.lib")
        #pragma     comment(lib,"jyan.lib")
        
        #define     IDM_LOGIN   1001
        #define     IDM_END     1002
        #define     IDM_GU      1003
        #define     IDM_TYOKI   1004
        #define     IDM_PA      1005
        
        JYAN        *App= NULL;
        HINSTANCE   g_hInst;
        HWND        g_hID,g_hURL,g_hPORT,g_hMSG;    // Edit Handle
        HWND        g_hSID,g_hSURL,g_hSPORT,g_hSMSG;// Static Handle
        HWND        g_hButtom1,g_hButtom2,g_hButtom3,g_hButtom4,g_hButtom5;
        char        URL[MAX_PATH]= "127.0.0.1";     //送信先 URL
        int         PORT= 12345;
        char        buf[256];
        
        int         my= 0;          //Client の手
        int         you= 0;         //Server の手
        int         cv= 0;          //Client の得点
        int         sv= 0;          //Server の得点
        char        wait[] = "         勝ち   負け  あいこ\n"
                             "グー      5      -5      3\n"
                             "チョキ    2       0      1\n"
                             "パー     15       0      0";
        RECT        rectwt= { 430, 170, 680, 300 };
        RECT        rt1=    { 190, 260, 290, 320 };
        RECT        rt2=    { 340, 260, 440, 320 };
        
  3. Server に送信する COMMAND を編集する Play() 関数です。
    ply には「グー/チョキ/パー」を示す数字が格納されています。
    PLAY COMMAND に続いて、GET COMMAND で現在の状況を取得します。
    Server からは「双方の手」と「得点」が buf に格納されて送り返されてきます。
    buf の情報を SetStatus() 関数で解析してウインドウに描画します。
        // Server に「グー/チョキ/パー」を送る
        void  Play(HWND hwnd, char ply[])
        {   char        wk[24];
        
            strcpy(buf,"PLAY ");
            GetWindowText(g_hID,wk,23);
            strcat(buf,wk);
            strcat(buf,ply);
            App->Connect(URL,PORT,buf,buf,64);
            SetWindowText(g_hMSG,buf);
            strcpy(buf,"GET ");
            strcat(buf,wk);
            App->Connect(URL,PORT,buf,buf,64);
            SetStatus(buf);
            InvalidateRect(hwnd,NULL,TRUE);
        }
        
  4. buf の情報を解析する SetStatus() 関数です。
    GET COMMAND で送られてくる「じゃんけん」情報の形式です。
    【例】  = 1 2 -3 15
    識別情報として "=" が格納されています
    Client の手(グー=0/チョキ=1/パー=2)
    Server の手(グー=0/チョキ=1/パー=2)
    現在の Client の得点
    現在の Server の得点
        // Status の設定
        void  SetStatus(char *buf)
        {   char    *p;
        
            if (*buf!='=')  return;
            my= atoi(buf+1);
            for(p=buf+1; *p!=0 && *p==' '; p++);
            for(; *p!=0 && *p!=' '; p++);
            you= atoi(p);
            for(; *p!=0 && *p==' '; p++);
            for(; *p!=0 && *p!=' '; p++);
            cv= atoi(p);
            for(; *p!=0 && *p==' '; p++);
            for(; *p!=0 && *p!=' '; p++);
            sv= atoi(p);
        }
        
  5. CALLBACK 関数です。
    WM_PAINT: では SetStatus() 関数で保存した「双方の手と得点」をウインドウに描画します。
    ID と URL をタイプして接続ボタンで Login します。
    Login が出来れば「グー/チョキ/パー」のボタンで Client 側の手を送ります。
    終了ボタンで Logout します。
        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);
                    App->Show(my,150,80);
                    App->Show(you,300,80);
                    DrawText(hdc,wait,-1,&rectwt,DT_WORDBREAK);
                    wsprintf(buf,"%6d",cv);
                    DrawText(hdc,buf,-1,&rt1,0);
                    wsprintf(buf,"%6d",sv);
                    DrawText(hdc,buf,-1,&rt2,0);
                    EndPaint(hWnd, &ps);
                    break;
                case WM_COMMAND:
                    switch(LOWORD(wParam))
                    {   case IDM_LOGIN:
                            if (App->Startup()==-1)
                            {   SetWindowText(g_hMSG,App->szMSG);
                                break;
                            }
                            strcpy(buf,"LOGIN ");
                            GetWindowText(g_hID,wk,23);
                            strcat(buf,wk);
                            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:
                            strcpy(buf,"END ");
                            GetWindowText(g_hID,wk,23);
                            strcat(buf,wk);
                            App->Connect(URL,PORT,buf,buf,64);
                            SendMessage(hWnd,WM_CLOSE,0,0L);
                            break;
                        case IDM_GU:
                            Play(hWnd, " 0");
                            break;
                        case IDM_TYOKI:
                            Play(hWnd, " 1");
                            break;
                        case IDM_PA:
                            Play(hWnd, " 2");
                            break;
                    }
                    break;
        

【課題】

  1. Client 同士が対戦する「三山くずしゲーム」 を参考にして Client 同士が対戦する「じゃんけんゲーム」を作成して下さい。
    同時に10組ぐらいが対戦できるようにしましょう。
  2. Server を相手に「三山くずしゲーム」をするゲームプログラムを作成して下さい。
    同時に10人の Client が接続できるようにしましょう。

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