Help us understand the problem. What is going on with this article?

CodinGame はBOT(AIプログラム)でバトルするのが正しい楽しみ方かもしれません

『【CodinGame】ブラウザでコーディングの基礎からトレーニングできるサイト (疑似ゲーム開発環境を使って学べます。解答は25種類のプログラミング言語から選択して記述可能!)』 という記事で、CodinGame に対してなにやら否定的なコメントを書いてしまいましたが、Twitter で「codingame」を検索してみると、「CodinGame はBOT(AIプログラム)でバトルするのが正しい楽しみ方」的な発言がみられたので、AIについては素人ながら挑戦してみました。

  • まだ、挑戦し始めなのでログ(ブログ)っぽく、やったことをそのまま記述…
  • 勝ち方の指南なんてできないので…「他の人が自分もやってみたい」と思えるような紹介風で…

という目標で書いてます。

長くなる(と思う)ので記事分けながら書いて、あとでマトメの記事が上手くできればいいなと考えています。

それでは、以下本文へ

1. https://www.codingame.com/start にアクセスします。

image.png

2. 「Sign up with Google」を選択します。

image.png

3. サインアップ完了後、https://www.codingame.com/home に自動的に遷移します。

image.png

4. https://www.codingame.com/multiplayer に遷移します。

image.png

5. https://www.codingame.com/multiplayer/bot-programming に遷移します。

image.png

6. TRON BATTLE のリプレイ動画

  • 以下のgif画像を見て分かるように、各プレイヤーの車は異なった色のリボンを残しながら進んでいきます。

2019-08-16_21h16_27.gif

  • 各プレイヤーの車は自分のリボンも他人のリボンも踏んではいけません(踏んだらアウト)。もちろん場外に出てもいけません。上記に違反した時点でそのプレイヤーはアウトとなり、そのプレイヤーのリボンがゲーム画面から消えます(ここもポイント!)。

  • 最後までアウトにならずに生き残ったプレイヤーの勝ちです。(実際には1位、2位、…と順位がつきます)

7. TRON BATTLE のメイン画面の「JOIN」を押してプログラム編集画面(IDE)を開きます

image.png

image.png

8. コードエディタに初期に表示される内容(C#の場合)

解答に使うプログラミング言語は、C#, C++, Java, Javascript, Python3, Bash, C, Clojure, Dart, F#, Go, Groovy, Haskell, Kotlin, Lua, ObjectiveC, OCaml, Pascal, Perl, PHP, Python2, Ruby, Rust, Scala, Swift, VB.NET の中から自由に選べます。

CodinGame のプログラミング問題はほとんど(全て?)、刻々と標準入力から情報を読み取り、刻々と標準出力に指示を書き出すというループから成り立っています。

  • このおかげでプログラミング言語間の差異を吸収しやすくなっています。ユーザーが書くプログラムを取り巻く親プロセスのプログラムは共通の物が使えるからです。
  • static void Main(string[] args) に合わせて、全プログラムを static メソッドで書こうとするとクラスを導入する際にハマることがありますのでご注意。
コードエディタに初期に表示される内容(C#の場合)
using System;
using System.Linq;
using System.IO;
using System.Text;
using System.Collections;
using System.Collections.Generic;

/**
 * Auto-generated code below aims at helping you parse
 * the standard input according to the problem statement.
 **/
class Player
{
    static void Main(string[] args)
    {
        string[] inputs;

        // game loop
        while (true)
        {
            inputs = Console.ReadLine().Split(' ');
            int N = int.Parse(inputs[0]); // total number of players (2 to 4).
            int P = int.Parse(inputs[1]); // your player number (0 to 3).
            for (int i = 0; i < N; i++)
            {
                inputs = Console.ReadLine().Split(' ');
                int X0 = int.Parse(inputs[0]); // starting X coordinate of lightcycle (or -1)
                int Y0 = int.Parse(inputs[1]); // starting Y coordinate of lightcycle (or -1)
                int X1 = int.Parse(inputs[2]); // starting X coordinate of lightcycle (can be the same as X0 if you play before this player)
                int Y1 = int.Parse(inputs[3]); // starting Y coordinate of lightcycle (can be the same as Y0 if you play before this player)
            }

            // Write an action using Console.WriteLine()
            // To debug: Console.Error.WriteLine("Debug messages...");

            Console.WriteLine("LEFT"); // A single line with UP, DOWN, LEFT or RIGHT
        }
    }
}

9. さて、TRON BATTLE の課題(問題)文は以下のような内容です

(長いので折りたたみ中。展開してご覧ください)

◎The Goal
◎目標

In this game your are a program driving the legendary tron light cycle and fighting against other programs on the game grid.

このゲームであなたが目指すのは、ゲームグリッド上で、伝説のトロンライトサイクルを運転して他のプログラムと戦うことのできるプログラムです。

The light cycle moves in straight lines and only turn in 90° angles while leaving a solid light ribbon in its wake. Each cycle and associated ribbon features a different color.
Should a light cycle stop, hit a light ribbon or goes off the game grid it will be instantly deactivated.

ライトサイクルは真っすぐに進むか90°の角度でしか曲がれず、起動時から固形分からなる光のリボンを残しながら進みます。それぞれのライトサイクルと関連付けられたリボンは異なる色を放ちます。
ライトサイクルが停止せざるを得ない、または光のリボンに衝突した、またはゲームグリッドの外に出た場合、そのライトサイクルは即座に非活性化されます。

The last cycle in play wins the game. Your goal is to be the best program: once sent to the arena, programs will compete against each-others in battles gathering 2 to 4 cycles. The more battles you win, the better your rank will be.

最後まで残ったライトサイクルがゲームの勝者となります。あなたの目標はベストプログラムを目指すことです: アリーナに送られれば(訳注: SUBMITボタンを押せば)、プログラム達が、2~4台でのライトサイクルバトルでお互いに競争となります。より多くかつほどあなたのランクが上がります。

◎Rules
◎ルール

Each battle is fought with 2 players. Each player plays in turn during a battle. When your turn comes, the following happens:

それぞれのバトルは2プレイヤーで戦います。それぞれのプレイヤーが順番にプレイします。あなたのターンが来たら、以下が発生します:

  • Information about the location of players on the grid is sent on the standard input of your program. So your AI must read information on the standard input at the beginning of a turn.
  • グリッド上のプレイヤーの位置情報があなたのプログラムの標準入力に送信されます。そのため、あなたのAIはターンの最初に標準入力上の情報を読み込まなければなりません。
  • Once the inputs have been read for the current game turn, your AI must provide its next move information on the standard ouput. The output for a game turn must be a single line stating the next direction of the light cycle: either UP, DOWN, LEFT or RIGHT.
  • 現在のゲームターンのための情報を読み込んだら、AIは次の移動のための情報を標準出力に提供しなければなりません。ゲームターン時の出力は、ライトサイクルの次の移動方向を宣言する一行の出力でなければなりません: UP, DOWN, LEFT, RIGHT のいずれかを出力します。
  • Your light cycle will move in the direction your AI provided.
  • あなたのライトサイクルはAIが出力した方向に動きます。
  • At this point your AI should wait for your next game turn information and so on and so forth. In the mean time, the AI of the other players will receive information the same way you did.
  • この時点で、あなたのAIは次のゲームターンの情報を待たなければなりません。後は、ここまでの繰り返しとなります。一方で、他のプレイヤーのAIもあなたと同様に情報を受け取ります。

If your AI does not provide output fast enough when your turn comes, or if you provide an invalid output or if your output would make the light cycle move into an obstacle, then your program loses.

もし、あなたのAIがあなたのターンが来た時に、十分高速に出力を提供できない場合、または妥当でない出力をした場合、または出力に従うとライトサイクルが障害物に衝突してしまう等の場合には、あなたのプログラムの負けとなります。

If another AI loses before yours, its light ribbon disappears and the game continues until there is only one player left.

もし他のAIがあなたより前に負けた場合は、その光のリボンは消滅し、一人のプレイヤーのみが残るまでゲームは継続します。

The game grid has a 30 by 20 cells width and height. Each player starts at a random location on the grid.

ゲームグリッドは、30x20 のセルで構成されます。それぞれのプレイヤーはグリッド上のランダムな位置からスタートします。

◎Victory Conditions
◎勝利条件

Be the last remaining player
最後まで残るプレイヤーとなること。

◎Game Input
◎ゲームの入力

Input for one game turn
ゲームターン毎の入力

Line 1: Two integers N and P. Where N is the total number of players and P is your player number for this game.

一行目: N と P の2つの整数。Nはプレイヤーの総人数で、Pはこのゲームでのプレイヤー番号です。

The N following lines: One line per player. First line is for player 0, next line for player 1, etc. Each line contains four values X0, Y0, X1 and Y1. (X0, Y0) are the coordinates of the initial position of the light ribbon (tail) and (X1, Y1) are the coordinates of the current position of the light ribbon (head) of the player. Once a player loses, his/her X0 Y0 X1 Y1 coordinates are all equal to -1 (no more light ribbon on the grid for this player).

続くN行: プレイヤー毎に一行。最初の行はプレイヤー0に対するもの、次の行はプレイヤー1、という形になります。それぞれの行は4つの値 X0, Y0, X1, Y1 を含みます。(X0, Y0) は光のリボンの初期位置(tail)で (X1, Y1) はプレイヤーの光のリボンの現在位置(head)です。あるプレイヤーの負けが決定すると、そのプレイヤーの X0 Y0 X1 Y1 の値hあ全て -1 となり、そのプレイヤーの光のリボンはグリッド上には存在しないことを意味します。

Output for one game turn
ゲームターン毎の出力

A single line with UP, DOWN, LEFT or RIGHT

UP, DOWN, LEFT, RIGHT のいずれかを一行で出力。

Constraints
制約

2 ≤ N ≤ 2
0 ≤ P < N
0 ≤ X0, X1 < 30
0 ≤ Y0, Y1 < 20

Your AI must answer in less than 100ms for each game turn.
あなたのAIは各ゲームターンに対して100ms未満で応答しなければなりません。

10. とりあえず、壁への激突、リボンへの激突を避ける目的で作ったプログラム

まったくAI的なことしてませんが、ゲームターン毎に隣(上下左右)のセルだけ見て、障害物がなければそちらに進む(判定順序: 左⇒右⇒上⇒下)。毎ターン、自キャラも含めて位置情報を二次元配列に格納(死んだキャラのリボン情報の消去も一応実装済み。初期は2キャラしかいないのでテストできませんw)。

  • コメントに大体書いたので一点だけ補足すると、ライトサイクルが曲がるとき90°までしか曲がれない(来た方向に戻るようなことはできない)というのをどう表現しようかと迷っていたんですが、自キャラの光のリボンも配列(マップ)に記録して障害物と見做しているので、とりあえず障害物判定するだけでいける方向に進めば良いことだと気づきました。
using System;
using System.Linq;
using System.IO;
using System.Text;
using System.Collections;
using System.Collections.Generic;

class Player
{
    static void Main(string[] args) {
        Player control = new Player();
        string[] inputs;
        Position[] positions;
        // game loop
        while (true) {
            inputs = Console.ReadLine().Split(' ');
            int N = int.Parse(inputs[0]); // total number of players (2 to 4).
            int P = int.Parse(inputs[1]); // your player number (0 to 3).
            Console.Error.WriteLine("P={0}", P);
            positions = new Position[N];
            for (int i = 0; i < N; i++) {
                inputs = Console.ReadLine().Split(' ');
                int X0 = int.Parse(inputs[0]); // starting X coordinate of lightcycle (or -1)
                int Y0 = int.Parse(inputs[1]); // starting Y coordinate of lightcycle (or -1)
                int X1 = int.Parse(inputs[2]); // starting X coordinate of lightcycle (can be the same as X0 if you play before this player)
                int Y1 = int.Parse(inputs[3]); // starting Y coordinate of lightcycle (can be the same as Y0 if you play before this player)
                positions[i] = new Position(i, X0, Y0, X1, Y1);
            }
            string dir = control.HandleVehicless(positions, P);
            control.DumpMap();
            Console.WriteLine(dir);
        }
    }
    // 自分も含めて誰かが通った座標を記憶しておくために使う。
    // 誰も通ってない場合は -1。通った、または現在いるマスに対してはプレイヤーのメンバーIDを格納する。
    int[,] map = new int[30,20];
    // メインコントロールクラスのコンストラクタ(map内の値を-1(=誰も通ってない)に初期化しておく。)
    private Player() {
        for(int y=0; y<20; y++) {
            for(int x=0; x<30; x++) {
                map[x, y] = -1;
            }
        }
    }
    private string HandleVehicless(Position[] positions, int myIndex) {
        Position me = positions[myIndex]; // me = 自分の座標情報
        Console.Error.WriteLine(me); // me を標準エラーに出力(public override string ToString()の定義による)
        foreach(var p in positions) { // (me も含めて)全キャラの座標を通ってはいけない場所に登録。
            AddToMap(p); // ただし、死にキャラの場合はそのキャラの座標情報を全消去する。
        }
        // 上下左右のマスを判定し通ってはいけない場所でなければその方向を返す。
        if (!FoundFromMap(me, -1, 0)) return "LEFT";
        if (!FoundFromMap(me, 1, 0)) return "RIGHT";
        if (!FoundFromMap(me, 0, -1)) return "UP";
        if (!FoundFromMap(me, 0, 1)) return "DOWN";
        return "LEFT"; // ここに来た時点でどの方向も通れないが一応正式な値の一つとして "LEFT" を返す。
    }
    void AddToMap(Position p) {
        if (p.X1 < 0) {
            DeleteMemberIdsFromMap(p.Id); // 現在座標がマイナス値で来たら死にキャラなのでマップから消す。
            return;
        }
        this.map[p.X1, p.Y1] = p.Id; // 配列にプレイヤーのメンバーIDを登録する。
    }
    void DeleteMemberIdsFromMap(int id) {
        for(int y=0; y<20; y++) {
            for(int x=0; x<30; x++) {
                if (map[x, y] == id) map[x, y] = -1;
            }
        }
    }
    // map を検索して通れない場所の場合 true を返す。通れる場合は false。
    // me(自機の座標)に xOffset と yOffset を加えた場所について判定(検索)する。
    bool FoundFromMap(Position me, int xOffset, int yOffset) {
        int x = me.X1+xOffset;
        int y = me.Y1+yOffset;
        if (x < 0) return true;
        if (x > 29) return true;
        if (y < 0) return true;
        if (y > 19) return true;
        return map[x, y] != -1;
    }
    // デバッグ用に 30x20 のマップを表示(現在生きているメンバーのIDを表示。空のマスは '-' を出力)
    void DumpMap() {
        for(int y=0; y<20; y++) {
            for(int x=0; x<30; x++) {
                if (map[x, y] == -1) Console.Error.Write("-"); // -1の場合はマイナス記号を出力。
                else Console.Error.Write(map[x, y]); // -1でなければプレイヤーID(0以上)を出力。
                Console.Error.Write(" ");
            }
            Console.Error.WriteLine();
        }
    }
}
// キャラクターの座標を登録・記憶しておくための入れ物。
// Main 関数が受け取る標準入力の情報を格納するための構造体のようなもの。
// Player インスタンスの各メソッドの引数は標準入力とのやり取りを意識せず、この構造体を期待できる。
class Position
{
    public int Id;
    public int X0;
    public int Y0;
    public int X1;
    public int Y1;
    public Position(int id, int x0, int y0, int x1, int y1)
    {
        this.Id = id;
        this.X0 = x0;
        this.Y0 = y0;
        this.X1 = x1;
        this.Y1 = y1;
    }
    // デバッグなどで出力される際のフォーマットを制御する。
    public override string ToString()
    {
        //return "{X0:" + X0 + ", Y0:" + Y0 + ", X1:" + X1 + ", Y1:" + Y1 + "}";
        return String.Format("{{X0:{0}, Y0:{1}, X1:{2}, Y1:{3}}}", X0, Y0, X1, Y1);
    }
}

11. アリーナでリーグ戦をする前に「PLAY MY CODE」ボタンで確認

image.png

12. 対戦実行速度を上げてサクサクデバッグ

image.png

image.png

13. アリーナ(リーグ戦)に挑戦

image.png

image.png

14. リーグ戦でボスに勝ったら以下のような画面が表示されます

  • 最初のリーグではプレイヤーの数は2ですが、リーグが上がっていくと増えていくみたいです。

image.png

15. リーグ戦で勝てず上位リーグに上がれなかった場合の対処法

image.png

image.png

image.png

image.png

他にも負けた相手がいる場合には、同様の手順でIDEに読み込んで対戦しながらプログラム(AI)を強くするとよいでしょう。

16. 最後に

AIを作るノウハウを持っていないことと、強いプログラムを記事で晒すのはいいアイディアではないかなと思ってますので、今回は TRON BATTLE を紹介しましたが、次はまた別のプログラムについて紹介したいと思っています。以下のツイートの画像をクリックしていただければ、そのゲームのリプレイ画面が表示されます。
もし、分かりにくいところなどありましたらコメント等をよろしくお願いいたします。それでは…

P.S.

https://www.codingame.com/multiplayer/bot-programming/tron-battle/leaderboard

image.png

javacommons
TOEICスコアは910点です。絵文字はいかがでしょうか?Qiitaの記事タイトルにも使えますよw 🎧😀😬😁😂😃😄😅😆😇😉😊🙂😋😌😍😘😗😙😚😜😝😛😎😏😶😐😑😒😳😞😟😠😡😔😕🙁😣😖😫😩😤😮😱😨😰😯😦😧😢😥😪😓😭😵😲😷😴💤💩😈👿👹👺💀👻
https://code.dlang.org/packages/pegged-cutter
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした