Posted on

Screeps:Arena Tutorialやった

  • [[2022-04-08]] 買った

「JavaScript」を実際に打ち込んで対戦する『Screeps: Arena』が配信 をみて, 面白そうだと思ったので購入. [[CODINGAME]] みたいにゲームAIを書いて対戦するらしい.

チュートリアルのメモ書きです.

Tutorial

Screeps Documentation

  • spawnCreep: creeps (ユニットのこと) を spawn
    • それぞれ名前といろいろなスキルを持つ
    • Game.spawns['Spawn1'] でアクセス
Game.spawns['Spawn1'].spawnCreep([WORK, CARRY, MOVE], 'Harvester1');

黄色いマスはエネルギー源, WORK で収穫でき, CARRY で運べる

Script

ctrl+enter で実行

  • creep は1500tickの寿命を持つ
  • この例では, spawnCreep には 200energy必要

Room Controller

  • 施設を色々作れる, レベル上げると色々出来る
  • creep.memory に色々記憶できる
    • 内容はタブで確認できる

import { getObjectsByPrototype } from "/game/utils";
import { Creep, Flag } from "/game/prototypes";
import {} from "/game/constants";
import {} from "/arena";

import { getTicks } from "/game/utils";

export function loop() {
  var myCreep = getObjectsByPrototype(Creep).find(creep => creep.my);
  • MOVE: cost = 50, 動ける
  • ATTACK: cost = 80, 攻撃できる
  • RANGED_ATTACK: cost = 250, 3マス以内なら攻撃できる
  • HEAL: cost = 250, 回復できる
  • WORK: cost = 100, エネルギーを収穫できる
  • CARRY: cost = 50, リソースを運べる
  • TOUGH: cost = 10, 何も出来ない
const hasPart = (creep, part) => {
  return creep.body.some(bodyPart => bodyPart.type === part);
}

const invoke = (creep, method, ...args) => {
  creep[method](...args);
};
import { getObjectsByPrototype } from "/game/utils";
import { Creep } from "/game/prototypes";
import { ATTACK, RANGED_ATTACK, HEAL, ERR_NOT_IN_RANGE } from "/game/constants";
import {} from "/arena";

export function loop() {
  const party = getObjectsByPrototype(Creep).filter((c) => c.my);
  const enemies = getObjectsByPrototype(Creep).filter((c) => !c.my);

  for (let creep of party) {
    if (hasPart(creep, ATTACK)) {
      invoke(creep, "attack", enemies[0]);
    }
    if (hasPart(creep, RANGED_ATTACK)) {
      invoke(creep, "rangedAttack", enemies[0]);
    }
    if (hasPart(creep, HEAL)) {
      const damaged = party.find((c) => c.hits < c.hitsMax);
      if (damaged) {
        invoke(creep, "moveTo");
        invoke(creep, "heal", damaged);
      }
    }
  }
}
  • Tower は10エネルギー使って, 50マス以内の敵を攻撃する(減衰有り)
  • creep.transfer(tower, RESOURCE_ENERGY)
  • エネルギーは Container から引き出せる
    • creep.withdraw(container, RESOURCE_ENERGY)
const party = getObjectsByPrototype(Creep).filter((c) => c.my);
const enemies = getObjectsByPrototype(Creep).filter((c) => !c.my);
const tower = getObjectsByPrototype(StructureTower)[0];
const container = getObjectsByPrototype(StructureContainer)[0];

if (tower.store[RESOURCE_ENERGY] < 10) {
	if (party[0].store[RESOURCE_ENERGY] == 0) {
		invoke(party[0], "withdraw", container, RESOURCE_ENERGY);
	} else {
		invoke(party[0], "transfer", tower, RESOURCE_ENERGY);
	}
} else {
	invoke(tower, "attack", enemies[0]);
}

Terrain

  • Plain: 平地
  • Natural: 壊せない壁
  • Constructed: 壊せる壁
  • Swamps: 5tickに1回しか動けない, 5個の MOVE があれば毎ターン動ける
  • Roads: ?? (後でちゃんと読む)
const flags = getObjectsByPrototype(Flag);
const flag = creep.findClosestByPath(flags);
creep.moveTo(flag);
  • spawnCreep に必要な RESOURCE_ENERGY が足りないと { error: -6 } が返ってくる
    • 事前にコストを計算する必要がある

Havest Source

const source = getObjectsByPrototype(Source)[0];
creep.harvest(source);

Build Structure

const site = createConstructionSite(50, 55, StructureTower);
creep.build(site);

// -1: ERR_NOT_OWNER
  • source.mytrue にならないので注意

Tutorial: Final test

  • しばらく経つと敵が壁を壊してこっちに来るのでそれまでにたくさんスポーンさせて迎え撃てばok
  • 色々ユーティリティコード書いたら膨れた
    • フェーズ毎にコールバックを呼んでステートマシンみたいにした
import {
  getObjectsByPrototype,
  createConstructionSite,
  findClosestByPath,
} from "/game/utils";
import {
  // Flag,
  Creep,
  Source,
  StructureTower,
  StructureContainer,
  StructureSpawn,
  ConstructionSite,
} from "/game/prototypes";
import {
  MOVE,
  WORK,
  CARRY,
  ATTACK,
  RANGED_ATTACK,
  HEAL,
  ERR_NOT_IN_RANGE,
  RESOURCE_ENERGY,
} from "/game/constants";
import { getTicks } from "/game";

const hasPart = (creep, part) => {
  return creep.body.some((bodyPart) => bodyPart.type === part);
};

const invoke = (creep, method, ...args) => {
  const res = creep[method](...args);
  console.log(`${method}() => ${res}`);
  if (res === ERR_NOT_IN_RANGE) {
    creep.moveTo(args[0]);
  }
  return res;
};

const inspect = (creeps) => {
  console.log(
    creeps
      .filter((c) => !!c)
      .map((c) => c.body.map((part) => part.type).join(", "))
  );
};

const i = (view) => {
  console.log(`${view.name} @ (${view.x},${view.y})`);
};

const $ = (type, my = true) => {
  const result = getObjectsByPrototype(type).filter((o) =>
    my ? o.my === my : true
  )[0];
  if (!result) {
    console.log(`No ${type.name} found`);
    return null;
  }
  return result;
};

const $$ = (type, my = true) => {
  return getObjectsByPrototype(type).filter((o) => o.my === my);
};

const needCost = (parts) => {
  const map = {
    [MOVE]: 50,
    [WORK]: 100,
    [CARRY]: 50,
    [ATTACK]: 80,
    [RANGED_ATTACK]: 150,
    [HEAL]: 250,
  };
  return parts.map((part) => map[part]).reduce((a, b) => a + b, 0);
};

var party = [];

const tasks = [
  // spawn creeps
  () => {
    const spawner = $(StructureSpawn);
    const parts = [MOVE, ATTACK];
    if (party.length < 5 && spawner.store[RESOURCE_ENERGY] >= needCost(parts)) {
      const res = spawner.spawnCreep(parts);
      console.log(res);
      if (res.object) {
        party.push(res.object);
      }
    }
    console.log(
      `party: ${party.length}, spawner: ${spawner.store[RESOURCE_ENERGY]}`
    );
    // inspect(party);
    return party.length === 5;
  },
  // wait enemy
  () => {
    const enemies = getObjectsByPrototype(Creep).filter((c) => !c.my);
    inspect(enemies);
    const creep = $(Creep);
    const path = findClosestByPath(creep, enemies);
    console.log(`path: ${path}`);
    creep.moveTo(path);
    return !path;
  },
  // attack enemy
  () => {
    const creeps = $$(Creep);
    const enemies = getObjectsByPrototype(Creep).filter((c) => !c.my);
    creeps.forEach((creep) => {
      invoke(creep, "attack", enemies[0]);
    });
    return false;
  },
];

export function loop() {
  console.log(`t = ${getTicks()} tasks: ${tasks.length}`);
  const task = tasks[0];
  if (task && task()) {
    tasks.shift();
  }
}

所感

  • import pathが通らないので補完が全く効かなく, DXが低い
    • Rustで書きたい (Web Assembly として実行できるらしい? #todo )
  • デバッグが結構辛い
    • エラーコードが定数なのでドキュメントを見に行かなければいけない
    • シミュレータみたいの欲しい