Skip to content
Closed
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
3 changes: 2 additions & 1 deletion coworld/wasm.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ Only the immutable compiled module may be shared across attempts.

The build uses ARC and Emscripten's default allocator directly. Nim allocation tracing is disabled because its counters
require Nim's allocator. This affects runtime instrumentation, not game behavior. Linear memory grows from 16 MiB to a
96 MiB cap. That cap leaves room for the host but does not guarantee a complete Worker fits its isolate limit.
96 MiB cap, and allocation failure aborts the instance. That cap leaves room for the host but does not guarantee a
complete Worker fits its isolate limit.

## ABI version 1

Expand Down
3 changes: 3 additions & 0 deletions examples/gods_of_the_arena/config.nims
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ when defined(coworldWasm):
switch("gc", "arc")
switch("exceptions", "goto")
switch("define", "noSignalHandler")
switch("passC", "-flto")
switch("passL", "-flto")
switch("passL", "-s ABORTING_MALLOC=1")
switch("passL", "-O3 -s MODULARIZE=1 -s EXPORT_ES6=1 -s ENVIRONMENT=worker -s ALLOW_MEMORY_GROWTH=1 -s INITIAL_MEMORY=16777216 -s MAXIMUM_MEMORY=100663296 -s EXPORTED_FUNCTIONS=_main,_pw_alloc,_pw_free,_pw_initialize,_pw_advance,_pw_finalize,_pw_output,_pw_output_length -s EXPORTED_RUNTIME_METHODS=HEAPU8 -s FILESYSTEM=0")
107 changes: 53 additions & 54 deletions examples/gods_of_the_arena/sim.nim
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,8 @@ type
teamExplored*: array[2, seq[uint8]]
visionCache: array[2, VisionCache]
visionSkipKeys: seq[int32]
scriptObjects: seq[WorldObject]
scriptObjectCount: int
scriptObjects: array[Team, seq[WorldObject]]
scriptObjectsReady: array[Team, bool]
scriptObjectsHeroId: int32
scriptObjectsTick: int32
observationsFrozen: bool
Expand Down Expand Up @@ -2103,7 +2103,7 @@ proc rawWorldObjectAt(world: World, index: int, value: var WorldObject): bool =
return true
let buildingIndex = index - world.forts.len
if buildingIndex < world.buildings.len:
let tower = world.buildings[buildingIndex]
let tower {.cursor.} = world.buildings[buildingIndex]
value = WorldObject(
id: tower.id,
kind: (if tower.kind == TowerBuilding: TowerObjectKind
Expand Down Expand Up @@ -2147,7 +2147,7 @@ proc rawWorldObjectAt(world: World, index: int, value: var WorldObject): bool =
return true
let footmanIndex = heroIndex - world.heroes.len
if footmanIndex < world.footmen.len:
let footman = world.footmen[footmanIndex]
let footman {.cursor.} = world.footmen[footmanIndex]
value = WorldObject(
id: footman.id,
kind: (if footman.camp > 0: NeutralObjectKind else: FootmanObjectKind),
Expand Down Expand Up @@ -2202,7 +2202,7 @@ proc freezeObservations*(world: World): bool =
cmp(world.spellObservationKey(first, team),
world.spellObservationKey(second, team))
)
world.scriptObjectsTick = -1
world.scriptObjectsReady = [false, false]
world.observationsFrozen = true
true

Expand All @@ -2212,7 +2212,7 @@ proc thawObservations*(world: World) =
world.observedObjects.setLen(0)
for spells in world.observedSpells.mitems:
spells.setLen(0)
world.scriptObjectsTick = -1
world.scriptObjectsReady = [false, false]

iterator observedCasts*(world: World, team: Team): SpellCast =
## Reads the common decision frame, or live casts outside that phase.
Expand Down Expand Up @@ -2241,44 +2241,42 @@ proc scriptObjectKey(value: WorldObject, observer: Team):
(group, int(value.faction != observer.ord.int32), direction * value.position.z,
direction * value.position.x, value.id)

proc ensureScriptObjects(world: World, heroId: int32) =
## Rebuilds the visible object list once per hero decision tick.
if world.scriptObjectsHeroId == heroId and
world.scriptObjectsTick == world.tick:
return
world.scriptObjectCount = 0
proc ensureScriptObjects(world: World, heroId: int32): int =
## A frozen frame has one visibility and ordering per team, shared by its heroes.
let observer = heroIndex(world, heroId)
if observer >= 0:
let team = world.heroes[observer].team
var value: WorldObject
let count =
if world.observationsFrozen: world.observedObjects.len
else: rawWorldObjectCount(world)
for i in 0 ..< count:
if world.observationsFrozen:
value = world.observedObjects[i]
elif not rawWorldObjectAt(world, i, value):
continue
if not objectVisibleTo(world, team, value) or
(value.kind in [TowerObjectKind, BarracksObjectKind] and value.hp <= 0):
continue
if world.scriptObjectCount == world.scriptObjects.len:
world.scriptObjects.add value
else:
world.scriptObjects[world.scriptObjectCount] = value
inc world.scriptObjectCount
world.scriptObjects.setLen(world.scriptObjectCount)
world.scriptObjects.sort(proc(first, second: WorldObject): int =
## Orders observed identities in the querying team's coordinate frame.
cmp(first.scriptObjectKey(team), second.scriptObjectKey(team))
)
if observer < 0:
world.scriptObjectsReady = [false, false]
return -1
let team = world.heroes[observer].team
result = team.ord
if world.scriptObjectsReady[team] and (world.observationsFrozen or
(world.scriptObjectsHeroId == heroId and world.scriptObjectsTick == world.tick)):
return
world.scriptObjects[team].setLen(0)
var value: WorldObject
let count =
if world.observationsFrozen: world.observedObjects.len
else: rawWorldObjectCount(world)
for i in 0 ..< count:
if world.observationsFrozen:
value = world.observedObjects[i]
elif not rawWorldObjectAt(world, i, value):
continue
if not objectVisibleTo(world, team, value) or
(value.kind in [TowerObjectKind, BarracksObjectKind] and value.hp <= 0):
continue
world.scriptObjects[team].add value
world.scriptObjects[team].sort(proc(first, second: WorldObject): int =
cmp(first.scriptObjectKey(team), second.scriptObjectKey(team))
)
world.scriptObjectsReady[team] = true
world.scriptObjectsHeroId = heroId
world.scriptObjectsTick = world.tick

proc worldObjectCount*(world: World, heroId: int32): int =
## Returns the number of objects visible to one hero script.
world.ensureScriptObjects(heroId)
world.scriptObjectCount
let team = world.ensureScriptObjects(heroId)
if team >= 0: world.scriptObjects[Team(team)].len else: 0

proc worldObjectAt*(
world: World,
Expand All @@ -2287,10 +2285,10 @@ proc worldObjectAt*(
value: var WorldObject
): bool =
## Reads one object from a hero's stable visibility-filtered enumeration.
world.ensureScriptObjects(heroId)
if index < 0 or index >= world.scriptObjectCount:
let team = world.ensureScriptObjects(heroId)
if team < 0 or index < 0 or index >= world.scriptObjects[Team(team)].len:
return false
value = world.scriptObjects[index]
value = world.scriptObjects[Team(team)][index]
true

proc worldObjectById*(
Expand Down Expand Up @@ -3062,7 +3060,7 @@ proc updateTower*(world: World, tower: var Building) =
targetFootman = footmanIndex(world, tower.targetId)
targetHero = heroIndex(world, tower.targetId)
if targetFootman >= 0:
let footman = world.footmen[targetFootman]
let footman {.cursor.} = world.footmen[targetFootman]
if not world.hostile(footman, tower.team) or
footman.state == Dying or footman.hp <= 0 or
not within(tower.position, footman.position, attackRange) or
Expand All @@ -3080,7 +3078,8 @@ proc updateTower*(world: World, tower: var Building) =
bestSquared = int64(attackRange) * attackRange
bestId = 0'i32
bestPosition: WorldPoint
for i, footman in world.footmen:
for i in 0 ..< world.footmen.len:
let footman {.cursor.} = world.footmen[i]
if not world.hostile(footman, tower.team) or footman.state == Dying or
footman.hp <= 0 or
not visible(world, tower.team, footman.position):
Expand Down Expand Up @@ -3493,7 +3492,7 @@ proc updateFootman(world: World, footman: var Footman) =
targetHero = heroIndex(world, footman.targetHeroId)
targetBuilding = buildingIndex(world, footman.targetBuildingId)
if targetFootman >= 0:
let other = world.footmen[targetFootman]
let other {.cursor.} = world.footmen[targetFootman]
if not world.hostile(other, footman.team) or
not visible(world, footman.team, other.position) or
not within(
Expand Down Expand Up @@ -3527,7 +3526,8 @@ proc updateFootman(world: World, footman: var Footman) =
bestSquared = int64(FootmanSightRadius) * FootmanSightRadius
bestId = 0'i32
bestPosition: WorldPoint
for i, other in world.footmen:
for i in 0 ..< world.footmen.len:
let other {.cursor.} = world.footmen[i]
if not world.hostile(other, footman.team):
continue
if not visible(world, footman.team, other.position):
Expand Down Expand Up @@ -3824,7 +3824,7 @@ proc applyDraft*(
hero.maxMana = heroMaxMana(hero.class, hero.level)
hero.mana = hero.maxMana
hero.initHeroCharges()
world.scriptObjectsTick = -1
world.scriptObjectsReady = [false, false]
inc world.draftTurn
world.draftTurnTicks = 0
if world.draftTurn == world.draftOrder.len:
Expand Down Expand Up @@ -5114,7 +5114,8 @@ proc separateUnits(game: Game) =
let world = game.world
var maximumRadius = FixedZero
game.collisionUnits.setLen(0)
for i, footman in world.footmen:
for i in 0 ..< world.footmen.len:
let footman {.cursor.} = world.footmen[i]
if footman.state != Dying:
game.collisionUnits.add CollisionUnit(body: footman.body, index: i,
layer: footman.navLayer, team: footman.team,
Expand Down Expand Up @@ -5506,8 +5507,8 @@ proc tickWorld*(game: Game, onHeroTurn: proc() {.closure.}) {.measure.} =

# Plan every unit against the same actor state, then publish together.
game.nextFootmen.setLen(world.footmen.len)
for i, footman in world.footmen:
game.nextFootmen[i] = footman
for i in 0 ..< world.footmen.len:
game.nextFootmen[i] = world.footmen[i]
game.nextHeroes.setLen(world.heroes.len)
for i, hero in world.heroes:
if game.nextHeroes[i] == nil:
Expand All @@ -5528,7 +5529,7 @@ proc tickWorld*(game: Game, onHeroTurn: proc() {.closure.}) {.measure.} =

swap(world.footmen, game.nextFootmen)
for i, hero in game.nextHeroes:
world.heroes[i][] = hero[]
swap(world.heroes[i][], hero[])

world.advanceSpells()
world.advanceTowerShots()
Expand All @@ -5540,7 +5541,7 @@ proc tickWorld*(game: Game, onHeroTurn: proc() {.closure.}) {.measure.} =
if world.footmen[read].state != Dying or
world.footmen[read].deathTicks < FootmanDeathTicks + CorpseLingerTicks:
if write != read:
world.footmen[write] = world.footmen[read]
world.footmen[write] = move(world.footmen[read])
inc write
else:
when defined(replayEvents):
Expand Down Expand Up @@ -5725,9 +5726,7 @@ proc newGame*(
world: World(
forts: startingForts(map),
nextFootmanId: FirstFootmanId,
winner: RedTeam,
scriptObjects: newSeqOfCap[WorldObject](256),
scriptObjectsTick: -1
winner: RedTeam
),
map: map,
replayMode: replayMode,
Expand Down
2 changes: 1 addition & 1 deletion src/polyworld/tapes.nim
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ proc encodeReplayFile*[T](
result.addUint16(gameVersion)
result.addUint16(uint16(game.len))
result.add game
result.add data.toFlatty()
result.toFlatty(data)
if result.len > maxBytes:
fail("encoded replay exceeds the file size limit")

Expand Down
20 changes: 13 additions & 7 deletions src/polyworld/visions.nim
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,10 @@ proc revealVision*(
):
visible[index] = 255

proc sameHeights(first, second: seq[int16]): bool =
first.len == second.len and (first.len == 0 or
equalMem(unsafeAddr first[0], unsafeAddr second[0], first.len * sizeof(int16)))

proc revealVisionCached*(
cache: var VisionCache,
visible: var seq[uint8],
Expand All @@ -367,7 +371,8 @@ proc revealVisionCached*(
## Retains only the previous frame's source rays. Terrain or blocker changes
## invalidate every entry, including height changes without moving a source.
if cache.width != width or cache.height != height or
cache.terrain != terrainHeights or cache.blockers != blockerHeights:
not sameHeights(cache.terrain, terrainHeights) or
not sameHeights(cache.blockers, blockerHeights):
cache.sources.clear()
cache.width = width
cache.height = height
Expand All @@ -376,22 +381,23 @@ proc revealVisionCached*(
visible.setLen(int(width * height))
for value in visible.mitems:
value = 0
var nextSources: Table[VisionSource, seq[int32]]
var nextSources = initTable[VisionSource, seq[int32]](sources.len)
for source in sources:
if nextSources.hasKey(source):
continue
if not cache.sources.hasKey(source):
var cells: seq[int32]
var cells: seq[int32]
cache.sources.withValue(source, previous):
cells = move(previous[])
do:
for z in max(0'i32, source.z - source.radius) .. min(height - 1, source.z + source.radius):
for x in max(0'i32, source.x - source.radius) .. min(width - 1, source.x + source.radius):
if source.radius > 0 and source.inVisionRange(x, z) and lineVisible(
width, height, terrainHeights, blockerHeights,
source.x, source.z, x, z, source.radius, source.eyeHeight):
cells.add z * width + x
cache.sources[source] = move(cells)
for index in cache.sources[source]:
for index in cells:
visible[index] = 255
nextSources[source] = move(cache.sources[source])
nextSources[source] = move(cells)
cache.sources = move(nextSources)

proc blurVisibility*(visible: openArray[uint8], width, height: int32): seq[uint8] =
Expand Down
30 changes: 30 additions & 0 deletions tests/test_gota_observations.nim
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,36 @@ proc reveal(world: World, team: Team, position: WorldPoint) =
z = mapCoordinate(position.z)
world.teamVisible[team.ord][int(z) * mapTiles() + int(x)] = 255

proc objects(world: World, heroId: int32): seq[WorldObject] =
for i in 0 ..< world.worldObjectCount(heroId):
var value: WorldObject
doAssert world.worldObjectAt(heroId, i, value)
result.add value

echo "Testing team observations stay frozen and refresh for the next frame"
block:
let world = observationWorld()
world.heroes[0].hp = 100
let
red = world.objects(100)
blue = world.objects(105)
doAssert red != blue
doAssert world.objects(100) == red
world.heroes[0].hp = 75
doAssert world.objects(100) == red
world.heroes[0].hp = 100
doAssert world.freezeObservations()
doAssert world.objects(100) == red
world.heroes[0].hp = 50
doAssert world.objects(105) == blue
doAssert world.objects(101) == red
world.thawObservations()
doAssert world.freezeObservations()
let next = world.objects(101)
doAssert next != red
doAssert world.objects(100) == next
world.thawObservations()

proc warning(heroId: int32, x = 0'i32): SpellCast =
## Creates a delayed spell whose aim position differs from its origin.
SpellCast(
Expand Down
Loading