From: Alexis Laferrière Date: Thu, 2 Jul 2015 00:13:49 +0000 (-0400) Subject: contrib/tinks: add the game logic X-Git-Tag: v0.7.8~106^2~2^2~9 X-Git-Url: http://nitlanguage.org contrib/tinks: add the game logic Signed-off-by: Alexis Laferrière --- diff --git a/contrib/tinks/src/game/framework.nit b/contrib/tinks/src/game/framework.nit new file mode 100644 index 0000000..49c74b0 --- /dev/null +++ b/contrib/tinks/src/game/framework.nit @@ -0,0 +1,138 @@ +# 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 diff --git a/contrib/tinks/src/game/game.nit b/contrib/tinks/src/game/game.nit new file mode 100644 index 0000000..06a0a09 --- /dev/null +++ b/contrib/tinks/src/game/game.nit @@ -0,0 +1,18 @@ +# 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 diff --git a/contrib/tinks/src/game/players.nit b/contrib/tinks/src/game/players.nit new file mode 100644 index 0000000..ac5bb76 --- /dev/null +++ b/contrib/tinks/src/game/players.nit @@ -0,0 +1,149 @@ +# 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 diff --git a/contrib/tinks/src/game/powerups.nit b/contrib/tinks/src/game/powerups.nit new file mode 100644 index 0000000..2c7cd5b --- /dev/null +++ b/contrib/tinks/src/game/powerups.nit @@ -0,0 +1,91 @@ +# 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 diff --git a/contrib/tinks/src/game/tanks.nit b/contrib/tinks/src/game/tanks.nit new file mode 100644 index 0000000..9d82965 --- /dev/null +++ b/contrib/tinks/src/game/tanks.nit @@ -0,0 +1,384 @@ +# 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 diff --git a/contrib/tinks/src/game/world.nit b/contrib/tinks/src/game/world.nit new file mode 100644 index 0000000..a9e1844 --- /dev/null +++ b/contrib/tinks/src/game/world.nit @@ -0,0 +1,276 @@ +# 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