Skip to content

Commit

Permalink
Merge pull request #27 from dysonreturns/docs-tutorial6
Browse files Browse the repository at this point in the history
docs: adding tutorial6 and fixing tut5 segue.
  • Loading branch information
dysonreturns committed Mar 20, 2024
2 parents d081a04 + 9f9e9bd commit b495806
Show file tree
Hide file tree
Showing 5 changed files with 521 additions and 5 deletions.
35 changes: 32 additions & 3 deletions docs/TUTORIAL_05.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,13 +235,42 @@ gas.nearest(units: units.workers, amount: 5)
.each { |worker| worker.smart(target: gas) }
```


## Enough is enough

You may have noticed that we fill our whole supply with workers. We should leave room for some army units.
Using the harvester methods above, we can prevent out main base from over-producing workers.

Previously, we simply checked if we can afford workers:
```ruby
if can_afford?(unit_type_id: Api::UnitTypeId::SCV)
#... build SCV
end

```
Going forward, only construct new workers if we have `missing_harvesters`.
```ruby

if main_base.missing_harvesters > 0 && can_afford?(unit_type_id: Api::UnitTypeId::SCV)
# ... build SCV
end
```
This is not 100% as sometimes:

- a worker goes off to construct something and
- the harvester count drops,
- which causes a new worker to be added
- and then the constructor returns to find his position filled,
- which over-saturates the base.

But, this code is solid enough enough for most cases and we will distribute workers evenly later.

---

Now we're cooking with gas. Download and run the full example and see it in action:
Download and run the full example and see it in action:
[05_terran_gas.rb](https://github.com/dysonreturns/sc2ai/blob/main/docs/examples/05_terran_gas.rb)

So we have one base up and running, but we're clearly over-saturating on workers per mineral patch. We need more bases.
Are you thinking what I'm thinking? Time to take over the map!
We're ready to arm ourselves. 🚬 Heck, it's about time.

---

Expand Down
312 changes: 312 additions & 0 deletions docs/TUTORIAL_06.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
# TUTORIAL 5

# Terran

⚠️ If you know the game well, you can skip the primer and the math and get straight to [building barracks](#label-Building+Barracks).
It is important to be methodical as we are still building a system, even if inside a game engine. Tiny lives are at stake! :P
The equations below is to help (re)introduce you to theory crafting. First and last time, promise.

<hr />
<div class="docstring">
<div class="note">
<strong>Military primer</strong><br/>

<br/>


The Terran production is standard RTS format:<br/>
You queue a unit at a production facility at a cost. One unit from the queue is trained at a time. A new unit appears when completed.<br/>
Optimally, unit production is <strong> constant</strong> - providing consistent growth.<br/>
<br/>
Three production buildings distinctly produce: 💪Biological, 🦾Mechanical and 🚀Air units.<br/>
<br/>
Each building can choose on of two <strong> add-on</strong> structures, which either<br/>
- allows two units to be built at once (<strong> Reactor</strong> 2️⃣) or<br/>
- provides additional tech (<strong> Tech Lab</strong> 🧪).<br/>
These are mini-structures which attach on the side of the main building.<br/>
<br/>
A <strong>Command Centre</strong> may be upgraded converted into a defensive structure with ground attack. A <strong> Bunker</strong> structure can load biological units while offering additional safety. A Turret is an anti-air defense structure.<br/>
<br/>
The army can utilize two regenerative mechanics:<br/>
- <strong> Repair</strong> (mechanical) via SCV workers and<br/>
- <strong> Heal</strong> (biological) via flying Medivac army units.<br/>


</div>
</div>

## Army Production

### What is our goal?

To start simple, will only build Biological units.
We will construct Barracks to produce Marines as our main army.

We happen to know that adding a Tech Lab add-on provides upgrades for our Marines, so we will add one of those.
We also know that a Reactor allows Barracks to double it's output, so let's use that too.

When adding a Tech Lab to a Barrack, we get access to a heavier unit, the Marauder, so from that one Barrack we will build this unit.
For the rest of the Barracks, we want Reactors to produce Marines two at a time.

```
1 x Barracks + Tech Lab
N x Barracks + Reactor
```
How many is N and what can we afford?

### How many Barracks

Let's work out minerals per minute for income vs production and solve.
We ignore gas requirements to keep things simple, but in many cases you rather want to consider gas as the restriction.

```ruby

<<~NAPKIN_MATHEMATICS
1x Base without tricks gives roughly 925 Minerals per minute (googled)
Marauder costs 100M and builds in 21s (googled)
Marine costs 50M and builds in 18s (googled)
Now normalize everything to Minerals per minute:
100M / 21s * 60s = 285.714285714285714 Mpm Marauder cost
50M / 18s * 60s = 166.666666666666667 Mpm Marine cost
Subtract our constant Marauder production cost from total income to see remaining income
925 - 285.714285714285714 = 639.2857142857143 Mpm income
Then divide our Marine cost per minute into that to see how many Marines per minute we can build
639.2857142857143 / 166.666666666666667 = 3.835714285714285 Marines per minute
Therefore our base needs 3x Barracks:
1x Barrack with a Tech Lab (builds one Marauder at a time)
2x Barracks with Reactors (builds 2 Marines at a time) = 4 marines per minute
NAPKIN_MATHEMATICS
```
This kind of exercise you can do any time. It is within your power and to your own benefit.
You'll learn most unit costs and stats by heart over time.


### Building Barracks

```ruby
def on_step
# ...

# Build barracks once we have a completed depot, up to a max of 3 per base
barracks = structures.select_type(Api::UnitTypeId::BARRACKS)
depots = structures.select_type(Api::UnitTypeId::SUPPLYDEPOT).select(&:is_completed?)

if depots.size > 0 && barracks.size < 3
(3 - barracks.size).times do

# ensure we can afford it
break unless can_afford?(unit_type_id: Api::UnitTypeId::BARRACKS)

builder = units.workers.random

# Use 6-spaced build blocks for a 3x3 structure + a 2x2 add-on + 1 open square for walking
# 3 + 2 + 1 = 6
build_location = geo.build_placement_near(length: 6, target: main_base, random: 3)
builder.build(unit_type_id: Api::UnitTypeId::BARRACKS, target: build_location)
builder.smart(target: geo.minerals_for_base(main_base).random, queue_command: true)
end
end

end
```

There are no new API concepts here.

```ruby
barracks = structures.select_type(Api::UnitTypeId::BARRACKS)
depots = structures.select_type(Api::UnitTypeId::SUPPLYDEPOT).select(&:is_completed?)
```
We select our barracks (initially zero) and _completed_ supply depots.
There are many ways to check completion, but use {Api::Unit#is_completed? Unit#is_completed?} in a short-hand `select` for it's brevity.
Any boolean from Unit that doesn't need parameters can be used in this short form (`is_flying?`, `is_mechanical?`, etc.).

```ruby
# If missing anything...
if depots.size > 0 && barracks.size < 3

# That many times...
(3 - barracks.size).times do

break unless can_afford?(unit_type_id: Api::UnitTypeId::BARRACKS)
# Build ...
end

end
```

3 minus our barrack count will tell us how many are missing.
For however many are missing, start construction if we can afford it.

Building is a familiar sequence of code:
```ruby
builder = units.workers.random

# Use 6-spaced build blocks for a 3x3 structure + a 2x2 add-on + 1 open square for walking
# 3 + 2 + 1 = 6
build_location = geo.build_placement_near(length: 6, target: main_base, random: 3)
builder.build(unit_type_id: Api::UnitTypeId::BARRACKS, target: build_location)
builder.smart(target: geo.minerals_for_base(main_base).random, queue_command: true)

```

Send a random worker to build on a 6x6 spaced grid near the main base and then continue mining.

### 7x7 sim cities

Barracks are `3x3`, so you'd think it'd need a `4`-spaced building point to allow `1` space for units to walk around (`3+1`).
The add-on adds `2x2` to it's shape, though, effectively making it's size `5x5`.

Therefore,
💪Barracks are best spaced `6` squares.
🦾Factories which produce massive mechanical units might need `7`.
🚀Starports which doesn't really need walking space for it's own units can take up 5 or `6` spaces.

The Problem is packing your Barracks too tightly might limit the movement of your Factory units later!

**Universally using 7-spaced grid** for all army building placements takes away all worries of trapping units, but you can run out of space in your main base faster than usual.

### Add-on

Back to the Barracks, lets build those extensions we planned around:
```ruby

# Build add-ons to completed barracks
barracks = barracks.select(&:is_completed?) # only focussing on completed barracks...

# If we can't find a barracks with a tech lab
if barracks.size > 0 && !barracks.find(&:has_tech_lab?)
# Build a tech lab
barracks.random.build_tech_lab if can_afford?(unit_type_id: Api::UnitTypeId::BARRACKSTECHLAB)
else
# We have at least one tech lab already, for the rest we add reactors

# Select without add_on == Reject where add_on present
barracks.reject(&:add_on).each do |barrack|
break unless can_afford?(unit_type_id: Api::UnitTypeId::REACTOR)

# Build a reactor
barrack.build_reactor
end
end

```

We filter our group of barracks to hold only completed structures for this exercise.
Then, start off by building the Tech Lab first, which means we simply add reactors to everything which follows.

```ruby
if barracks.size > 0 && !barracks.find(&:has_tech_lab?)
# Build a tech lab
barracks.random.build_tech_lab if can_afford?(unit_type_id: Api::UnitTypeId::BARRACKSTECHLAB)
else
# reactors..,.
end
```
#### find a unit

We have done a lot of filters with `select`, which narrows down a group of units, but how about finding a single specific unit?
The unit group `find` method returns the first Unit for which the block returns a truthy value (or `nil` if not found).

```ruby
barrack_with_tech_lab = barracks.find(&:has_tech_lab?) #=> the first barrack where has_tech_lab? is true

# Block syntax for more complex find
barrack_with_idle_tech_lab = barracks.find do |barrack|
barrack.has_tech_lab? && !barrack.is_active?
end #=> the first barrack which has an idle tech lab
```

#### build_tech_lab / build_reactor
The `#build_tech_lab` ability is simply a helper which executes `#build` with the correctly detected type, i.e. `Api::UnitTypeId::BARRACKSTECHLAB`.

Reactors have a similar method in `#build_reactor`.
```ruby
# else...

barracks.reject(&:add_on).each do |barrack|
break unless can_afford?(unit_type_id: Api::UnitTypeId::REACTOR)

barrack.build_reactor
end
```
Both of these supports any type of building (Barracks/Factories/Starports).

#### add_on
On a production building, you can call `#add_on`, which returns the Unit if present or `nil` if there is no add-on.
This access will be useful later as our Tech Lab has some research buttons we wish to press!

We use the fact that it returns `nil` with the `reject` filter, to get only barracks which don't have add-ons.
If you find it clearer, you can remove the short-hand and filter inside such a loop instead:
```ruby
barracks.each do |barrack|
next unless barrack.add_on.nil?

# ... build a add-on
end
```

### Train an army

All according to plan.

```ruby

barracks.each do |barrack|
# Ensure we have an add-on and that it's completed
next unless barrack.add_on && barrack.add_on.is_completed?

# If we have a tech lab, build 1x MARAUDER, if a reactor, then 2x MARINES
if barrack.has_tech_lab?
unit_type_to_train = Api::UnitTypeId::MARAUDER
quantity = 1
else
unit_type_to_train = Api::UnitTypeId::MARINE
quantity = 2
end

# If our orders are empty or near completion...
if barrack.orders.size == 0 || barrack.orders.size <= 2 && barrack.orders.any? { |order| order.progress > 0.9 }
# Send the train command quantity times.
quantity.times do
# Note queue_command is true for the reactor, because multiple actions on the same frame overwrite each other.
barrack.train(unit_type_id: unit_type_to_train, queue_command: true)
end
end
end
```


Remember that `barracks` are all the completed barracks.
We additionally make sure that we have add-ons and that they are completed.

As from our goals, the Tech Lab barrack builds the heavier Marauder unit (one at a time) and the Reactor barracks build marines (two at a time).

- We set these in `quantity` and `unit_type_to_train` and then do our usual production limit checks.
- If the barrack has no orders or has two or less which are nearly done, then...
- For `quanity` amount of times...
- Queue a train command for the unit type id, `unit_type_to_train`.


![heck_yeah](images/06_train_army.jpg)

And there they are! We did it, we came up with a plan and executed on it!

The full example is here if you'd like to run it exactly:
[06_terran_army.rb](https://github.com/dysonreturns/sc2ai/blob/main/docs/examples/06_terran_army.rb)

---

They do look a bit awkward standing next to the barracks like that.
Next, let's rally these troops to a defensive position.
Then, once we reach critical mass, we'll send them out to attack!

---

{file:docs/TUTORIAL_07.md Next ➡️}

3 changes: 1 addition & 2 deletions docs/examples/05_terran_gas.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ def on_step
return if main_base.nil?

# 02 - Construct an SCV
if can_afford?(unit_type_id: Api::UnitTypeId::SCV)

if main_base.missing_harvesters > 0 && can_afford?(unit_type_id: Api::UnitTypeId::SCV)
workers_in_progress = main_base.orders.size
if workers_in_progress == 0
# Scenario 1: Queue is empty, lets build
Expand Down

0 comments on commit b495806

Please sign in to comment.