Powered by SmartDoc

より「強い」ロボットを作るために

Tue Mar 18 18:32:23 JST 2003
DRM
Robocodeをインストールし、MyFirstRobotやサンプルロボットを一通り確認し終えると、 今度はそれらに勝てるような、強いロボットが作りたくなると思います。 ここではそのような方を対象に、強いロボットを作成するための技術的な指針について、1つの見方を提示します。

目次

「強い」ロボット作成までの流れ

Robocodeは、戦闘によって得られるスコアによって勝敗が確定します。多くのスコアが得られる要因は、「最後まで生き残る」「敵に留めの一撃を刺す」などあるのですが、基本的に、ロボットには敵を的確に攻撃するスキルが要求されます。(ロボットが自分自身のエネルギーを回復するには、「敵を攻撃する」という手段しかありません。)

敵よりも多くの弾をあて、敵よりも多くの弾をかわすロボットを作成すれば、ゲームに勝つ可能性が増加すると考えられます。

そのために開発者が行うことができることは、

ということになります。

その後、作成したロボットを用いて

を繰り返すことにより、ロボットの戦闘性能を高めていくことになります。

基礎知識

ここでは主に、

について述べます。

これらの内容はRobocodeのヘルプ、またインターネットに点在する情報で調べることができます。

ロボット性能

ゲーム内の単位時間を、「Tick」という単位で表します。現在のゲーム内時間は、getTime()で取得可能です。

1Tickあたりのレーダーの最大回転半径

45(度)

1Tickあたりの砲塔の最大回転半径

20(度)

1Tickあたりのロボットの最大回転半径

(10 - 0.75 *ロボット速度)(度)

1Tickあたりのロボット最大加速度

1(ピクセル)

1Tickあたりのロボット最大減速度

2(ピクセル)

1Tickあたりのロボット最大速度

8(ピクセル)

弾丸性能

弾丸エネルギーの最小値

0.1(エネルギー)

弾丸エネルギーの最大値

3.0(エネルギー)

発砲時の砲塔熱量

(1 +弾丸エネルギー/ 5)(砲塔熱量)

なお、弾丸連射間隔は、(砲塔熱量/ getGunCoolingRate())(Tick)です。

発砲時のロボットエネルギー低下

弾丸エネルギー(エネルギー)

1Tickあたりの弾丸速度

(20 - 3 *弾丸エネルギー)(ピクセル)

弾丸が命中したときの敵ダメージ

弾丸エネルギー> 1.0の時:(4 *弾丸エネルギー+ 2 * (弾丸エネルギー- 1.0))(エネルギー)

弾丸エネルギー≦1.0の時:4 *弾丸エネルギー(エネルギー)

弾丸が命中したときの自機エネルギー獲得量

3 *弾丸エネルギー(エネルギー)

ダメージ量(弾丸以外)

壁衝突のダメージ

ロボット速度/ 2(エネルギー)

ロボット衝突のダメージ

0.6(エネルギー)

Robocodeにおける三角関数

Robocodeにおける戦場は、x軸が右に、y軸が上に伸びている直交座標系です。

しかし、絶対角度は真上が0度で、時計回りに90度、180度、270度となっています。

よって、「数学的なsin、cosの適用を(x, y)に対し逆に行う必要がある」という注意が必要となります。

これを踏まえて、以下の変換式がRobocodeの世界では定義されます。

極座標(r,θ)を直交座標(x,y)へ変換する式

θをラジアンではなく、弧度法による角度(360度表記)とします。

	x = r * Math.sin(Math.toRadians(θ));
	y = r * Math.cos(Math.toRadians(θ));

直交座標(x,y)を極座標(r,θ)へ変換する式

θをラジアンではなく、弧度法による角度(360度表記)とします。

	r = Math.sqrt(y * y + x * x);
	θ = Math.toDegrees(Math.atan2(y, x));

その他

その他必要と思われる数値の取得方法等、下表に列挙します。

現在のゲーム内時間 getTime()
戦場の横の長さ getBattleFieldWidth()
戦場の縦の長さ getBattleFieldHeight()
銃身冷却率 getGunCoolingRate()
戦場の敵の数 getOthers()
ロボット半径 20(ピクセル)?

作成しておくと便利なメソッドについて

いくつかの、汎用に用いる計算はメソッドとして作成しておくことをお勧めします。

ここでは、弧度法角度の丸めを行うメソッドをご紹介します。それぞれ、-180≦θ<180、0≦θ<360の丸めを行います。

public double normalRelativeAngle(double deg){
	for(; deg >= 180D; deg -= 360D);
	for(; deg < -180D; deg += 360D);
	return deg;
}
public double normalAbsoluteAngle(double deg){
	for(; deg >= 360D; deg -= 360D);
	for(; deg <  0.0D; deg += 360D);
	return deg;
}

ロボットの戦略

ロボットは、レーダー、砲塔、車体によって構成されます。それぞれのパーツは、

という機能を持ちます。

Robocodeにおけるロボットの戦略とは、この3機能を、いつ、どのように制御するか、という方針を決めることに他なりません。

それぞれの機能について、すでに代表的な制御方法が整いつつあります。これらを利用する、あるいは応用することで、ロボット戦略の選択肢を増やすことができると考えられます。

索敵

索敵における制御項目は

があります。

敵の所在が判明しなければ、攻撃も防御もままなりません。認知の要となります。

敵を発見するためにはレーダーを回転させ、レーダーの索敵範囲が敵位置を通過する必要があります。1on1の場合は敵が1機であるため、一度敵をスキャンしたら敵をスキャンし続けるようにレーダーを動かすことにより、連続的に敵を捕捉し続けることができます。Meleeでは、より広範囲の敵の動きを知らなければならないため、レーダーの制御には工夫が必要になります。

1on1用のレーダー制御

melee用のレーダー制御

攻撃

攻撃における制御項目は

があります。ここでは、弾丸エネルギーの決定以外のアルゴリズムについて、「射撃アルゴリズム」としてご紹介します。

敵を破壊するのに不可欠な制御項目であり、攻撃の要となります。

それぞれの射撃アルゴリズムには得手、不得手があるため、いくつかの射撃アルゴリズムを持ち、命中率に応じて射撃アルゴリズムを切り替えるロボットもいます。

固定照準

レーダーと砲塔を同方向に動かすようにし、onScannedRobot()で敵を見つけた瞬間に射撃します。もしくは、onScannedRobot()で敵の座標を記憶しておき、run()メソッド内で砲塔をその座標に向け射撃します。

ほとんどのサンプルロボットが実装している射撃アルゴリズムです。

線形照準

敵の進行方向、速度を一定だと仮定し、弾丸が命中するための砲塔角度を求め、その角度だけ砲塔を回し射撃します。

円形照準

敵の進行方向、速度、移動角変化を一定だと仮定し、弾丸が命中するための砲塔角度を求め、その角度だけ砲塔を回し射撃します。

線形照準、円形照準については、外套と砲塔に詳しい説明が記載されています。

より優れた射撃アルゴリズム

移動

移動における制御項目は

があります。

ロボットのダメージを最小限に抑える、防御の要となります。また、体当たりによる攻撃や、敵より優位な位置を確保することでの攻撃支援といった意味も持つことができ、奥の深い制御項目であると言えます。

多くの優秀なロボットは、複数の移動アルゴリズムを持ち、戦況に応じて動的にアルゴリズムを切り替えようとします。特に、1on1、Meleeで異なる移動アルゴリズムを採用するロボットが多いようです。

ここでは、特定の射撃アルゴリズムに有利な移動アルゴリズムという観点から、いくつかのアルゴリズム例を提示します。実装に際して、これらを応用、あるいは組み合わせながら移動アルゴリズムを検討する、という手順をとることができるでしょう。

固定、線形、円形照準の回避

これらのアルゴリズムは、「射撃開始時点の敵ロボットの位置、向き、速度、回転から未来位置を予測する」ことができます。したがって、この予測をうまく「騙す」ことにより、容易に弾丸を回避することができます。

代表的な実装例であるDodgeBotは、敵が射撃するまでの間、敵に対し横向きの姿勢でじっとしています。敵に打たれた瞬間、DodgeBotは移動します。これだけのことで、固定、線形、円形照準による弾丸はほぼ回避できてしまいます。

DodgeBotについては、外套と砲塔に詳しい説明が記載されています。

Pattern Matchingの回避

本アルゴリズムは「敵の動き(角速度、速度変化)をログとして保存し、現在の動きが過去のログにある程度マッチしたら、アルゴリズム的に似たような軌跡を辿ると仮定して軌跡を先読みし、弾を撃つ」という照準を行います。

角速度、速度変化を不確定にすることにより、Pattern Matching効果を薄れさせることが可能です。また、Pattern Matchingは「そのパターンに本当にマッチするか?」の確定タイムラグがよく発生するので、たまにまっすぐ動いたり、止まったりするのも意外と安全なことがあります。

Guess Factor Aimingの回避

本アルゴリズムは「敵に対して弾を撃ち目標位置に弾が到着するまでの時間に敵が動ける範囲は、多くとも、敵がまっすぐ最大速で前後に走った範囲。したがってその範囲で存在位置の統計を取って存在確率の高いところに弾を撃ち、平均より高い確率で弾を当てる」という照準を行います。

敵から見える存在確率が平均化するように動くことで、Guess Factor Aimingの照準効果を薄れさせることができます。

その他

たとえば戦場の任意の位置(敵の位置など)に斥力を配置し、ロボットが受ける斥力をベクトルで合成して、そのベクトルに従順にロボットを動かすようにすると、柔軟に敵ロボットを避けるロボットができます。このような原理に基づく移動アルゴリズムを反重力移動と呼びます。主に、Meleeに効果を発揮すると言えます。

反重力移動については、Robocodeの達人たちが明かす秘訣: 反重力移動に詳しい説明が記載されています。

ロボット実装の例

AdvancedRobotの雛形

AdvancedRobotのサブクラスを用い、レーダー、砲塔、車体を並列で動かすときは、ゲーム時間「Tick」の存在を無視しては制御しきれないと思います。

ここでは、1Tickごとに動作内容を決定しながら動くロボットの雛形を提示します。

packege abc;

import robocode.*;

public class ABC extends AdvancedRobot(){
	// === クラスのプロパティを記述 === 
	・・・

	// === クラスのメソッドを記述 === 

	// runメソッド
	public void run(){
		init();            // 初期化コード
		while(true){
			move();    // 1Tickで収まる動作をここで記述
			execute(); // set***()メソッドでキャッシュされた動作を実行
		}
	}

	void init(){ // 初期化
		setColors(・・・);
		setAdjustGunForRobotTurn(true);
		setAdjustRadarForGunTurn(true);
		setAdjustRadarForRobotTurn(true);
		・・・
	}
	
	void move(){ // 通常動作
		// レーダー制御
		・・・
		setTurnRadarRight(・・・);//レーダーの回転

		// 砲塔制御
		・・・
		setTurnGunRight(・・・);//砲塔の回転
		・・・
		setFireBullet(・・・);//あるエネルギーを持つ弾丸の発射

		// 車体制御
		・・・
		setTurnRight(・・・);//車体の回転
		・・・
		setAhead(・・・);//車体の速度
	}
	
	// イベントハンドラ
	public void onScannedRobot(scannedRobotEvent e){
		//敵情報を保存
		・・・
	}
	・・・

	// その他メソッド
	・・・
}

ロボット実装例

また、メインルーチンをrun()メソッドではなく、onScannedRobot()に配置する実装方法もあります。

次のロボットは実際に動作するロボットです。上の雛形とはずいぶん構造が異なるのですが、これがどのように動作するか推測してみると、学習になるかと思います。

package drm.micro;

import robocode.*;
import java.util.Random;

public class AVB extends AdvancedRobot
{
	// Properties
	static double oppEnergy;
	static long   movementTime;
	static double speed;
	static int    reverseInt = 1;
	static double offset     = 0D;
	static Random random     = new Random();
	
	// Methods
	public void run() {
		setAdjustGunForRobotTurn(true);
		movementTime = 0L;
		oppEnergy    = 100D;
		do{
			turnRadarRightRadians(1);
		}while(true);
	}
	public void onBulletHit(BulletHitEvent e) {
		offset -= .04;
	}
	public void onScannedRobot(ScannedRobotEvent e) {
		double oppBulletPower = oppEnergy
		   - (oppEnergy = e.getEnergy());

		// turn body and decide movement speed.
		if (0L == movementTime) {
			setTurnRightRadians(Math.sin
			   (Math.toRadians(e.getBearing() + 90 + 5
			      * reverseInt)));
			if (oppBulletPower > 0D){
				reverseInt   = - reverseInt;
				movementTime = getTime()
				   + (long)(e.getDistance()
				   / (20D - 3D * oppBulletPower));
				speed = random.nextDouble() * 2D + 6D ;
			}
		}
		// decide stop.
		if ((getTime() > movementTime)
			|| (chkOutOfBattleField(Math.toRadians
			      (getHeading() + (reverseInt - 1) * -90), 70))
		    || (random.nextDouble() < 0.01)){
			speed        = 0D;
			movementTime = 0L;
		}
		//accelerate.
		setAhead(getDistanceToKeepPositiveVelocity(speed)
		   * reverseInt);

		double absoluteBearing = e.getBearing() + getHeading();
		// turn radar.
		setTurnRadarRightRadians(3.0 * Math.sin(Math.toRadians
		   (absoluteBearing - getRadarHeading())));

		// turn gun.
		double sign = (double)Math.round(random.nextDouble() * 1.5);
		if (Math.toRadians(e.getHeading() - absoluteBearing) < 0)
		   sign = -sign;
		if (e.getVelocity() < 0) sign = -sign;
		setTurnGunRightRadians(Math.asin(Math.sin(Math.toRadians
		   (absoluteBearing - getGunHeading())) + offset * sign ));

		// shoot.
		if  ((getEnergy() > .1 && setFireBullet(
		   Math.min(getEnergy() / 10.0, e.getEnergy() / 4.0)) != null)
		   && ((offset += .02) > .4))
			offset = -.2;
//out.println("offset = " + offset + " : sign = " + sign );
		execute();
	}
	double getDistanceToKeepPositiveVelocity(double velocity){
		double ret = 0;
		while(velocity > 0D){
			ret      += velocity;
			velocity -= 2.0D;
		}
		return ret;
	}
	boolean chkOutOfBattleField(double headingRadians, double distance){
		double x = getX() + distance * Math.sin(headingRadians);
		double y = getY() + distance * Math.cos(headingRadians);
		return ((x < 0) || (x > getBattleFieldWidth()) ||
		 (y < 0) || (y > getBattleFieldHeight()));
	}
}

ここからダウンロードして、作成したロボットと対戦させることができます。