--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Structure of a client/server game, based on turns, events, orders and rules
+module framework is serialize
+
+import geometry
+import realtime
+
+# Main game object containing the world, players and tanks
+#
+# The game object is shared by both the client and the server.
+# So a method using the game object as visitor is to be executed client-side.
+class TGame
+
+ # `Story`, or rule book, with all the stats for this game
+ var story = new Story
+
+ # `Clock` timing the elapsed time between turns
+ private var clock = new Clock is noserialize
+
+ # Tick count of the last turn (The first turn as a tick of 0)
+ var tick: Int = -1
+
+ # Execute the next turn and return it as a `TTurn`
+ #
+ # This is to be executed server-side only.
+ fun do_turn: TTurn
+ do
+ var dt = clock.lapse
+ tick += 1
+
+ var turn = new TTurn(self, tick, dt.to_f, dt.millisec)
+ return turn
+ end
+
+ # Apply `turn` locally by updating `tick` and applying all events
+ #
+ # This is to be executed client-side only.
+ fun apply_turn(turn: TTurn)
+ do
+ tick = turn.tick
+ for event in turn.events do event.apply self
+ end
+end
+
+# A single turn of a `TGame`
+#
+# The turn object is created and populated by the server (using `TGame::do_turn`).
+# It is transmitted to the client but it cannot modify it.
+# So methods using the turn object as visitor are to be executed server-side.
+class TTurn
+
+ # `TGame` of which `self` is part of
+ var game: TGame
+
+ # Tick of this turn
+ var tick: Int
+
+ # Elapsed seconds since previous turn (as a `Float`)
+ var dts: Float
+
+ # Elapsed milliseconds since previous turn (as a `Int`)
+ var dt: Int
+
+ # `TEvent` that happened during this turn
+ #
+ # Events are added using `add`.
+ # This information is used to apply the turn client-side to update its game object.
+ # It is also used by effects on the UI and could be used by an AI.
+ var events: SequenceRead[TEvent] = new Array[TEvent]
+
+ # Add an `event` to `events` and apply it right away server-side
+ fun add(event: TEvent)
+ do
+ event.apply game
+ events.as(Array[TEvent]).add event
+ end
+end
+
+# Game event sent from the server to the client
+class TEvent
+
+ # Executed client-side to apply this event on the `game`
+ fun apply(game: TGame) do end
+end
+
+# An order sent from the client to the server
+class TOrder
+
+ # Apply order server-side on `turn`, usually spawns `GameEvent`
+ fun apply(turn: TTurn) do end
+end
+
+# An entity acting on each turn
+class TTurnable
+
+ # Act on `turn`
+ fun do_turn(turn: TTurn) do end
+end
+
+# A collection of `Rule` guiding the game
+#
+# Changing the story could (in theory) be enough to completely change the context of the game.
+# In _Tinks!_ however, we use this class lightly and fill it with static content.
+class Story
+end
+
+# Metadata of in game `Ruled` entities, keep their stats and assets in a single place
+class Rule
+
+ # `Story` to which `self` belongs
+ var story: Story
+end
+
+# A game entity with metadata in its `rule`
+class Ruled
+
+ # Kind of `Rule` for `rule`
+ type R: Rule
+
+ # Metadata of this entity
+ var rule: R
+end
+
+# Should the game show more information for debugging?
+fun debug: Bool do return false
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import tanks
+import players
+import world
+import powerups
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Player related and tank spawning logic
+module players is serialize
+
+import tanks
+
+redef class TGame
+
+ # All the known players in the game
+ var players = new Array[Player]
+
+ redef fun do_turn
+ do
+ var turn = super
+ for player in players do
+ player.do_turn turn
+ end
+ return turn
+ end
+end
+
+redef class TTurn
+
+ # Range around the center of the world (0, 0) where a tank can spawn
+ var spawn_range = 256.0
+
+ # Spawn a new tank for `player`
+ fun spawn_tank(player: Player)
+ do
+ var pos = new Pos(spawn_range.rand, spawn_range.rand)
+ var tank = new Tank(game.story.tanks.rand, pos, 2.0*pi.rand)
+
+ if tank.next_move_collisions(self).not_empty then
+ # Clear the way
+ game.world.explode(self, pos, 3)
+ end
+
+ add new TankSpawnEvent(tank, player)
+ end
+end
+
+# A player in the game
+class Player
+ super TTurnable
+
+ # Queue of orders to apply at the end of the turn
+ var orders = new Array[TOrder]
+
+ # The tank controlled by this player, if any
+ var tank: nullable Tank = null
+
+ # Index of the "unique" player stencil applied on all its tanks
+ var stencil_index: Int do
+ var counter = once new Ref[Int](0)
+ var val = counter.item
+ counter.item = (counter.item+1) % 4
+ return val
+ end
+
+ redef fun do_turn(turn)
+ do
+ # Apply orders if they are legal
+ for order in orders do
+ if order.is_legal(turn.game, self) then
+ order.apply turn
+ else print "Server Warning: Order {order} is now illegal"
+ end
+
+ orders.clear
+ end
+end
+
+redef class Tank
+ # The player controlling this tank, if any
+ var player: nullable Player = null
+end
+
+redef class TOrder
+
+ # Is this order (still) legal?
+ #
+ # This is executed client-side.
+ fun is_legal(game: TGame, issed_by: Player): Bool do return true
+end
+
+redef abstract class TankOrder
+ redef fun is_legal(game, issed_by) do return issed_by == tank.player
+end
+
+# A request to spawn a new tank
+class SpawnTankOrder
+ super TOrder
+
+ # Requester
+ var player: Player
+
+ redef fun is_legal(turn, issed_by) do return issed_by == player and player.tank == null
+
+ redef fun apply(turn)
+ do
+ turn.spawn_tank player
+ end
+end
+
+# A new tank appeared
+class TankSpawnEvent
+ super TEvent
+
+ # The new tank
+ var tank: Tank
+
+ # The `tank` owner
+ var player: nullable Player
+
+ redef fun apply(game)
+ do
+ var player = player
+ if player != null then player.tank = tank
+ tank.player = player
+ game.tanks.add tank
+ end
+end
+
+redef class TankDeathEvent
+
+ redef fun apply(game)
+ do
+ super
+
+ # `player` has no tank anymore
+ var player = tank.player
+ if player != null then
+ player.tank = null
+ end
+ end
+end
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Support for pickable powerups (only health for now)
+module powerups is serialize
+
+import tanks
+
+# A powerup item
+class Powerup
+ super Feature
+ redef type R: PowerupRule
+end
+
+# Metadata of a powerup item
+class PowerupRule
+ super FeatureRule
+
+ # Restore all health when picked up
+ var restore_health: Bool
+end
+
+redef class Story
+ # A powerup that restores all health
+ var health = new PowerupRule(self, 2, true)
+
+ # All `PowerupRule` in this story
+ var powerups: Array[PowerupRule] = [health]
+end
+
+redef class Tank
+
+ redef fun destroy(turn)
+ do
+ super
+
+ # Put a random powerup at the center of the old tank
+ var pos = new Pos(pos.x.floor+0.5, pos.y.floor+0.5)
+ var powerup = new Powerup(turn.game.story.powerups.rand, pos)
+ turn.add new FeatureChangeEvent(powerup, pos)
+
+ # Add some debris around it
+ var forward = new Pos((pos.x+heading.cos*1.1).floor+0.5, (pos.y+heading.sin*1.1).floor+0.5)
+ var backward = new Pos((pos.x-heading.cos*1.1).floor+0.5, (pos.y-heading.sin*1.1).floor+0.5)
+ turn.add new FeatureChangeEvent(new Feature(turn.game.story.debris, forward), forward)
+ turn.add new FeatureChangeEvent(new Feature(turn.game.story.debris, backward), backward)
+ end
+
+ # Intercept collision detection of "absorb" powerups
+ #
+ # This is a wee bit hackish.
+ # The collision detection on tank move can return a max of 4 items (1 per side).
+ # If there is powerups, they may have hidden other features.
+ # This could cause the tank to move over a feature and get stuck.
+ # This is not a big problem as the tank can open fire to liberate itself,
+ # or even simply go back as the speed is static.
+ redef fun next_move_collisions(turn)
+ do
+ var collisions = super
+ if collisions.is_empty then return collisions
+
+ for coll in collisions do if not coll isa Powerup then
+ # An unavoidable collision
+ return collisions
+ end
+
+ # Only powerups! absorb them
+ for powerup in collisions do
+ if powerup isa Powerup then
+ turn.add new FeatureChangeEvent(null, powerup.pos)
+
+ if powerup.rule.restore_health then
+ turn.add new TankHealthChange(self, rule.max_health)
+ end
+ end
+ end
+
+ return new HashSet[Feature]
+ end
+end
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Tank and tank turret related logic
+module tanks is serialize
+
+import world
+
+redef class TGame
+ # All of the tanks in game
+ var tanks = new TankSet
+
+ redef fun do_turn
+ do
+ var turn = super
+ for tank in tanks do tank.do_turn turn
+ return turn
+ end
+end
+
+# Stats of a tank kind, its config should be move to the `Story` if we want more than 1
+class TankRule
+ super Rule
+
+ # Width of this tank
+ var width: Float = 64.0/32.0
+
+ # Height of this tank
+ var length: Float = 100.0/32.0
+
+ # Maximum `health` this tank can normally have
+ var max_health = 4
+
+ # Maximum speed of this tank (in world coordinate units per seconds)
+ var max_speed = 2.0
+
+ # Turret turning speed (in radians per second)
+ var turret_turn_speed = 1.6
+
+ # Waiting time between shots can be fired
+ var turret_cooldown_time = 2.0
+
+ # Maximum `direction_heading` heading, may be double
+ var max_direction = 1.0
+end
+
+redef class Story
+ # The main (and only) tank configuration in this game
+ var tanks: Array[TankRule] = [new TankRule(self)]
+end
+
+# A tank!
+class Tank
+ super TTurnable
+ super Ruled
+ redef type R: TankRule
+
+ # In world `Pos` of this entity
+ var pos: Pos
+
+ # Orientation of this entity
+ var heading: Float
+
+ # The turret mounted on this tank
+ var turret = new Turret(self)
+
+ # Commanded direction
+ var direction_heading = 0.0
+
+ # Commanded speed
+ var direction_forwards = 0.0
+
+ # Health of this tank, out of `rule.max_health`
+ var health: Int = rule.max_health is lazy
+
+ redef fun do_turn(turn)
+ do
+ var collisions = next_move_collisions(turn)
+
+ if collisions.is_empty then
+ var next = normal_next_pos(turn)
+ self.pos = next.first
+ self.heading = next.second
+ end
+
+ turret.do_turn turn
+
+ turn.add new TankMoveEvent(self, pos, direction_heading, direction_forwards, heading, turret.relative_heading)
+ end
+
+ # What would be the next position if not blocked by terrain features?
+ #
+ # Returns a couple of the new position and heading.
+ fun normal_next_pos(turn: TTurn): Couple[Pos, Float]
+ do
+ var heading = (heading + direction_heading * turn.dts).angle_normalize
+
+ var speed = direction_forwards * rule.max_speed * turn.dts
+ var pos = pos
+ if speed != 0.0 then
+ pos += heading.to_vector(speed)
+ end
+
+ return new Couple[Pos, Float](pos, heading)
+ end
+
+ # Damage this tank, server-side
+ fun hit(turn: TTurn)
+ do
+ var damage = 1
+ var health = health - damage
+
+ if health <= 0 then
+ destroy turn
+ else
+ turn.add new TankHealthChange(self, health)
+ end
+ end
+
+ # Destroy this tank, server-side
+ fun destroy(turn: TTurn)
+ do
+ turn.add new TankDeathEvent(self)
+ turn.game.world.explode(turn, pos, 3)
+ end
+
+ # Collisions on the next move
+ fun next_move_collisions(turn: TTurn): HashSet[Feature]
+ do
+ var next = normal_next_pos(turn)
+ var features = new HashSet[Feature]
+
+ # Use the lines between the corners to detect collisions
+ var corners = corners_at(next)
+ var prev_corner = corners.last
+ for corner in corners do
+ var feature = turn.game.world.first_collision(prev_corner, corner)
+ if feature != null then features.add feature
+ prev_corner = corner
+ end
+
+ return features
+ end
+
+ # Get the 4 corners at a `next` position
+ fun corners_at(next: Couple[Pos, Float]): Array[Pos]
+ do
+ var next_pos = next.first
+ var heading = next.second
+
+ var corners = new Array[Pos]
+
+ var hwy = rule.width/2.0 * (heading+pi/2.0).sin
+ var hwx = rule.width/2.0 * (heading+pi/2.0).cos
+ var hly = rule.length/2.0 * heading.sin
+ var hlx = rule.length/2.0 * heading.cos
+ corners.add new Pos(next_pos.x + hlx + hwx, next_pos.y + hly + hwy)
+ corners.add new Pos(next_pos.x + hlx - hwx, next_pos.y + hly - hwy)
+ corners.add new Pos(next_pos.x - hlx - hwx, next_pos.y - hly - hwy)
+ corners.add new Pos(next_pos.x - hlx + hwx, next_pos.y - hly + hwy)
+
+ return corners
+ end
+end
+
+# A tank turret
+class Turret
+ super TTurnable
+
+ # The `Tank` on which is mounted this turret
+ var tank: Tank
+
+ # Orientation of this turret relative to the tank
+ var relative_heading = 0.0
+
+ # Absolute orientation of this turret
+ fun heading: Float do return (tank.heading+relative_heading).angle_normalize
+
+ # Current target to aim for and fire upon
+ var target: nullable Pos = null
+
+ # Seconds left before the turret can open fire again
+ var cooldown = 0.0
+
+ redef fun do_turn(turn)
+ do
+ if cooldown > 0.0 then
+ cooldown = cooldown - turn.dts
+
+ if cooldown <= 0.0 then
+ cooldown = 0.0
+
+ # Notify clients
+ turn.add new TurretReadyEvent(tank)
+ end
+ end
+
+ var target = target
+ if target != null then
+
+ var angle_to_target = tank.pos.atan2(target)
+ var d = (heading - angle_to_target).angle_normalize
+
+ var max_angle = tank.rule.turret_turn_speed * turn.dts
+ if d.abs < max_angle then
+ self.relative_heading = angle_to_target - tank.heading
+
+ if cooldown == 0.0 then
+ # On target, fire
+ fire turn
+ self.target = null
+ end
+ else
+ # Turn towards target
+ if d < 0.0 then
+ self.relative_heading += max_angle
+ else self.relative_heading -= max_angle
+ end
+ end
+ end
+
+ # Open fire!
+ fun fire(turn: TTurn)
+ do
+ var dst = target
+ assert dst != null
+
+ # Is there something between the tank and the target?
+ var hit = turn.game.world.first_collision(tank.pos, dst)
+ if hit != null then dst = hit.pos
+
+ # Events!
+ turn.add new OpenFireEvent(tank)
+ turn.game.world.explode(turn, dst, 2)
+
+ # The turret need time to reload, cooldown!
+ cooldown = tank.rule.turret_cooldown_time
+ end
+end
+
+redef class World
+ redef fun explode(turn, center, force)
+ do
+ super
+
+ for tank in game.tanks do
+ if tank.health == 0 then continue
+ if center.dist(tank.pos) <= force.to_f + 1.0 then
+ tank.hit turn
+ end
+ end
+ end
+end
+
+# A collection of `Tank` that could be optimized
+class TankSet
+ super HashSet[Tank]
+end
+
+# A `tank` centric order
+abstract class TankOrder
+ super TOrder
+
+ # The `Tank` at the center of this order
+ var tank: Tank
+end
+
+# A command to change the behavior of `tank`
+class TankDirectionOrder
+ super TankOrder
+
+ # Desired direction, in [-1.0..1.0]
+ var direction_heading: Float
+
+ # Desired speed, in [-1.0..1.0]
+ var direction_forwards: Float
+
+ redef fun apply(game)
+ do
+ # TODO use events
+ var direction_heading = direction_heading
+ direction_heading = direction_heading.min(1.0).max(-1.0)
+ tank.direction_heading = direction_heading*tank.rule.max_direction
+
+ var direction_forwards = direction_forwards
+ direction_forwards = direction_forwards.min(1.0).max(-1.0)
+ tank.direction_forwards = direction_forwards*tank.rule.max_speed
+ end
+end
+
+# Order to aim and fire at `target`
+class AimAndFireOrder
+ super TankOrder
+
+ # Target for the turret
+ var target: Pos
+
+ redef fun apply(game)
+ do
+ tank.turret.target = target
+ end
+end
+
+# A `tank` centric event
+abstract class TankEvent
+ super TEvent
+
+ # The `Tank` at the center of this event
+ var tank: Tank
+end
+
+# `tank` opens fire
+class OpenFireEvent
+ super TankEvent
+end
+
+# The turret of `tank` is ready to open fire
+class TurretReadyEvent
+ super TankEvent
+end
+
+# `tank` has been destroyed
+class TankDeathEvent
+ super TankEvent
+
+ redef fun apply(game)
+ do
+ tank.health = 0
+ game.tanks.remove tank
+ end
+end
+
+# The health of `tank` changes to `new_health`
+class TankHealthChange
+ super TankEvent
+
+ # The new health for `tank`
+ var new_health: Int
+
+ redef fun apply(game)
+ do
+ tank.health = new_health
+ end
+end
+
+# A `tank` moved
+#
+# TODO this event is too big, divide in 2 or more and move more logic client-side
+class TankMoveEvent
+ super TankEvent
+
+ # The position
+ var pos: Pos
+
+ # The direction of the "wheels"
+ var direction_heading: Float
+
+ # The speed
+ var direction_forwards: Float
+
+ # Orientation of the tank
+ var tank_heading: Float
+
+ # Orientation of the turret
+ var turret_heading: Float
+
+ redef fun apply(game)
+ do
+ tank.pos = pos
+ tank.direction_heading = direction_heading
+ tank.direction_forwards = direction_forwards
+ tank.heading = tank_heading
+ tank.turret.relative_heading = turret_heading
+ end
+end
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Physical world logic
+module world is serialize
+
+import noise
+import more_collections
+
+import framework
+
+redef class TGame
+ # The physical world in which this game happens
+ var world = new World(self)
+end
+
+# A terrain features (a rock, a tree, etc.)
+class Feature
+ super Ruled
+ redef type R: FeatureRule
+
+ # Top-left corner of this feature
+ var pos: Pos
+end
+
+# Metadata for a `Feature`
+class FeatureRule
+ super Rule
+
+ # Strength to resist on `World::explode`
+ var strength: Int
+end
+
+# The physical world of the `game`
+class World
+ # Associated `TGame`
+ var game: TGame
+
+ # Past blast sites
+ var blast_sites = new Array[Pos]
+
+ private var mountain_map: Noise do
+ var map = new PerlinNoise
+ map.period = 10.0
+ return map
+ end
+
+ private var forest_map: Noise do
+ var map = new PerlinNoise
+ map.period = 24.0
+ return map
+ end
+
+ # Cache of discovered features, also keeps tracks of changes
+ private var features_cache = new FeatureMap
+
+ # Get the `Feature` at `x, y`, if any
+ fun [](x, y: Int): nullable Feature
+ do
+ if features_cache.has(x, y) then return features_cache[x, y]
+
+ # Generate a feature from the noise map
+ var pos = new Pos(x.to_f+0.5, y.to_f+0.5)
+
+ var feature = null
+ if mountain_map[x.to_f, y.to_f] > 0.55 then
+ feature = new Feature(game.story.rock, pos)
+ else if forest_map[x.to_f, y.to_f] > 0.5 then
+ feature = new Feature(game.story.tree, pos)
+ end
+
+ # Update cache
+ features_cache[x, y] = feature
+
+ return feature
+ end
+
+ # Detect the first collision with a `Feature` from `src` to `dst`
+ #
+ # This is the main collision detection method used by tanks shots, and at tank movement.
+ # The idea is to check all cases between `src` and `dst` and return the first feature found.
+ # Returns `null` if there is no obstacle features.
+ #
+ # Example of the cases that would be checked between `s` and `d`:
+ #
+ # ~~~raw
+ # ................
+ # .s###...........
+ # ....######......
+ # .........####d..
+ # ................
+ # ~~~
+ fun first_collision(src, dst: Pos): nullable Feature
+ do
+ var going_left = dst.x < src.x
+ var angle = src.atan2(dst)
+ var slope = angle.tan
+
+ # Soften slopes approaching infinity
+ if slope > 100.0 then slope = 100.0
+ if slope < -100.0 then slope = -100.0
+
+ # For each column (over x) from src.x to dst.x
+ var x0 = src.x.floor.to_i
+ var x1 = dst.x.floor.to_i
+ for x in [x0 .. x1].smart_step do
+ var dx = x.to_f - src.x
+ var y0 = src.y + dx*slope
+ var y1 = src.y + (dx+1.0)*slope
+
+ var first = y0.floor.to_i
+ var last = y1.floor.to_i
+ if going_left then
+ # Invert the first and last element of the range
+ var swap = first
+ first = last
+ last = swap
+ end
+
+ # For each row (over y)
+ # from where the line enters the column to where it leaves it
+ for y in [first .. last].smart_step do
+ if not y.in_between_floats(src.y, dst.y) then continue
+
+ var feature = self[x.to_i, y]
+ if feature != null then return feature
+ end
+ end
+
+ return null
+ end
+
+ # Apply an explosion at `center` of the given `power`
+ fun explode(turn: TTurn, center: Pos, power: Int)
+ do
+ var x = center.x.floor.to_i
+ var y = center.y.floor.to_i
+ var range = [-power .. power]
+ var features = new Array[Feature]
+
+ for dx in range do
+ for dy in range do
+ var f = self[x+dx, y+dy]
+ var force = (power-dx.abs) + (power-dy.abs)
+ if f != null and f.rule.strength <= force then features.add f
+ end
+ end
+
+ turn.add new ExplosionEvent(center, power, features)
+ end
+end
+
+# Map of features organized by their coordinates
+#
+# The naive implementation is using a `HashMap2`.
+# This class can be redefed with optimizations as needed.
+class FeatureMap
+ super HashMap2[Int, Int, nullable Feature]
+end
+
+redef class Story
+ # Forest tree
+ var tree = new FeatureRule(self, 2)
+
+ # Big rock
+ var rock = new FeatureRule(self, 3)
+
+ # Metallic debris
+ var debris = new FeatureRule(self, 4)
+end
+
+# An explosion
+class ExplosionEvent
+ super TEvent
+
+ # Center of the explosion
+ var pos: Pos
+
+ # Power of the blast
+ var power: Int
+
+ # All the features this explosion destroys
+ var destroyed_features: Array[Feature]
+
+ redef fun apply(game)
+ do
+ for feature in destroyed_features do
+ game.world.features_cache[feature.pos.x.floor.to_i, feature.pos.y.floor.to_i] = null
+ end
+
+ game.world.blast_sites.add pos
+ if game.world.blast_sites.length > 100 then game.world.blast_sites.shift
+ end
+end
+
+# The feature at `pos` changes to `feature`
+class FeatureChangeEvent
+ super TEvent
+
+ # New `Feature`, if any
+ var feature: nullable Feature
+
+ # `Pos` of this change
+ var pos: Pos
+
+ redef fun apply(game)
+ do
+ game.world.features_cache[pos.x.floor.to_i, pos.y.floor.to_i] = feature
+ end
+end
+
+# ---
+# Services
+
+# Position in the world
+class Pos
+ super Point[Float]
+
+ # Add `self` to `other` and return the new position
+ fun +(other: Point[Float]): Pos
+ do
+ var nx = other.x.add(x)
+ var ny = other.y.add(y)
+ return new Pos(x.value_of(nx), y.value_of(ny))
+ end
+end
+
+redef universal Int
+ # Is `self` in between `a` and `b`?
+ #
+ # ~~~
+ # assert 1.in_between_floats(0.0, 2.0)
+ # assert 1.in_between_floats(2.0, 0.0)
+ # assert not 1.in_between_floats(2.0, 4.0)
+ # ~~~
+ fun in_between_floats(a, b: Float): Bool
+ do
+ var f = to_f
+ if a < b then return a.floor - 1.0 < f and f < b.ceil
+ return a.ceil > f and f > b.floor - 1.0
+ end
+end
+
+redef universal Float
+ # Get the vector with `self` as direction and the given `magnitude`
+ fun to_vector(magnitude: Float): Pos
+ do
+ return new Pos(cos*magnitude, sin*magnitude)
+ end
+end
+
+redef class Range[E]
+ # Step appropriately to go from `first` to `last`
+ #
+ # ~~~
+ # assert [1..3].smart_step.to_a == [1, 2, 3]
+ # assert [3..1].smart_step.to_a == [3, 2, 1]
+ # ~~~
+ fun smart_step: Iterator[E]
+ do
+ var step = 1
+ if first > last then step = -1
+ return self.step(step)
+ end
+end