【Processing】【AI】ProcessingでサッカーゲームのAIシミュレーションアプリを作る


0.ソースコードのリンク

全体のソースコードは
↓リンク↓
http://qiita.com/66zaha_9su/items/3ebe6ba302b4627c53c6

1.ことの発端

同胞で集まる定例会にて、内輪のPCを覗かせてもらった。
それが、AIを使った1対1のシューティングのデモだった。

それから着想を得て、「それに似たものを作りたいなぁ」と思い。
制作に至った。

2.AIとは?

Artificial Intelligence の略。
日本語に訳せば、「人工的な知能」。



一括りに「AI」といっても、さまざま種類のAIが存在します。
大きく分けて2つ。
1つ目は「学習するAI」
もう1つは「学習しないAI」です。

1つ目の具体例として「Alpha Go」があり、
「Alpha Go」で囲碁プレーヤーを打ち負かしたことで
とても有名になりました。
他にも「Watson」など・・

もう1つの方は
各場面に応じて手を考える「オセロプログラムのAI」や
プレイヤーに対して絶対勝てる手を出す「じゃんけんプログラムの相手のAI」
などもその1つといえるでしょう。

3.プログラムの紹介とその他の説明

サッカーのプレイヤーをAIとして作った。
サッカーの代表的なルールの
「オフサイド」
「イエローカード」
「レッドカード」
は無しとする。

そしてキーパーも居ない。

4.ソース解説

プレイヤークラス

class Player{
  float p_radius = 28;
  float p_xPos;
  float p_yPos;
  float p_xVec;
  float p_yVec;
  float p_spd;
  float p_angl;
  float p_power;

  int p_siya;
  int p_siya_d;

  Player(){    //mainly initiallize process    initial position determine another process
  p_spd=12*random(0.6,0.9);
  p_angl=0;
  p_xPos=0;
  p_yPos=0;

  p_siya=(int) random(2,12);
  p_siya_d=(int) random(160,600);

  p_power=random(12,32);
  }


  void P_initialize(int _number){
    int p_number = _number;

    if(p_number%2 ==0){

      if(p_number == 0){
        fill(#ff7fff);
        ellipse(40, 80, p_radius, p_radius);
        p_xPos=40; p_yPos=80;
      }

      if(p_number == 2){
        fill(#ff7fff);
        ellipse(200, 80, p_radius, p_radius);
        p_xPos=200; p_yPos=80;
      }

      if(p_number == 4){
        fill(#ff7fff);
        ellipse(360, 80, p_radius, p_radius);
        p_xPos=360; p_yPos=80;
      }

      if(p_number == 6){
        fill(#ff7fff);
        ellipse(150, 350, p_radius, p_radius);
        p_xPos=150; p_yPos=350;
      }

      if(p_number == 8){
        fill(#ff7fff);
        ellipse(250, 350, p_radius, p_radius);
        p_xPos=250; p_yPos=350;
      }
    }

    else if(p_number%2 ==1){

      if(p_number == 1){
        fill(#00bfff);
        ellipse(40, 720, p_radius, p_radius);
        p_xPos=40; p_yPos=720;
      }

      if(p_number == 3){
        fill(#00bfff);
        ellipse(200, 720, p_radius, p_radius);
        p_xPos=200; p_yPos=720;
      }

      if(p_number == 5){
        fill(#00bfff);
        ellipse(360, 720, p_radius, p_radius);
        p_xPos=360; p_yPos=720;
      }

      if(p_number == 7){
        fill(#00bfff);
        ellipse(150, 450, p_radius, p_radius);
        p_xPos=150; p_yPos=450;
      }

      if(p_number == 9){
        fill(#00bfff);
        ellipse(250, 450, p_radius, p_radius);
        p_xPos=250; p_yPos=450;
      }
    }

  }


  void update(int num){
    float p_dist;
    float x_dist;
    float y_dist;
    float kakudo;

    float p_siya_f;    //f means from
    float p_siya_e;    //e means end

    float p_x_ran;
    float p_y_ran;


    p_dist=sqrt((p_xPos-eb_xPos)*(p_xPos-eb_xPos)+(p_yPos-eb_yPos)*(p_yPos-eb_yPos));
    x_dist=eb_xPos-p_xPos;
    y_dist=eb_yPos-p_yPos;

    kakudo=degrees(atan2(x_dist,y_dist));

    if(kakudo < 0){
      kakudo+=kakudo+360;
    }

    p_angl=kakudo;

    p_siya_f = 360 * ( (float)frame % (float)p_siya ) / (float)p_siya;
    p_siya_e = 360 * ( (float)frame % (float)p_siya +1) / (float)p_siya;


    if( p_dist < ((float)p_siya_d) && p_siya_f < kakudo && kakudo < p_siya_e){


      if(x_dist<=0 && y_dist <=0){
        p_x_ran=p_spd*(abs(cos(kakudo*PI/180)))*-1;
        p_y_ran=p_spd*abs(sin(kakudo*PI/180))*-1;

        p_xPos+=p_x_ran;
        p_yPos+=p_y_ran;
      }

      else if(x_dist>=0 && y_dist <=0){
        p_x_ran=p_spd*abs(cos(kakudo*PI/180));
        p_y_ran=p_spd*abs(sin(kakudo*PI/180))*-1;

        p_xPos+=p_x_ran;
        p_yPos+=p_y_ran;
      }

      if(x_dist<=0 && y_dist >=0){
        p_x_ran=p_spd*abs(cos(kakudo*PI/180))*-1;
        p_y_ran=p_spd*abs(sin(kakudo*PI/180));

        p_xPos+=p_x_ran;
        p_yPos+=p_y_ran;
      }

      else if(x_dist>=0 && y_dist >=0){
        p_x_ran=p_spd*abs(cos(kakudo*PI/180));
        p_y_ran=p_spd*abs(sin(kakudo*PI/180));

        p_xPos+=p_x_ran;
        p_yPos+=p_y_ran;
      }

      //hamidasi
      if(p_xPos>400-p_radius) p_xPos=400-p_radius;
      if(p_xPos<p_radius) p_xPos=p_radius;
      if(p_yPos>800-p_radius) p_yPos=800-p_radius;
      if(p_yPos<p_radius) p_yPos=p_radius;

      dist[num]=sqrt((p_xPos-eb_xPos)*(p_xPos-eb_xPos)+(p_yPos-eb_yPos)*(p_yPos-eb_yPos));
    }


    else if(p_dist < ((float)p_siya_d) ){
      if(x_dist<0 && y_dist <0){
        p_x_ran=p_spd*abs(cos(kakudo*PI/180))*-1*random(-0.75,0.75);
        p_y_ran=p_spd*abs(sin(kakudo*PI/180))*-1*random(-0.75,0.75);

        p_xPos+=p_x_ran;
        p_yPos+=p_y_ran;
      }

      else if(x_dist>0 && y_dist <0){
        p_x_ran=p_spd*abs(cos(kakudo*PI/180))*random(-0.75,0.75);
        p_y_ran=p_spd*abs(sin(kakudo*PI/180))*-1*random(-0.75,0.75);

        p_xPos+=p_x_ran;
        p_yPos+=p_y_ran;
      }

      if(x_dist<0 && y_dist >0){
        p_x_ran=p_spd*abs(cos(kakudo*PI/180))*-1*random(-0.75,0.75);
        p_y_ran=p_spd*abs(sin(kakudo*PI/180))*random(-0.75,0.75);

        p_xPos+=p_x_ran;
        p_yPos+=p_y_ran;
      }

      else if(x_dist>0 && y_dist >0){
        p_x_ran=p_spd*abs(cos(kakudo*PI/180))*random(-0.75,0.75);
        p_y_ran=p_spd*abs(sin(kakudo*PI/180))*random(-0.75,0.75);

        p_xPos+=p_x_ran;
        p_yPos+=p_y_ran;
      }

      //hamidasi
      if(p_xPos>400-p_radius) p_xPos=400-p_radius;
      if(p_xPos<p_radius) p_xPos=p_radius;
      if(p_yPos>800-p_radius) p_yPos=800-p_radius;
      if(p_yPos<p_radius) p_yPos=p_radius;

      dist[num]=sqrt((p_xPos-eb_xPos)*(p_xPos-eb_xPos)+(p_yPos-eb_yPos)*(p_yPos-eb_yPos));

    }

    else if(p_dist > ((float)p_siya_d) ){

            if(x_dist<=0 && y_dist <0){
        p_x_ran=p_spd*(abs(cos(kakudo*PI/180))+random(-5.0,5.0))*-1*random(0,0.4);
        p_y_ran=p_spd*abs(sin(kakudo*PI/180))*-1*random(0,0.4);

        p_xPos+=p_x_ran;
        p_yPos+=p_y_ran;
      }

      else if(x_dist>=0 && y_dist <0){
        p_x_ran=p_spd*(abs(cos(kakudo*PI/180))+random(-5.0,5.0))*random(0,0.4);
        p_y_ran=p_spd*abs(sin(kakudo*PI/180))*-1*random(0,0.4);

        p_xPos+=p_x_ran;
        p_yPos+=p_y_ran;
      }

      if(x_dist<=0 && y_dist >0){
        p_x_ran=p_spd*(abs(cos(kakudo*PI/180))+random(-5.0,5.0))*-1*random(0,0.4);
        p_y_ran=p_spd*abs(sin(kakudo*PI/180))*random(0,0.4);

        p_xPos+=p_x_ran;
        p_yPos+=p_y_ran;
      }

      else if(x_dist>=0 && y_dist >0){
        p_x_ran=p_spd*(abs(cos(kakudo*PI/180))+random(-5.0,5.0))*random(0,0.4);
        p_y_ran=p_spd*abs(sin(kakudo*PI/180))*random(0,0.4);

        p_xPos+=p_x_ran;
        p_yPos+=p_y_ran;
      }

      //hamidasi
      if(p_xPos>400-p_radius) p_xPos=400-p_radius;
      if(p_xPos<p_radius) p_xPos=p_radius;
      if(p_yPos>800-p_radius) p_yPos=800-p_radius;
      if(p_yPos<p_radius) p_yPos=p_radius;

    }

  }

  void p_kick_hantei(int num){
    float min=min(dist);
    if(dist[num]<p_radius && min == dist[num]){
      kicked=true;
      ball.kick(p_angl,p_power,num);    //ball.kick method uses as  ball.kick(ANGLE,POWER);
    }
  }



  void display(int num){
    float hani =(float) p_siya;
    float siya1 =(float) (frame%p_siya);
    float siya2 =(float) ((frame+1)%p_siya);
    float siya_d=p_siya_d;
    float kakudo=360.0;



    if(num%2 == 0){
      fill(#ff7fff,255);
      ellipse(p_xPos,p_yPos,p_radius,p_radius);
      fill(#ff7fff,63);
      arc( p_xPos, p_yPos, siya_d*2, siya_d*2, radians(kakudo*siya1/hani),radians(kakudo*siya2/hani));
      alpha(0);
    }

    else{
      fill(#00bfff,255);
      ellipse(p_xPos,p_yPos,p_radius,p_radius);
      fill(#00bfff,63);
      arc( p_xPos, p_yPos, siya_d*2, siya_d*2, radians(kakudo*siya1/hani),radians(kakudo*siya2/hani));
      alpha(0);
    }
  }

  void p_goal_init(int num){
    float x_pos=random(p_radius*2,400-p_radius*2);
    float y_pos=random(p_radius*2,800-p_radius*2);

    p_xPos=x_pos;
    p_yPos=y_pos;
  }
}

解説として、
①・ボールが視程の範囲内かつ視野のレーダーの中に入っていれば、活発に動き、
②・レーダーの中にボールが入っていないが、視程の範囲内であれば上の条件ほどではないが動く。
1,2にも満たさない場合は、少しずつボールに近づいていく。

ボールクラス

class Ball{
  float b_radius;
  float b_xPos;
  float b_yPos;

  Ball(){
    b_radius=18.0;
    b_xPos=200;
    b_yPos=400;
  }

  void B_initialize(){
    fill(#000000);
    ellipse(200, 400, b_radius, b_radius);
    b_xPos=200; b_yPos=400;
    eb_xPos=200; eb_yPos=400;
  }

  void display(){
    fill(#000000);
    ellipse(b_xPos, b_yPos, b_radius, b_radius);
    eb_xPos=b_xPos; eb_yPos=b_yPos;
  }

  void kick(float kakudo,float power,int num){
    float p_x_kick=(cos(kakudo*PI/180)+0.2)*power;
    float p_y_kick=(sin(kakudo*PI/180)+0.2)*power;



    if(num%2 ==0){
      b_xPos+=p_x_kick;
      b_yPos+=p_y_kick;

      if(b_xPos>400-b_radius) b_xPos=400-b_radius;  //ball is out of vesel (not goal)
      if(b_xPos<b_radius) b_xPos=b_radius;  //ball is out of vesel (not goal)

      if(b_yPos>800-b_radius){
        p_point++;
        b_yPos=400;
        b_xPos=200;

        for(int i=0; i<players.length; i++){
          players[i].p_goal_init(i);
        }
    }
      if(b_yPos<b_radius){
        b_point++;
        b_yPos=400;
        b_xPos=200;

        for(int i=0; i<players.length; i++){
          players[i].p_goal_init(i);
        }
    }

      eb_xPos=b_xPos;
      eb_yPos=b_yPos;
    }

    else if(num%2 ==1){
      b_xPos-=p_x_kick;
      b_yPos-=p_y_kick;

      if(b_xPos>400-b_radius) b_xPos=400-b_radius;  //ball is out of vesel (not goal)
      if(b_xPos<b_radius) b_xPos=b_radius;  //ball is out of vesel (not goal)

      if(b_yPos>800-b_radius){
        p_point++;
        b_yPos=400;
        b_xPos=200;

        for(int i=0; i<players.length; i++){
          players[i].p_goal_init(i);
        }
    }
      if(b_yPos<b_radius){
        b_point++;
        b_yPos=400;
        b_xPos=200;

        for(int i=0; i<players.length; i++){
          players[i].p_goal_init(i);
        }
    }

      eb_xPos=b_xPos;
      eb_yPos=b_yPos;
    }
  }
}

その他

long frame =0;
float eb_xPos;
float eb_yPos;
boolean kicked=false;
int b_point=0;
int p_point=0;

float[] dist;

Player[] players =new Player[22];
Ball ball =new Ball();

void setup() {
  size(400, 800);  
  background(255);
  smooth();
  noStroke();
  fill(0);
  frameRate(-60);

  dist=new float[players.length];

  for (int i = 0; i < players.length; i++) players[i] = new Player();
  for (int j = 0; j < players.length; j++) players[j].P_initialize(j);
  for (int k = 0; k < players.length; k++)dist[k]=9999.9;

  ball=new Ball();
  ball.B_initialize();
}

void draw() {
  background(255);
  frame++;


  for (int i = 0; i < players.length; i++) {
    dist[i]=9999.9;
    players[i].update(i);
    players[i].p_kick_hantei(i);
    players[i].display(i);

    //circles[i].update();
    //circles[i].display();
  }
  ball.display();
  kicked=false;

  fill(0);
  text(frame+"F", 5, 10);
  text(nfs(frameRate, 3, 1)+"FPS", 80, 10);
  text(p_point+"-"+b_point, 160, 10);
}

4.1 変数などの説明

初期宣言

Playerクラス

Ballクラス

プレイヤーのプロパティ

Play.updateの変数解説

4.2 スナップショット

5 改善点

ボールが壁に行きやすい
ボールとプレイヤーがほぼ一直線を描いて、動きにくくなる。

青チームと桃チームの戦力差
↑プレイヤーの人数が少ないときはとりわけ著しくなる。

戦力差はあっても、長時間シミュレーションをしていくと、段々2チームの差は数字に現れなくなる。

↑視野の扇形が一部描写されていませんが、更新処理よるもので、実際にはすべて見えています。↑

6 感想

少し不満は残ったものの、概ね満足できるものができた。

パラグラフ4で説明している、不具合を解決した場合、
コメントで改善箇所などをコメントしていただくと、
大変嬉しゅうございます。

これは本当に声を大にして「人工知能(AI)」と呼べるのだろうか......

7 出典

https://processing.org/
↑Processing.orgのリファレンス↑

詳細記事も書ければ書く予定であります。

8 omake


33-4よりもひどい・・