In this notebook you will find a run-through of how you could use the cljsc2 library to interact with the StarCraft II AI API, we'll build a base and army and try to defeat the built in easy AI!

Get all the functions and data from the core namespaces so we can start a client and connect to it:

In [1]:
(use 'cljsc2.clj.core)

(def sc-process (start-client "/home/bb/cljsc2/StarCraftII/Versions/Base59877/SC2_x64"))

(def conn (restart-conn))
Out[1]:
#'user/conn

Get functions and data for game information:

In [2]:
(use 'cljsc2.clj.game-parsing)
In [3]:
(keys knowledge-layout)
Out[3]:
(:datascript-schema :ability-type-attributes :unit-type-attributes :unit-attributes :upgrade-type-attributes :order-attributes)
In [4]:
(get knowledge-layout :ability-type-attributes)
Out[4]:
(:ability-type/allow-minimap :ability-type/is-instant-placement :ability-type/is-building :ability-type/id :ability-type/allow-autocast :ability-type/footprint-radius :ability-type/remaps-to-ability-id :ability-type/available :ability-type/target :ability-type/name :ability-type/cast-range)

The knowledge is stored in a datalog query engine called datascript:

In [5]:
(require '[datascript.core :as ds])

It's simple and powerful, it can do relational/logical reasoning on a set of "facts"

A fact is a vector of [entity-id attribute-name value]

In [6]:
(ds/q '[:find ?name
        :where
        [_ :unit-type/name ?name]]
      knowledge-base)
Out[6]:
#{["Armory"] ["FactoryReactor"] ["Ghost"] ["Raven"] ["StarportFlying"] ["GhostAcademy"] ["StarportTechLab"] ["Starport"] ["SensorTower"] ["MissileTurret"] ["Battlecruiser"] ["CommandCenter"] ["ThorAP"] ["Barracks"] ["WidowMine"] ["Thor"] ["HellionTank"] ["BarracksFlying"] ["BarracksReactor"] ["Bunker"] ["Marine"] ["Hellion"] ["OrbitalCommandFlying"] ["Banshee"] ["SupplyDepot"] ["FactoryFlying"] ["FactoryTechLab"] ["SiegeTank"] ["Cyclone"] ["Liberator"] ["Medivac"] ["PlanetaryFortress"] ["BarracksTechLab"] ["EngineeringBay"] ["MULE"] ["Marauder"] ["OrbitalCommand"] ["FusionCore"] ["CommandCenterFlying"] ["Factory"] ["Reaper"] ["SupplyDepotLowered"] ["Reactor"] ["VikingFighter"] ["Refinery"] ["StarportReactor"] ["SCV"]}
In [7]:
(clojure.pprint/pprint 
    (take 5 (ds/q 
                '[:find ?name ?minerals ?vespene ?build-time
                  :where
                  [?unit-e-id :unit-type/name ?name]
                  [?unit-e-id :unit-type/mineral-cost ?minerals]
                  [?unit-e-id :unit-type/vespene-cost ?vespene]
                  [?unit-e-id :unit-type/build-time ?build-time]]
              knowledge-base)))
(["BarracksTechLab" 50 25 400.0]
 ["FactoryTechLab" 50 25 400.0]
 ["Medivac" 100 100 672.0]
 ["EngineeringBay" 125 0 560.0]
 ["Raven" 100 200 960.0])

Queries can take arguments, so you could create functions using it:

In [8]:
(defn get-abilities-for-unit-name [unit-name]
  (ds/q '[:find ?ability-id ?ability-name
          :in $ ?unit-name
          :where
          [?unit-type-e-id :unit-type/name ?unit-name]
          [?unit-type-e-id :unit-type/abilities ?ability-e-id]
          [?ability-e-id :ability-type/name ?ability-name]
          [?ability-e-id :ability-type/id ?ability-id]]
        knowledge-base
        unit-name))

(get-abilities-for-unit-name "Marine")
Out[8]:
#{[380 "Effect Stim Marine"] [1 "Smart"] [17 "Patrol"] [16 "Move"] [4 "Stop Stop"] [18 "HoldPosition"] [23 "Attack Attack"]}

I find having a query system like this on the static data of the API is quite powerful. But it doesn't end with just the static data!

We can run the game and turn the game observations into this facts format to query runtime data!

In [9]:
(send-request-and-get-response-message
   conn
   #:SC2APIProtocol.sc2api$RequestCreateGame
   {:create-game #:SC2APIProtocol.sc2api$RequestCreateGame
    {:map #:SC2APIProtocol.sc2api$LocalMap
     {:local-map
      {:map-path "/home/bb/cljsc2/StarCraftII/Maps/Simple64.SC2Map"}}
     :player-setup
     [#:SC2APIProtocol.sc2api$PlayerSetup
      {:race "Terran" :type "Participant"}
      #:SC2APIProtocol.sc2api$PlayerSetup
      {:race "Protoss" :type "Computer"}]}})

(send-request-and-get-response-message
   conn
   #:SC2APIProtocol.sc2api$RequestJoinGame
   {:join-game
    #:SC2APIProtocol.sc2api$RequestJoinGame
    {:participation
     #:SC2APIProtocol.sc2api$RequestJoinGame
     {:race "Terran"}
     :options #:SC2APIProtocol.sc2api$InterfaceOptions
     {:raw true
      :feature-layer #:SC2APIProtocol.sc2api$SpatialCameraSetup
      {:width 24
       :resolution #:SC2APIProtocol.common$Size2DI{:x 84 :y 84}
       :minimap-resolution #:SC2APIProtocol.common$Size2DI{:x 64 :y 64}
       }
      :render #:SC2APIProtocol.sc2api$SpatialCameraSetup
      {:width 24
       :resolution #:SC2APIProtocol.common$Size2DI{:x 480 :y 270}
       :minimap-resolution #:SC2APIProtocol.common$Size2DI{:x 64 :y 64}
       }
      }}})
Out[9]:
{:join-game {:player-id 1}, :status :in-game}

After creating and joining a game we can run actions in the game and run for x amount of steps

Now we capture the observation data in a variable in the step function:

In [10]:
(do-sc2 conn
        (fn [observation _]
          (def observation observation)
          [])
        {:run-until-fn (run-for 1)})
Out[10]:
[#object[clojure.lang.PersistentVector$TransientVector 0x6f6bcf38 "clojure.lang.PersistentVector$TransientVector@6f6bcf38"] nil]

Say we would like to build an SCV worker unit, cljsc2 provides a rule:

(can-build ?name ?ability-id ?builder)

We give the current game observation data facts and have the query engine figure out all the possibilities:

In [11]:
(use 'cljsc2.clj.rules)

(let [unit-name "SCV"
      observation-facts (ds/db-with
                         knowledge-base
                         (obs->facts observation))
      builder-query '[:find ?builder ?ability-id
                      :in $ % ?name
                      :where
                      (can-build ?name ?ability-id ?builder)]
      builder-build-action (ds/q builder-query
                                 observation-facts
                                 can-build-rule
                                 unit-name)]
  builder-build-action)
Out[11]:
#{[4313841665 524]}

The unit tag and the ability id that we would need to build a worker SCV!

In [12]:
(ability-to-action [4313841665] 524)
Out[12]:
#:SC2APIProtocol.sc2api$Action{:action-raw #:SC2APIProtocol.raw$ActionRaw{:action #:SC2APIProtocol.raw$ActionRaw{:unit-command #:SC2APIProtocol.raw$ActionRawUnitCommand{:unit-tags [4313841665], :queue-command false, :ability-id 524, :target {}}}}}

To actually execute actions ingame we return a collection of these actions in from the stepping function:

In [13]:
(use 'cljsc2.clj.rendering)
In [14]:
(do-sc2
 conn
 (fn [observation _]
   [(ability-to-action [4313841665] 524)])
 {:run-until-fn (run-for 1)
  :collect-observations true})

Let it run for a bit to collect minerals.

In [ ]:
(do-sc2
 conn
 (fn [_ _]
   [])
 {:run-until-fn (run-for 300)
  :collect-observations true})

Here we do the same but for a supply depot, it also needs a location to be built at, so we find the command centers coordinates and build it 3 world units right of the command center.

In [16]:
(do-sc2
 conn
 (fn [observation connection]
   [(let [unit-name "SupplyDepot"
          observation-facts (ds/db-with
                             knowledge-base
                             (obs->facts observation))
          builder-build-action (-> '[:find ?builder ?ability-id
                                     :in $ % ?name
                                     :where
                                     (can-build ?name ?ability-id ?builder)
                                     ]
                                   (ds/q observation-facts
                                         can-build-rule
                                         unit-name))
          [builder-unit-tag ability-id] (first builder-build-action)
          [x y] (-> '[:find ?x ?y
                      :in $ % ?name
                      :where
                      (units-of-type ?name ?command-center-id)
                      [?command-center-id :unit/x ?x]
                      [?command-center-id :unit/y ?y]
                      ]
                    (ds/q observation-facts
                          units-of-type-rule
                          "CommandCenter")
                    first)
          ]
      (ability-to-action [builder-unit-tag] ability-id (+ x 3) y {}))]
   )
 {:run-until-fn (run-for 1)
  :collect-observations true})
Out[16]:
[#object[clojure.lang.PersistentVector$TransientVector 0x6956511d "clojure.lang.PersistentVector$TransientVector@6956511d"] nil]

Give it time to get some minerals for a bunch of barracks, and while we're at it build some extra workers:

to make sure the command centers only add one worker to the queue we check that they have less than 2 orders

In [17]:
(do-sc2
 conn
 (fn [observation _]
   (let [rules (concat can-build-rule
                       has-order-or-nil-order-rule)
         facts (ds/db-with knowledge-base
                           (obs->facts observation))
         builders (ds/q '[:find ?builder-unit-tag ?ability-id (count ?order)
                          :in $ % ?unit-name
                          :where
                          (can-build ?unit-name ?ability-id ?builder-unit-tag)
                          (has-order-or-nil-order ?builder-unit-tag ?order)
                          ]
                        facts
                        rules
                        "SCV")]
     (->> builders
          (filter (fn [[unit-tag ability-id order-count]]
                    (< order-count 2)))
          (map (fn [[unit-tag ability-id _]]
                 (ability-to-action [unit-tag] ability-id))))))
 {:run-until-fn (run-for 1200)
  :collect-observations :yes-please})

We saved up enough for three barracks, let us build them!

In [18]:
(do-sc2
 conn
 (fn [observation connection]
   (let [unit-name "Barracks"
         observation-facts (ds/db-with
                            knowledge-base
                            (obs->facts observation))
         builder-build-action (-> '[:find ?builder-tag ?ability-id
                                    :in $ % ?name
                                    :where
                                    (can-build ?name ?ability-id ?builder-tag)
                                    ]
                                  (ds/q observation-facts
                                        can-build-rule
                                        unit-name))
         [supply-depot-x supply-depot-y] (-> '[:find ?x ?y
                                               :in $ % ?name
                                               :where
                                               (units-of-type ?name ?unit-id)
                                               [?unit-id :unit/x ?x]
                                               [?unit-id :unit/y ?y]
                                               ]
                                             (ds/q observation-facts
                                                   units-of-type-rule
                                                   "SupplyDepot")
                                             first)
         ]
     (map-indexed (fn [i [builder-unit-tag ability-id]]
                    (ability-to-action [builder-unit-tag]
                                       ability-id
                                       (+ supply-depot-x 3)
                                       (+ supply-depot-y (* 3 i))
                                       {}))
                  (take 3 builder-build-action)))
   )
 {:run-until-fn (run-for 1)})
Out[18]:
[#object[clojure.lang.PersistentVector$TransientVector 0x7cc81f49 "clojure.lang.PersistentVector$TransientVector@7cc81f49"] nil]

Let's keep on building scv's and start building marines!

In [19]:
(defn build-thing [observation thing-name]
  (let [rules (concat can-build-rule
                      has-order-or-nil-order-rule)
        facts (ds/db-with knowledge-base
                          (obs->facts observation))
        builders (ds/q '[:find ?builder-unit-tag ?ability-id (count ?order)
                         :in $ % ?unit-name
                         :where
                         (can-build ?unit-name ?ability-id ?builder-unit-tag)
                         (has-order-or-nil-order ?builder-unit-tag ?order)
                         ]
                       facts
                       rules
                       thing-name)]
    (->> builders
         (filter (fn [[unit-tag ability-id order-count]]
                   (< order-count 2)))
         (map (fn [[unit-tag ability-id _]]
                (ability-to-action [unit-tag] ability-id))))))

(do-sc2
 conn
 (fn [observation _]
   (concat (build-thing observation "SCV")
           (build-thing observation "Marine")))
 {:run-until-fn (run-for 1200)
  :collect-observations :uhu})

We're getting close to supply capped, so lets make some more Supply Depots

In [20]:
(do-sc2
 conn
 (fn [observation connection]
   (let [unit-name "SupplyDepot"
         observation-facts (ds/db-with
                            knowledge-base
                            (obs->facts observation))
         builder-build-action (-> '[:find ?builder ?ability-id
                                    :in $ % ?name
                                    :where
                                    (can-build ?name ?ability-id ?builder)
                                    ]
                                  (ds/q observation-facts
                                        can-build-rule
                                        unit-name))
         [builder-unit-tag ability-id] (first builder-build-action)
         [x y] (-> '[:find ?x ?y
                     :in $ % ?name
                     :where
                     (units-of-type ?name ?command-center-id)
                     [?command-center-id :unit/x ?x]
                     [?command-center-id :unit/y ?y]
                     ]
                   (ds/q observation-facts
                         units-of-type-rule
                         "CommandCenter")
                   first)
         ]
     [(ability-to-action [builder-unit-tag] ability-id (+ x 3) (- y 2) {:queue-command true})
      (ability-to-action [builder-unit-tag] ability-id (+ x 3) (+ y 2) {:queue-command true})
      (ability-to-action [builder-unit-tag] ability-id (+ x 3) (+ y 4) {:queue-command true})])
   )
 {:run-until-fn (run-for 1)})
Out[20]:
[#object[clojure.lang.PersistentVector$TransientVector 0x3690cf34 "clojure.lang.PersistentVector$TransientVector@3690cf34"] nil]

Build more scvs and marines.

In [21]:
(do-sc2
 conn
 (fn [observation _]
   (concat (build-thing observation "SCV")
           (build-thing observation "Marine")))
 {:run-until-fn (run-for 1200)
  :collect-observations :yup})

We should have enough to teach this protoss enemy a lesson! We send our marines to attack at the bottom of the map with the attack ability (id 23 as shown at step 8 in this notebook)

In [22]:
(do-sc2
 conn
 (fn [observation connection]
   (let [unit-name "Marine"
         observation-facts (ds/db-with
                            knowledge-base
                            (obs->facts observation))
         [avg-x avg-y] (-> '[:find [(avg ?x) (avg ?y)]
                             :in $ % ?name
                             :where
                             (units-of-type ?name ?marine-e-id)
                             [?marine-e-id :unit/x ?x]
                             [?marine-e-id :unit/y ?y]
                             ]
                           (ds/q observation-facts
                                 units-of-type-rule
                                 unit-name))
         marine-unit-tags (-> '[:find [?unit-tag ...]
                                :in $ % ?name
                                :where
                                (units-of-type ?name ?unit-tag)
                                ]
                              (ds/q observation-facts
                                    units-of-type-rule
                                    unit-name))]
     (map (fn [unit-tag] (ability-to-action unit-tag 23 (+ avg-x 15) (- avg-y 50) {}))
           marine-unit-tags))
   )
 {:run-until-fn (run-for 300)
  :collect-observations true})

I haven't yet implemented camera action follow features, but trust me when I say these marines win the immensely difficult easy built in AI protoss..