Skip to content

Commit

Permalink
Merge pull request #33 from dysonreturns/docs-tut8
Browse files Browse the repository at this point in the history
docs: adding tutorials 7 and 8 along with some refactors on existing …
  • Loading branch information
dysonreturns committed Apr 9, 2024
2 parents 0f7a509 + 5798169 commit fff663f
Show file tree
Hide file tree
Showing 17 changed files with 1,002 additions and 79 deletions.
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ class MyCleverBot < Sc2::Player::Bot
end
end
```
The `on_step` method executes whenever the game stepped forward. The very first game loop, we send all our workes to attack the enemy's start position.
The `on_step` method executes whenever the game stepped forward. The very first game loop, we send all our workers to attack the enemy's start position.

**The Match:**
To run a match, you can simply create a file which includes your bot and executes it the following way:
Expand Down
24 changes: 13 additions & 11 deletions docs/TUTORIAL_05.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Therefore, it's worth keeping a continuous eye on our refineries to ensure that
After our construction code, we add:
```ruby
# Gas saturation checks @ every 2s
if game_loop % 32 == 0
if game_loop % 44 == 0

# Get all gas structures for main base
gasses = geo.gas_for_base(main_base)
Expand All @@ -122,34 +122,36 @@ end

Now, when we execute our bot, the green starts pouring in. Let's review.

#### Prioritize tasks
#### Prioritize tasks using time
All code we execute comes at a performance cost.
It would be convenient to check everything on every frame, but we must always be mindful of priority.

This next condition limits how frequently refinery saturation is checked. Instead of every frame, let's do once every 2 in-game seconds.
`1 in-game second = 16 frames`
`2 in-game seconds = 32 frames`, so check that `game_loop` is divisible by `32`.
`1 in-game second = 22.4 frames`
`2 in-game seconds = 44.8 frames`, so check that `game_loop` is divisible by `44`.


```ruby
if game_loop % 32 == 0
if game_loop % 44 == 0
#...
end
```

It works for us, because our `@step_count` is `2` (we step forward two frames at a time), so we run on all even numbers for `game_loop`.
It works for us, because our `@step_count` is `2` (we step forward two frames at a time), so we run on all even numbers for `game_loop`.
But to be fair `44.8` is closer to `45`, but that math doesn't work with our even numbers.

The danger though, is that the steps we take might sometimes be variable.
Another danger is that the steps we take might sometimes be variable.
In realtime mode (Bot vs Human), **steps counts change unpredictably**.

To have finer control and also to be less rigid, we should rather implement a time-difference method.
To have finer control and also to be less rigid, we should rather implement a time-difference (delta time) method.

Here is an approach which remembers- and compares how many frames have passed since the last saturation check:
```ruby
# Save the game_loop on which we last checked (initially zero)
@last_saturation_check ||= 0

# If 32 frames have passed since our last check
if game_loop - @last_saturation_check >= 32
if game_loop - @last_saturation_check >= 45

# Perform logic...

Expand All @@ -158,8 +160,8 @@ if game_loop - @last_saturation_check >= 32
end
```

That's two more lines of code, but our bot is more flexible.
`32` frames can be 31, 33, anything and our step count can be variable.
That's two more lines of code, but our bot is more flexible.
We can check any frame distance and step any length.
This code is safe for realtime mode.


Expand Down
36 changes: 19 additions & 17 deletions docs/TUTORIAL_06.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
⚠️ 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.
The equation below is to help (re)introduce you to theory crafting. First and last time, promise.

<hr />
<div class="docstring">
Expand All @@ -26,7 +26,7 @@ Each building can choose one of two <strong> add-on</strong> structures, which e
- 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/>
A <strong>Command Centre</strong> may be upgraded converted into a defensive structure with ground attack or a utility structure which grants vision. 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/>
Expand All @@ -36,11 +36,11 @@ The army can utilize two regenerative mechanics:<br/>
</div>
</div>

## Army Production
## Theorycrafting

### What is our goal?

To start simple, will only build Biological units.
To start simple, we'll 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.
Expand Down Expand Up @@ -86,20 +86,22 @@ 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.


## Army Production

### Building Barracks

```ruby
def on_step
# ...

# Build barracks once we have a completed depot, up to a max of 3 per base
# Build barracks once we have a completed depot, up to a max of 3
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
barracks_num_desired = 3

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

# ensure we can afford it
break unless can_afford?(unit_type_id: Api::UnitTypeId::BARRACKS)
Expand Down Expand Up @@ -128,11 +130,13 @@ There are many ways to check completion, but here we choose {Api::Unit#is_comple
Any boolean from Unit that doesn't need parameters can be used in this short form (`is_flying?`, `is_mechanical?`, etc.).

```ruby
barracks_num_desired = 3

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

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

break unless can_afford?(unit_type_id: Api::UnitTypeId::BARRACKS)
# Build ...
Expand Down Expand Up @@ -184,7 +188,7 @@ barracks = barracks.select(&:is_completed?) # only focussing on completed barrac
# 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)
barracks.random.build_tech_lab if can_afford?(unit_type_id: Api::UnitTypeId::TECHLAB)
else
# For the rest, we add reactors

Expand All @@ -205,7 +209,7 @@ Then, start off by building the Tech Lab first, which means we simply add reacto
```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)
barracks.random.build_tech_lab if can_afford?(unit_type_id: Api::UnitTypeId::TECHLAB)
else
# reactors..,.
end
Expand All @@ -227,7 +231,7 @@ end #=> the first barrack which has an idle tech lab
```

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

Reactors have a similar method in `#build_reactor`.
```ruby
Expand Down Expand Up @@ -263,7 +267,7 @@ Reminder: `train` and `build` are aliases of each other, should you prefer to `b
</div>
</div>


Finally, add unit training code:
```ruby

barracks.each do |barrack|
Expand Down Expand Up @@ -292,8 +296,6 @@ barracks.each do |barrack|
end
```

There are no new methods here, let's just walk over the steps.

Remember that `barracks` was filtered down to only the completed structures.
For each, we make sure that we have add-ons and that they are completed, because units can not train during their construction.

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

# Terran

<div class="docstring">
<div class="note">
<strong>Glossary</strong><br/>
<br/>
<strong>Expo</strong>: Short for "expansion". A location where you would- or already have established a base.<br /><br/>
<strong>Natural Expansion</strong>: The most likely first expansion you would take. Typically it's nearest to the main base, while also separating your main and your enemy.<br />
<br/>
</div>
</div>

## Rallying

All of our new army units are piling up around the Barracks.
We can set rally points for structures to make emerging units go a target position.
Let's rally all the Barracks to the entrance of our base.

Once we have a few units in hand and we'll send them across the map.

### Where to go

For a rally point, let's use our natural expansion location and then take a few steps forward in the direction of our enemy's base.
This way, the army isn't blocking ourselves when we decide to expand and defends our entrance.

#### Expansion points
The first step is finding our natural expansion.
`geo.expansion_points` holds a list of all expansion points (where you can build a base structure).
We'll learn more about `geo.expansions`, `geo.expansion_points` and `geo.expansions_unoccupied` later.
For now, here is some code you can run to visualize these points in-game:

```ruby
def on_step
#...

# Debug: Draw spheres and labels for each expansion point
geo.expansion_points.each do |point|
point = Api::Point[point.x, point.y, geo.terrain_height_for_pos(point)]
debug_text_world("x: #{point.x}, y: #{point.y}", point: point)
debug_draw_sphere(point: point)
end
end
```

#### Natural position

From these points, we can detect which one is at our natural using a simple distance check:

```ruby
geo.expansion_points.min_by(2) { |p| p.distance_to(geo.start_position) }.last
```

Each expansion point is compared our start position (`geo.start_position`) and the nearest 2 are picked using a `Array#min_by`.
Of these two points, the first will be our main base's position and the second nearest is the natural base. So, we pick the latter with `.last`.

You can get the distance between any two things using `distance_to`. This is true for both Units and Positions.

#### A step in the right direction

From the natural we wish to move a few squares, say `8`, in the general direction of our enemy.

Both `Point` (3D) and `Point2D` share a module `Sc2::Position`, which provide geometry helpers such as `towards` (and `distance_to` we used above).
`towards` takes the current position and moves it by a `distance` in the direction of an`other` position.

```ruby
new_position = one_position.towards(another_position, distance)
```

By using a relative method like `towards`, this equation works regardless of which side of the map you get spawned.

⌖ Explore {Sc2::Position}'s methods first when dealing with such matters as positioning. Auto-complete will guide you too.

Let's see everything in action:
```ruby
def on_step
# ...

# Get a rally point and save it, since it's expensive.
if @army_rally_point.nil?
# Calculate our natural's position
natural_expansion_pos = geo.expansion_points
.min_by(2) { |point| point.distance_to(geo.start_position) }
.last

# 8 squares forward from the natural, in the direction of our enemy
@army_rally_point = natural_expansion_pos.towards(geo.enemy_start_position, 8)
end
end
```

#### Set the rally

```ruby
def on_step
# @army_rally_point = nat...

barracks.each do | barrack|
barrack.smart(target: @army_rally_point) unless barrack.rally_targets.size > 0
end
end
```

We loop over the barracks and if they don't have a `rally_targets` set, we set one.

If you look at full exercise, there is debug code which prints spheres at all the location points with their coordinates.
It also marks out our rally point and it's coordinates.
[07_terran_attack.rb](https://github.com/dysonreturns/sc2ai/blob/main/docs/examples/07_terran_attack.rb)


![rallly_point](images/07_rally_point.jpg)
### Attack

Add the code below if you've been following along or check out and run [07_terran_attack.rb](https://github.com/dysonreturns/sc2ai/blob/main/docs/examples/06_terran_army.rb).

After assembling a small army of, say, a dozen units, send everyone to attack to enemy's start position:

```ruby
def on_step
#...
# barracks.each do rally...

# As soon as we have a few units, attack that enemy's starting position
if units.army.size >= 12
units.army.attack(target: geo.enemy_start_position)
end

end
```

Look at those little guys go!
Sometimes the enemy dares to send a few units across to our base, but we are read and waiting at the entrance.

Run the simulation a few times to see the varied outcomes.
There are some obvious things we can fix to improve our chances.

---

The Barracks enabled a tech upgrade to our Command Centre and we haven't used the Tech Lab for upgrades yet.
Let's get the upgrades and see our new tools in action.

---

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

0 comments on commit fff663f

Please sign in to comment.