Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/test-go-agent.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Test Go Starter Kit

# Public repo: this runs on GitHub-hosted runners (ubuntu-latest), never the
# self-hosted homeserver runners, so untrusted fork PRs can't execute on our
# infrastructure.
#
# The engine's JSON schema is the source of truth for the wire protocol. This
# workflow validates the Go starter kit's fixtures against that schema, and is
# triggered whenever the schema changes — so a protocol update to the engine
# surfaces any drift in the Go kit immediately.

on:
push:
branches:
- master
pull_request:
branches:
- master
paths:
- "agents/go/**"
- "engine/bomberland-engine/src/runtime-validation/validation.schema.json"
- ".github/workflows/test-go-agent.yaml"
workflow_dispatch:

concurrency:
group: test-go-agent-${{ github.ref }}
cancel-in-progress: true

env:
ENGINE_SCHEMA: engine/bomberland-engine/src/runtime-validation/validation.schema.json

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: stable
cache-dependency-path: agents/go/go.sum

# The engine schema is the source of truth. Fail if the kit's bundled copy has
# drifted from it — that copy is what the Docker build and standalone clones use,
# since they don't have the engine tree available.
- name: Check bundled schema is in sync with engine
run: |
if ! cmp -s "$ENGINE_SCHEMA" agents/go/validation.schema.json; then
echo "::error::agents/go/validation.schema.json is out of sync with $ENGINE_SCHEMA"
echo "Re-sync with: cp $ENGINE_SCHEMA agents/go/validation.schema.json"
diff -u agents/go/validation.schema.json "$ENGINE_SCHEMA" || true
exit 1
fi

# Validate fixtures against the engine's live schema (not just the bundled copy),
# so fixture/protocol drift fails CI even before the copy is re-synced.
- name: Run Go tests against engine schema
working-directory: agents/go
env:
BOMBERLAND_SCHEMA_PATH: ${{ github.workspace }}/engine/bomberland-engine/src/runtime-validation/validation.schema.json
run: go test ./... -count=1
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ docker-compose up --abort-on-container-exit --force-recreate
| Python3-gym-wrapper | [Link](https://github.com/CoderOneHQ/bomberland/tree/master/agents/python3) | Open AI Gym wrapper | ✅ | Coder One |
| TypeScript | [Link](https://github.com/CoderOneHQ/bomberland/tree/master/agents/typescript) | Basic TypeScript starter | ✅ | Coder One |
| TypeScript-fwd | [Link](https://github.com/CoderOneHQ/bomberland/tree/master/agents/typescript) | Includes example for using forward model simulator | ❌ | Coder One |
| Go | [Link](https://github.com/CoderOneHQ/bomberland/tree/master/agents/go) | Basic Go starter | | [dtitov](https://github.com/dtitov) |
| Go | [Link](https://github.com/CoderOneHQ/bomberland/tree/master/agents/go) | Basic Go starter | | [dtitov](https://github.com/dtitov) |
| C++ | [Link](https://github.com/CoderOneHQ/bomberland/tree/master/agents/cpp) | Basic C++ starter | ✅ | [jfbogusz](https://github.com/jfbogusz) |
| Rust | [Link](https://github.com/CoderOneHQ/bomberland/tree/master/agents/rust) | Basic Rust starter | ❌ | [K-JBoon](https://github.com/K-JBoon) |

Expand Down
22 changes: 17 additions & 5 deletions agents/go/bomberland/game_state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,26 @@ import (
"encoding/json"
"github.com/stretchr/testify/assert"
"github.com/xeipuuv/gojsonschema"
"os"
"path/filepath"
"testing"
)

func TestJSONSchema(t *testing.T) {
abs, err := filepath.Abs("../validation.schema.json")
// The engine's schema is the source of truth. CI points BOMBERLAND_SCHEMA_PATH at
// engine/bomberland-engine/src/runtime-validation/validation.schema.json so fixtures are
// validated against the live engine contract; the bundled copy is the fallback used by the
// Docker build and standalone clones (which don't have the engine tree available).
schemaPath := os.Getenv("BOMBERLAND_SCHEMA_PATH")
if schemaPath == "" {
schemaPath = "../validation.schema.json"
}
abs, err := filepath.Abs(schemaPath)
assert.NoError(t, err)
schemaLoader := gojsonschema.NewReferenceLoader("file://" + abs)
// Validate against the ServerPacket union specifically. The document root only holds
// `definitions` (no root constraints), so referencing it directly would accept anything;
// the fragment pins validation to the real server->client packet shapes.
schemaLoader := gojsonschema.NewReferenceLoader("file://" + abs + "#/definitions/ServerPacket")
tests := []struct {
name string
payload interface{}
Expand Down Expand Up @@ -161,10 +173,10 @@ func createMockTickPacket(t *testing.T, tickNumber int, events interface{}) map[
func getMockState(t *testing.T) interface{} {
var mockState interface{}
err := json.Unmarshal([]byte(`
{"agents": {"a": {"agent_id": "a", "unit_ids": ["c", "e", "g"]}, "b": {"agent_id": "b", "unit_ids": ["d", "f", "h"]}}, "unit_state": {"c": {"coordinates": [3, 10], "hp": 3, "inventory": {"bombs": 3}, "blast_diameter": 3, "unit_id": "c", "agent_id": "a", "invulnerability": 0}, "d": {"coordinates": [11, 10], "hp": 3, "inventory": {"bombs": 3}, "blast_diameter": 3, "unit_id": "d", "agent_id": "b", "invulnerability": 0}, "e": {"coordinates": [1, 9], "hp": 3, "inventory": {"bombs": 3}, "blast_diameter": 3, "unit_id": "e", "agent_id": "a", "invulnerability": 0}, "f": {"coordinates": [13, 9], "hp": 3, "inventory": {"bombs": 3}, "blast_diameter": 3, "unit_id": "f", "agent_id": "b", "invulnerability": 0}, "g": {"coordinates": [12, 7], "hp": 3, "inventory": {"bombs": 3}, "blast_diameter": 3, "unit_id": "g", "agent_id": "a", "invulnerability": 0}, "h": {"coordinates": [2, 7], "hp": 3, "inventory": {"bombs": 3}, "blast_diameter": 3, "unit_id": "h", "agent_id": "b", "invulnerability": 0}}, "entities": [{"created": 0, "x": 11, "y": 7, "type": "m"}, {"created": 0, "x": 3, "y": 7, "type": "m"}, {"created": 0, "x": 10, "y": 11, "type": "m"}, {"created": 0, "x": 4, "y": 11, "type": "m"}, {"created": 0, "x": 11, "y": 2, "type": "m"}, {"created": 0, "x": 3, "y": 2, "type": "m"}, {"created": 0, "x": 4, "y": 8, "type": "m"}, {"created": 0, "x": 10, "y": 8, "type": "m"}, {"created": 0, "x": 14, "y": 14, "type": "m"}, {"created": 0, "x": 0, "y": 14, "type": "m"}, {"created": 0, "x": 13, "y": 13, "type": "m"}, {"created": 0, "x": 1, "y": 13, "type": "m"}, {"created": 0, "x": 14, "y": 0, "type": "m"}, {"created": 0, "x": 0, "y": 0, "type": "m"}, {"created": 0, "x": 2, "y": 3, "type": "m"}, {"created": 0, "x": 12, "y": 3, "type": "m"}, {"created": 0, "x": 3, "y": 14, "type": "m"}, {"created": 0, "x": 11, "y": 14, "type": "m"}, {"created": 0, "x": 0, "y": 1, "type": "m"}, {"created": 0, "x": 14, "y": 1, "type": "m"}, {"created": 0, "x": 5, "y": 2, "type": "m"},
{"game_id": "dev-game", "agents": {"a": {"agent_id": "a", "unit_ids": ["c", "e", "g"]}, "b": {"agent_id": "b", "unit_ids": ["d", "f", "h"]}}, "unit_state": {"c": {"coordinates": [3, 10], "hp": 3, "inventory": {"bombs": 3}, "blast_diameter": 3, "unit_id": "c", "agent_id": "a", "invulnerable": 0, "stunned": 0}, "d": {"coordinates": [11, 10], "hp": 3, "inventory": {"bombs": 3}, "blast_diameter": 3, "unit_id": "d", "agent_id": "b", "invulnerable": 0, "stunned": 0}, "e": {"coordinates": [1, 9], "hp": 3, "inventory": {"bombs": 3}, "blast_diameter": 3, "unit_id": "e", "agent_id": "a", "invulnerable": 0, "stunned": 0}, "f": {"coordinates": [13, 9], "hp": 3, "inventory": {"bombs": 3}, "blast_diameter": 3, "unit_id": "f", "agent_id": "b", "invulnerable": 0, "stunned": 0}, "g": {"coordinates": [12, 7], "hp": 3, "inventory": {"bombs": 3}, "blast_diameter": 3, "unit_id": "g", "agent_id": "a", "invulnerable": 0, "stunned": 0}, "h": {"coordinates": [2, 7], "hp": 3, "inventory": {"bombs": 3}, "blast_diameter": 3, "unit_id": "h", "agent_id": "b", "invulnerable": 0, "stunned": 0}}, "entities": [{"created": 0, "x": 11, "y": 7, "type": "m"}, {"created": 0, "x": 3, "y": 7, "type": "m"}, {"created": 0, "x": 10, "y": 11, "type": "m"}, {"created": 0, "x": 4, "y": 11, "type": "m"}, {"created": 0, "x": 11, "y": 2, "type": "m"}, {"created": 0, "x": 3, "y": 2, "type": "m"}, {"created": 0, "x": 4, "y": 8, "type": "m"}, {"created": 0, "x": 10, "y": 8, "type": "m"}, {"created": 0, "x": 14, "y": 14, "type": "m"}, {"created": 0, "x": 0, "y": 14, "type": "m"}, {"created": 0, "x": 13, "y": 13, "type": "m"}, {"created": 0, "x": 1, "y": 13, "type": "m"}, {"created": 0, "x": 14, "y": 0, "type": "m"}, {"created": 0, "x": 0, "y": 0, "type": "m"}, {"created": 0, "x": 2, "y": 3, "type": "m"}, {"created": 0, "x": 12, "y": 3, "type": "m"}, {"created": 0, "x": 3, "y": 14, "type": "m"}, {"created": 0, "x": 11, "y": 14, "type": "m"}, {"created": 0, "x": 0, "y": 1, "type": "m"}, {"created": 0, "x": 14, "y": 1, "type": "m"}, {"created": 0, "x": 5, "y": 2, "type": "m"},
{"created": 0, "x": 9, "y": 2, "type": "m"}, {"created": 0, "x": 9, "y": 6, "type": "m"}, {"created": 0, "x": 5, "y": 6, "type": "m"}, {"created": 0, "x": 11, "y": 13, "type": "m"}, {"created": 0, "x":
3, "y": 13, "type": "m"}, {"created": 0, "x": 2, "y": 10, "type": "m"}, {"created": 0, "x": 12, "y": 10, "type": "m"}, {"created": 0, "x": 2, "y": 13, "type": "m"}, {"created": 0, "x": 12, "y": 13, "type": "m"}, {"created": 0, "x": 12, "y": 6, "type": "m"}, {"created": 0, "x": 2, "y": 6, "type": "m"}, {"created": 0, "x": 1, "y": 10, "type": "m"}, {"created": 0, "x": 13, "y": 10, "type": "m"}, {"created": 0, "x": 1, "y": 6, "type": "m"}, {"created": 0, "x": 13, "y": 6, "type": "m"}, {"created": 0, "x": 9, "y": 12, "type": "m"}, {"created": 0, "x": 5, "y": 12, "type": "m"}, {"created": 0, "x": 1, "y": 0, "type": "m"}, {"created": 0, "x": 13, "y": 0, "type": "m"}, {"created": 0, "x": 14, "y": 2, "type": "m"}, {"created": 0, "x": 0, "y": 2, "type": "m"}, {"created": 0, "x": 10, "y": 1, "type": "m"}, {"created": 0, "x": 4, "y": 1, "type": "m"}, {"created": 0, "x": 11, "y": 8, "type": "m"}, {"created": 0, "x": 3, "y": 8, "type": "m"}, {"created": 0, "x": 0, "y": 6, "type": "m"}, {"created": 0, "x": 14, "y": 6, "type": "m"}, {"created": 0, "x": 12, "y": 5, "type": "m"}, {"created": 0, "x": 2, "y": 5, "type": "m"}, {"created": 0, "x": 1, "y": 3, "type": "w", "hp": 1}, {"created": 0, "x": 13, "y": 3, "type": "w", "hp": 1}, {"created": 0, "x": 1, "y": 8, "type": "w", "hp": 1}, {"created": 0, "x": 13, "y": 8, "type": "w", "hp": 1}, {"created": 0, "x": 0, "y": 8, "type": "w", "hp": 1}, {"created": 0, "x": 14, "y": 8, "type": "w", "hp": 1}, {"created": 0, "x": 6, "y": 3, "type": "w", "hp": 1}, {"created": 0, "x": 8, "y": 3, "type": "w", "hp": 1}, {"created": 0, "x": 6, "y": 1, "type": "w", "hp": 1}, {"created": 0, "x": 8, "y": 1, "type": "w", "hp": 1}, {"created": 0, "x": 9, "y": 1, "type": "w", "hp": 1}, {"created": 0, "x": 5, "y": 1, "type": "w", "hp": 1}, {"created": 0, "x": 13, "y": 14, "type": "w", "hp": 1}, {"created": 0, "x": 1, "y": 14, "type": "w", "hp": 1}, {"created": 0, "x": 5, "y": 9, "type": "w", "hp": 1}, {"created": 0, "x": 9, "y": 9, "type": "w", "hp": 1}, {"created": 0, "x": 3, "y": 1, "type": "w", "hp": 1}, {"created": 0, "x": 11, "y": 1, "type": "w", "hp": 1}, {"created": 0, "x": 13, "y": 11, "type": "w", "hp": 1}, {"created": 0, "x": 1, "y": 11, "type": "w", "hp": 1}, {"created": 0, "x": 8, "y": 12, "type": "w", "hp": 1}, {"created": 0, "x": 6, "y": 12, "type": "w", "hp": 1}, {"created": 0, "x": 14, "y": 3, "type": "w", "hp": 1}, {"created": 0, "x": 0, "y": 3, "type": "w",
"hp": 1}, {"created": 0, "x": 10, "y": 13, "type": "w", "hp": 1}, {"created": 0, "x": 4, "y": 13, "type": "w", "hp": 1}, {"created": 0, "x": 14, "y": 13, "type": "w", "hp": 1}, {"created": 0, "x": 0, "y": 13, "type": "w", "hp": 1}, {"created": 0, "x": 10, "y": 14, "type": "w", "hp": 1}, {"created": 0, "x": 4, "y": 14, "type": "w", "hp": 1}, {"created": 0, "x": 5, "y": 3, "type": "w", "hp": 1}, {"created": 0, "x": 9, "y": 3, "type": "w", "hp": 1}, {"created": 0, "x": 2, "y": 11, "type": "w", "hp": 1}, {"created": 0, "x": 12, "y": 11, "type": "w", "hp": 1}, {"created": 0, "x": 10, "y": 2, "type": "w", "hp": 1}, {"created": 0, "x": 4, "y": 2, "type": "w", "hp": 1}, {"created": 0, "x": 9, "y": 0, "type": "w", "hp": 1}, {"created": 0, "x": 5, "y": 0, "type": "w", "hp": 1}, {"created": 0, "x": 1, "y": 4, "type": "w", "hp": 1}, {"created": 0, "x": 13, "y": 4, "type": "w", "hp": 1}, {"created": 0, "x": 1, "y": 2, "type": "w", "hp": 1}, {"created": 0, "x": 13, "y": 2, "type": "w", "hp": 1}, {"created": 0, "x": 8, "y": 5, "type": "w", "hp": 1}, {"created": 0, "x": 6, "y": 5, "type": "w", "hp": 1}, {"created": 0, "x": 6, "y": 2, "type": "w", "hp": 1}, {"created": 0, "x": 8, "y": 2, "type": "w", "hp": 1}, {"created": 0, "x": 0, "y": 4, "type": "w", "hp": 1}, {"created": 0, "x": 14, "y": 4, "type": "w", "hp": 1}, {"created": 0, "x": 13, "y": 1, "type": "w", "hp": 1}, {"created": 0, "x": 1, "y": 1, "type": "w", "hp": 1}, {"created": 0, "x": 5, "y": 14, "type": "w", "hp": 1}, {"created": 0, "x": 9, "y": 14, "type": "w", "hp": 1}, {"created": 0, "x": 14, "y": 11, "type": "w", "hp": 1}, {"created": 0, "x": 0, "y": 11, "type": "w", "hp": 1}, {"created": 0, "x": 5, "y": 5, "type": "w", "hp": 1}, {"created": 0, "x": 9, "y": 5, "type": "w", "hp": 1}, {"created": 0, "x": 10, "y": 4, "type": "o", "hp": 3}, {"created": 0, "x": 4, "y": 4, "type": "o", "hp": 3}, {"created": 0, "x": 9, "y": 11, "type": "o", "hp": 3}, {"created": 0, "x": 5, "y": 11, "type": "o", "hp": 3}, {"created": 0, "x": 4, "y": 12, "type": "o", "hp": 3}, {"created": 0, "x": 10, "y": 12, "type": "o", "hp": 3}, {"created": 0, "x": 2, "y": 1, "type": "o", "hp": 3}, {"created": 0, "x": 12, "y": 1, "type": "o", "hp": 3}, {"created": 0, "x": 10, "y": 6, "type": "o", "hp": 3}, {"created": 0, "x": 4, "y": 6, "type": "o", "hp": 3}, {"created": 0, "x": 11, "y": 3, "type": "o", "hp": 3}, {"created": 0, "x": 3, "y": 3, "type": "o", "hp": 3}, {"created": 0, "x": 2, "y": 4, "type": "o", "hp": 3}, {"created": 0, "x": 12, "y": 4, "type": "o", "hp": 3}], "world": {"width": 15, "height": 15}, "tick": 0, "config": {"tick_rate_hz": 10, "game_duration_ticks": 1, "fire_spawn_interval_ticks": 5}, "connection": {"id": 2, "role": "agent", "agent_id": "b"}}
"hp": 1}, {"created": 0, "x": 10, "y": 13, "type": "w", "hp": 1}, {"created": 0, "x": 4, "y": 13, "type": "w", "hp": 1}, {"created": 0, "x": 14, "y": 13, "type": "w", "hp": 1}, {"created": 0, "x": 0, "y": 13, "type": "w", "hp": 1}, {"created": 0, "x": 10, "y": 14, "type": "w", "hp": 1}, {"created": 0, "x": 4, "y": 14, "type": "w", "hp": 1}, {"created": 0, "x": 5, "y": 3, "type": "w", "hp": 1}, {"created": 0, "x": 9, "y": 3, "type": "w", "hp": 1}, {"created": 0, "x": 2, "y": 11, "type": "w", "hp": 1}, {"created": 0, "x": 12, "y": 11, "type": "w", "hp": 1}, {"created": 0, "x": 10, "y": 2, "type": "w", "hp": 1}, {"created": 0, "x": 4, "y": 2, "type": "w", "hp": 1}, {"created": 0, "x": 9, "y": 0, "type": "w", "hp": 1}, {"created": 0, "x": 5, "y": 0, "type": "w", "hp": 1}, {"created": 0, "x": 1, "y": 4, "type": "w", "hp": 1}, {"created": 0, "x": 13, "y": 4, "type": "w", "hp": 1}, {"created": 0, "x": 1, "y": 2, "type": "w", "hp": 1}, {"created": 0, "x": 13, "y": 2, "type": "w", "hp": 1}, {"created": 0, "x": 8, "y": 5, "type": "w", "hp": 1}, {"created": 0, "x": 6, "y": 5, "type": "w", "hp": 1}, {"created": 0, "x": 6, "y": 2, "type": "w", "hp": 1}, {"created": 0, "x": 8, "y": 2, "type": "w", "hp": 1}, {"created": 0, "x": 0, "y": 4, "type": "w", "hp": 1}, {"created": 0, "x": 14, "y": 4, "type": "w", "hp": 1}, {"created": 0, "x": 13, "y": 1, "type": "w", "hp": 1}, {"created": 0, "x": 1, "y": 1, "type": "w", "hp": 1}, {"created": 0, "x": 5, "y": 14, "type": "w", "hp": 1}, {"created": 0, "x": 9, "y": 14, "type": "w", "hp": 1}, {"created": 0, "x": 14, "y": 11, "type": "w", "hp": 1}, {"created": 0, "x": 0, "y": 11, "type": "w", "hp": 1}, {"created": 0, "x": 5, "y": 5, "type": "w", "hp": 1}, {"created": 0, "x": 9, "y": 5, "type": "w", "hp": 1}, {"created": 0, "x": 10, "y": 4, "type": "o", "hp": 3}, {"created": 0, "x": 4, "y": 4, "type": "o", "hp": 3}, {"created": 0, "x": 9, "y": 11, "type": "o", "hp": 3}, {"created": 0, "x": 5, "y": 11, "type": "o", "hp": 3}, {"created": 0, "x": 4, "y": 12, "type": "o", "hp": 3}, {"created": 0, "x": 10, "y": 12, "type": "o", "hp": 3}, {"created": 0, "x": 2, "y": 1, "type": "o", "hp": 3}, {"created": 0, "x": 12, "y": 1, "type": "o", "hp": 3}, {"created": 0, "x": 10, "y": 6, "type": "o", "hp": 3}, {"created": 0, "x": 4, "y": 6, "type": "o", "hp": 3}, {"created": 0, "x": 11, "y": 3, "type": "o", "hp": 3}, {"created": 0, "x": 3, "y": 3, "type": "o", "hp": 3}, {"created": 0, "x": 2, "y": 4, "type": "o", "hp": 3}, {"created": 0, "x": 12, "y": 4, "type": "o", "hp": 3}, {"created": 10, "x": 7, "y": 7, "type": "fp", "expires": 50, "hp": 1}, {"created": 10, "x": 6, "y": 7, "type": "bp", "expires": 50, "hp": 1}], "world": {"width": 15, "height": 15}, "tick": 0, "config": {"tick_rate_hz": 10, "game_duration_ticks": 1, "fire_spawn_interval_ticks": 5}, "connection": {"id": 2, "role": "agent", "agent_id": "b"}}
`), &mockState)
assert.NoError(t, err)
return mockState
Expand Down Expand Up @@ -198,7 +210,7 @@ func getMockTickExpiredPacket(t *testing.T) map[string]interface{} {
func getMockUnitStatePayload(t *testing.T) interface{} {
var mockUnitStatePayload interface{}
err := json.Unmarshal([]byte(`
{"coordinates": [3, 10], "hp": 3, "inventory": {"bombs": 2}, "blast_diameter": 3, "unit_id": "c", "agent_id": "a", "invulnerability": 0}
{"coordinates": [3, 10], "hp": 3, "inventory": {"bombs": 2}, "blast_diameter": 3, "unit_id": "c", "agent_id": "a", "invulnerable": 0, "stunned": 0}
`), &mockUnitStatePayload)
assert.NoError(t, err)
return mockUnitStatePayload
Expand Down
6 changes: 4 additions & 2 deletions agents/go/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func tickHandler(tickNumber float64, state map[string]interface{}) (err error) {
}
logrus.WithField("action", "detonate").Info()
x, y := getBombToDetonate(unitID, state)
if x > 0 && y > 0 {
if x >= 0 && y >= 0 {
err = gameState.SendDetonate(x, y, unitID)
if err != nil {
logrus.Error(err)
Expand All @@ -84,7 +84,9 @@ func tickHandler(tickNumber float64, state map[string]interface{}) (err error) {
func getBombToDetonate(unitID string, state map[string]interface{}) (float64, float64) {
for _, iEntity := range state["entities"].([]interface{}) {
entity := iEntity.(map[string]interface{})
if entity["owner_unit_id"] == unitID && entity["type"] == "b" {
// Bombs are type "b"; the placing unit is identified by "unit_id"
// (the engine renamed this from the old "owner_unit_id").
if entity["unit_id"] == unitID && entity["type"] == "b" {
return entity["x"].(float64), entity["y"].(float64)
}
}
Expand Down
Loading
Loading