]> git.r.bdr.sh - rbdr/sumo/commitdiff
Add Naive Game Rules (#11)
authorRubén Beltrán del Río <redacted>
Tue, 29 May 2018 07:34:38 +0000 (02:34 -0500)
committerGitHub <redacted>
Tue, 29 May 2018 07:34:38 +0000 (02:34 -0500)
* Add components for tracking points

* Add methods to create new entities

* Adds nodes for points

* Add system to detect points

* Adds system to detect a winner

* Add system to render points and winner

* Add points required to win to config

* Add it all to the engine

* Add mention of points to changelog

14 files changed:
CHANGELOG.md
lib/components/points.js [new file with mode: 0644]
lib/components/points_collider.js [new file with mode: 0644]
lib/components/winner.js [new file with mode: 0644]
lib/config.js
lib/factories/sumo.js
lib/nodes/points.js [new file with mode: 0644]
lib/nodes/points_collider.js [new file with mode: 0644]
lib/nodes/winner.js [new file with mode: 0644]
lib/sumo.js
lib/systems/detect_points_collision.js [new file with mode: 0644]
lib/systems/detect_winner.js [new file with mode: 0644]
lib/systems/render_points.js [new file with mode: 0644]
lib/systems/render_winner.js [new file with mode: 0644]

index c64a8fef2268d338b892073c6a87534dd3f4d8a0..f3b119fb91a73040b20414a6b23e779c0ca1e7db 100644 (file)
@@ -20,5 +20,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 - Keyboard Support
 - Dash & grab mechanics
 - Add support for Gamepads (Tested using PS4 only)
 - Keyboard Support
 - Dash & grab mechanics
 - Add support for Gamepads (Tested using PS4 only)
+- Add points keeping system
 
 [Unreleased]: https://github.com/rbdr/sumo/compare/master...develop
 
 [Unreleased]: https://github.com/rbdr/sumo/compare/master...develop
diff --git a/lib/components/points.js b/lib/components/points.js
new file mode 100644 (file)
index 0000000..02b7e44
--- /dev/null
@@ -0,0 +1,11 @@
+import { Component } from '@serpentity/serpentity';
+
+/**
+ * Component that stores points in arbitrary labels
+ *
+ * @extends {external:Serpentity.Component}
+ * @class PointsComponent
+ * @param {object} config a configuration object to extend.
+ */
+export default class PointsComponent extends Component {};
+
diff --git a/lib/components/points_collider.js b/lib/components/points_collider.js
new file mode 100644 (file)
index 0000000..ceec7ee
--- /dev/null
@@ -0,0 +1,35 @@
+import { Component } from '@serpentity/serpentity';
+
+/**
+ * Component that stores a collision target and points accumulation
+ * target
+ *
+ * @extends {external:Serpentity.Component}
+ * @class PointsColliderComponent
+ * @param {object} config a configuration object to extend.
+ */
+export default class PointsColliderComponent extends Component {
+  constructor(config) {
+
+    super(config);
+
+    /**
+     * The target entity that will generate points if it collides
+     *
+     * @property {external:Serpentity.Entity} collisionTarget
+     * @instance
+     * @memberof PointsColliderComponent
+     */
+    this.collisionTarget = this.collisionTarget || null;
+
+    /**
+     * The target entity that will generate points if it collides
+     *
+     * @property {string} pointsTarget
+     * @instance
+     * @memberof PointsColliderComponent
+     */
+    this.pointsTarget = this.pointsTarget || 'nobody';
+  }
+};
+
diff --git a/lib/components/winner.js b/lib/components/winner.js
new file mode 100644 (file)
index 0000000..74de1c8
--- /dev/null
@@ -0,0 +1,26 @@
+import { Component } from '@serpentity/serpentity';
+
+/**
+ * Component that stores a winner
+ *
+ * @extends {external:Serpentity.Component}
+ * @class WinnerComponent
+ * @param {object} config a configuration object to extend.
+ */
+export default class WinnerComponent extends Component {
+  constructor(config) {
+
+    super(config);
+
+    /**
+     * The properthy that holds the winner
+     *
+     * @property {string} winner
+     * @instance
+     * @memberof WionnerComponent
+     */
+    this.winner = this.winner || null;
+  }
+};
+
+
index 856fc1879193d592eb6c2ce710bf6c60ae67c867..c39fedf2ea2fb7478757135293586e3536e2d0db 100644 (file)
@@ -28,14 +28,22 @@ const config = {
    * @property {number} verticalResolution
    * @memberof Config
    */
    * @property {number} verticalResolution
    * @memberof Config
    */
-  verticalResolution: 224
+  verticalResolution: 224,
+
+  /**
+   * The points needed to win
+   *
+   * @property {number} maxPoints
+   * @memberof Config
+   */
+  maxPoints: 200
 
 };
 
 /**
 
 };
 
 /**
- * How many pixels to use per meter
+ * The horizontal resolution of the screen
  *
  *
- * @property {number} meterSize
+ * @property {number} horizontalResolution
  * @memberof Config
  */
 config.horizontalResolution = Math.round(config.verticalResolution * config.aspectRatio[0] / config.aspectRatio[1]);
  * @memberof Config
  */
 config.horizontalResolution = Math.round(config.verticalResolution * config.aspectRatio[0] / config.aspectRatio[1]);
index 18e9d72dfafacfd37455658b5cddc40f0db2bf14..009ba8cbbad903be63b37934743d69af4d36da05 100644 (file)
@@ -14,8 +14,11 @@ import GrabAreaComponent from '../components/grab_area';
 import GrabbableComponent from '../components/grabbable';
 import GrabComponent from '../components/grab';
 import MaxVelocityComponent from '../components/max_velocity';
 import GrabbableComponent from '../components/grabbable';
 import GrabComponent from '../components/grab';
 import MaxVelocityComponent from '../components/max_velocity';
+import PointsColliderComponent from '../components/points_collider';
+import PointsComponent from '../components/points';
 import PositionComponent from '@serpentity/components.position';
 import PixiContainerComponent from '../components/pixi_container';
 import PositionComponent from '@serpentity/components.position';
 import PixiContainerComponent from '../components/pixi_container';
+import WinnerComponent from '../components/winner';
 
 import PixiFactory from '../factories/pixi';
 import Config from '../config';
 
 import PixiFactory from '../factories/pixi';
 import Config from '../config';
@@ -620,14 +623,17 @@ export default {
     const friction = 0;
     const frictionStatic = 0;
     const restitution = 1;
     const friction = 0;
     const frictionStatic = 0;
     const restitution = 1;
+    const label = config.label || 'Invisible Block';
+    const isSensor = !!(config.isSensor);
 
     const body = Bodies.rectangle(position.x / Config.meterSize,
       position.y / Config.meterSize,
       config.width / Config.meterSize,
       config.height / Config.meterSize,
       {
 
     const body = Bodies.rectangle(position.x / Config.meterSize,
       position.y / Config.meterSize,
       config.width / Config.meterSize,
       config.height / Config.meterSize,
       {
+        isSensor,
         isStatic: true,
         isStatic: true,
-        label: 'Invisible Block',
+        label,
         friction,
         restitution,
         frictionStatic
         friction,
         restitution,
         frictionStatic
@@ -638,6 +644,61 @@ export default {
       engine.addEntity(entity);
     }
 
       engine.addEntity(entity);
     }
 
+    return entity;
+  },
+
+  /**
+   * Creates an invisible block that accumulates points if certain
+   * entity collids with it
+   *
+   * @function createPointsCollider
+   * @memberof SumoFactory
+   * @param {external:Serpentity} [engine] the serpentity engine to attach
+   * to. If not sent, it will not be attached.
+   * @param {object} [config] the config to override the entity, accepts
+   * the key `position` as an object with an x and y property.
+   * @return {external:Serpentity.Entity} the created entity
+   */
+  createPointsCollider(engine, config = {}) {
+
+    const entity = this.createInvisibleBlock(null, Object.assign({
+      isSensor: true,
+      label: 'Points Detector'
+    }, config));
+
+    // Points Collider
+
+    entity.addComponent(new PointsColliderComponent(config));
+
+    if (engine) {
+      engine.addEntity(entity);
+    }
+
+    return entity;
+  },
+
+  /**
+   * Creates an entity representing the game state
+   *
+   * @function createGameState
+   * @memberof SumoFactory
+   * @param {external:Serpentity} [engine] the serpentity engine to attach
+   * to. If not sent, it will not be attached.
+   * @param {object} [config] the config to override the entity, accepts
+   * the key `position` as an object with an x and y property.
+   * @return {external:Serpentity.Entity} the created entity
+   */
+  createGameState(engine, config = {}) {
+
+    const entity = new Entity();
+
+    entity.addComponent(new PointsComponent(config));
+    entity.addComponent(new WinnerComponent(config));
+
+    if (engine) {
+      engine.addEntity(entity);
+    }
+
     return entity;
   }
 };
     return entity;
   }
 };
diff --git a/lib/nodes/points.js b/lib/nodes/points.js
new file mode 100644 (file)
index 0000000..40404b4
--- /dev/null
@@ -0,0 +1,25 @@
+import { Node } from '@serpentity/serpentity';
+
+import PointsComponent from '../components/points';
+
+/**
+ * Node identifying an entity that stores points
+ *
+ * @extends {external:Serpentity.Node}
+ * @class PointsNode
+ */
+export default class PointsNode extends Node {
+
+};
+
+/**
+ * Holds the types that are used to identify a points keeping entity
+ *
+ * @property {object} types
+ * @name types
+ * @memberof PointsNode
+ */
+PointsNode.types = {
+  points: PointsComponent
+};
+
diff --git a/lib/nodes/points_collider.js b/lib/nodes/points_collider.js
new file mode 100644 (file)
index 0000000..4b8fe92
--- /dev/null
@@ -0,0 +1,29 @@
+import { Node } from '@serpentity/serpentity';
+
+import BodyComponent from '../components/body';
+import PointsColliderComponent from '../components/points_collider';
+
+/**
+ * Node identifying an entity that can identify a collision and give out
+ * points
+ *
+ * @extends {external:Serpentity.Node}
+ * @class PointsColliderNode
+ */
+export default class PointsColliderNode extends Node {
+
+};
+
+/**
+ * Holds the types that are used to identify an entity that can give out
+ * points on collision
+ *
+ * @property {object} types
+ * @name types
+ * @memberof PhysicalWithAttributesNode
+ */
+PointsColliderNode.types = {
+  body: BodyComponent,
+  pointsCollider: PointsColliderComponent
+};
+
diff --git a/lib/nodes/winner.js b/lib/nodes/winner.js
new file mode 100644 (file)
index 0000000..8c5a448
--- /dev/null
@@ -0,0 +1,24 @@
+import { Node } from '@serpentity/serpentity';
+
+import WinnerComponent from '../components/winner';
+
+/**
+ * Node identifying an entity that stores winner
+ *
+ * @extends {external:Serpentity.Node}
+ * @class WinnerNode
+ */
+export default class WinnerNode extends Node {
+
+};
+
+/**
+ * Holds the types that are used to identify a winner keeping entity
+ *
+ * @property {object} types
+ * @name types
+ * @memberof WinnerNode
+ */
+WinnerNode.types = {
+  winner: WinnerComponent
+};
index cdf8cc65bdb73e98e8d70ad659dd9756178bc7b0..835e910ff48c18ea256af2bf9eca8d4e9e42caa8 100644 (file)
@@ -8,13 +8,17 @@ import ApplyForceSystem from './systems/apply_force';
 import CreateCouplingLineSystem from './systems/create_coupling_line';
 import ControlMapperSystem from './systems/control_mapper';
 import DashSystem from './systems/dash';
 import CreateCouplingLineSystem from './systems/create_coupling_line';
 import ControlMapperSystem from './systems/control_mapper';
 import DashSystem from './systems/dash';
+import DetectPointsCollisionSystem from './systems/detect_points_collision';
+import DetectWinnerSystem from './systems/detect_winner';
 import DrawDashSystem from './systems/draw_dash';
 import DrawGrabSystem from './systems/draw_grab';
 import ElasticSystem from './systems/elastic';
 import GrabSystem from './systems/grab';
 import PhysicsWorldControlSystem from './systems/physics_world_control';
 import PhysicsToAttributesSystem from './systems/physics_to_attributes';
 import DrawDashSystem from './systems/draw_dash';
 import DrawGrabSystem from './systems/draw_grab';
 import ElasticSystem from './systems/elastic';
 import GrabSystem from './systems/grab';
 import PhysicsWorldControlSystem from './systems/physics_world_control';
 import PhysicsToAttributesSystem from './systems/physics_to_attributes';
+import RenderPointsSystem from './systems/render_points';
 import RenderSystem from './systems/render';
 import RenderSystem from './systems/render';
+import RenderWinnerSystem from './systems/render_winner';
 import AttributesToRenderableSystem from './systems/attributes_to_renderable';
 
 // Factories
 import AttributesToRenderableSystem from './systems/attributes_to_renderable';
 
 // Factories
@@ -209,6 +213,18 @@ internals.Sumo = class Sumo {
       engine: this._matterJs
     }));
 
       engine: this._matterJs
     }));
 
+    this._engine.addSystem(new DetectPointsCollisionSystem());
+
+    this._engine.addSystem(new DetectWinnerSystem());
+
+    this._engine.addSystem(new RenderPointsSystem({
+      application: this._pixi
+    }));
+
+    this._engine.addSystem(new RenderWinnerSystem({
+      application: this._pixi
+    }));
+
     this._engine.addSystem(new ElasticSystem());
 
     this._engine.addSystem(new PhysicsToAttributesSystem());
     this._engine.addSystem(new ElasticSystem());
 
     this._engine.addSystem(new PhysicsToAttributesSystem());
@@ -268,6 +284,8 @@ internals.Sumo = class Sumo {
       entityB: harness
     });
 
       entityB: harness
     });
 
+    // Walls
+
     SumoFactory.createInvisibleBlock(this._engine, {
       width: this.horizontalResolution * 2,
       height: this.verticalResolution * 0.1,
     SumoFactory.createInvisibleBlock(this._engine, {
       width: this.horizontalResolution * 2,
       height: this.verticalResolution * 0.1,
@@ -286,6 +304,33 @@ internals.Sumo = class Sumo {
       }
     });
 
       }
     });
 
+    // Points Detector
+
+    SumoFactory.createPointsCollider(this._engine, {
+      collisionTarget: sumoA,
+      pointsTarget: 'red',
+      height: this.verticalResolution,
+      width: this.horizontalResolution,
+      position: {
+        x: this.horizontalResolution + this.horizontalResolution / 2,
+        y: this.verticalResolution / 2
+      }
+    });
+
+    SumoFactory.createPointsCollider(this._engine, {
+      collisionTarget: sumoB,
+      pointsTarget: 'blue',
+      height: this.verticalResolution,
+      width: this.horizontalResolution,
+      position: {
+        x: -this.horizontalResolution / 2,
+        y: this.verticalResolution / 2
+      }
+    });
+
+    // The game state
+    SumoFactory.createGameState(this._engine);
+
     // To keep the coupling behind, we'll manually add the sumos later
 
     this._engine.addEntity(sumoA);
     // To keep the coupling behind, we'll manually add the sumos later
 
     this._engine.addEntity(sumoA);
diff --git a/lib/systems/detect_points_collision.js b/lib/systems/detect_points_collision.js
new file mode 100644 (file)
index 0000000..c84f3fc
--- /dev/null
@@ -0,0 +1,100 @@
+import { System } from '@serpentity/serpentity';
+import { SAT } from 'matter-js';
+
+import BodyComponent from '../components/body';
+import PointsColliderNode from '../nodes/points_collider';
+import PointsNode from '../nodes/points';
+
+/**
+ * Handles grabbing between entities
+ *
+ * @extends {external:Serpentity.System}
+ * @class DetectPointsCollisionSystem
+ * @param {object} config a configuration object to extend.
+ */
+export default class DetectPointsCollisionSystem extends System {
+
+  constructor(config = {}) {
+
+    super();
+
+    /**
+     * The collection of points colliders
+     *
+     * @property {external:Serpentity.NodeCollection} pointsColliders
+     * @instance
+     * @memberof DetectPointsCollisionSystem
+     */
+    this.pointsColliders = null;
+
+    /**
+     * The collection of points keepers
+     *
+     * @property {external:Serpentity.NodeCollection} pointsKeepers
+     * @instance
+     * @memberof DetectPointsCollisionSystem
+     */
+    this.pointsKeepers = null;
+  }
+
+  /**
+   * Initializes system when added. Requests points colliders and points
+   * accumulators
+   *
+   * @function added
+   * @memberof DetectPointsCollisionSystem
+   * @instance
+   * @param {external:Serpentity.Engine} engine the serpentity engine to
+   * which we are getting added
+   */
+  added(engine) {
+
+    this.pointsCollider = engine.getNodes(PointsColliderNode);
+    this.pointsKeepers = engine.getNodes(PointsNode);
+  }
+
+  /**
+   * Clears system resources when removed.
+   *
+   * @function removed
+   * @instance
+   * @memberof DetectPointsCollisionSystem
+   */
+  removed() {
+
+    this.pointsCollider = null;
+    this.pointsKeepers = null;
+  }
+
+  /**
+   * Runs on every update of the loop. Triggers grab and manages cooldown
+   *
+   * @function update
+   * @instance
+   * @param {Number} currentFrameDuration the duration of the current
+   * frame
+   * @memberof DetectPointsCollisionSystem
+   */
+  update(currentFrameDuration) {
+
+    const points = {};
+
+    for (const collider of this.pointsCollider) {
+      const collisionTargetBody = collider.pointsCollider.collisionTarget.getComponent(BodyComponent);
+      const pointsTarget = collider.pointsCollider.pointsTarget;
+
+      if (collisionTargetBody) {
+        const collision = SAT.collides(collider.body.body, collisionTargetBody.body);
+        if (collision.collided) {
+          points[pointsTarget] = (points[pointsTarget] || 0) + 1;
+        }
+      }
+    }
+
+    for (const pointsKeeper of this.pointsKeepers) {
+      for (const pointsLabel of Object.keys(points)) {
+        pointsKeeper.points[pointsLabel] = (pointsKeeper.points[pointsLabel] || 0) + points[pointsLabel];
+      }
+    }
+  }
+};
diff --git a/lib/systems/detect_winner.js b/lib/systems/detect_winner.js
new file mode 100644 (file)
index 0000000..e7fbd76
--- /dev/null
@@ -0,0 +1,104 @@
+import { System } from '@serpentity/serpentity';
+
+import WinnerNode from '../nodes/winner';
+import PointsNode from '../nodes/points';
+
+import Config from '../config';
+
+/**
+ * Handles finding the winner
+ *
+ * @extends {external:Serpentity.System}
+ * @class DetectWinnerSystem
+ * @param {object} config a configuration object to extend.
+ */
+export default class DetectWinnerSystem extends System {
+
+  constructor(config = {}) {
+
+    super();
+
+    /**
+     * The collection of points keepers
+     *
+     * @property {external:Serpentity.NodeCollection} pointsKeepers
+     * @instance
+     * @memberof DetectWinnerSystem
+     */
+    this.pointsKeepers = null;
+
+    /**
+     * The collection of winner keepers
+     *
+     * @property {external:Serpentity.NodeCollection} winnerKeepers
+     * @instance
+     * @memberof DetectWinnerSystem
+     */
+    this.winnerKeepers = null;
+  }
+
+  /**
+   * Initializes system when added. Requests points and winner keepers
+   *
+   * @function added
+   * @memberof DetectWinnerSystem
+   * @instance
+   * @param {external:Serpentity.Engine} engine the serpentity engine to
+   * which we are getting added
+   */
+  added(engine) {
+
+    this.pointsKeepers = engine.getNodes(PointsNode);
+    this.winnerKeepers = engine.getNodes(WinnerNode);
+  }
+
+  /**
+   * Clears system resources when removed.
+   *
+   * @function removed
+   * @instance
+   * @memberof DetectWinnerSystem
+   */
+  removed() {
+
+    this.pointsKeepers = null;
+    this.winnerKeepers = null;
+  }
+
+  /**
+   * Runs on every update of the loop. Checks if someone won
+   *
+   * @function update
+   * @instance
+   * @param {Number} currentFrameDuration the duration of the current
+   * frame
+   * @memberof DetectWinnerSystem
+   */
+  update(currentFrameDuration) {
+
+    let winner = null;
+
+    for (const pointsKeeper of this.pointsKeepers) {
+      for (const pointsCandidate of Object.keys(pointsKeeper.points)) {
+        if (pointsKeeper.points[pointsCandidate] >= Config.maxPoints) {
+          winner = pointsCandidate;
+          break;
+        }
+      }
+    }
+
+    if (!winner) {
+      return;
+    }
+
+    for (const winnerKeeper of this.winnerKeepers) {
+
+      // Only one winner, another system should reset
+      if (!winnerKeeper.winner.winner) {
+        winnerKeeper.winner.winner = winner;
+      }
+
+    }
+  }
+};
+
diff --git a/lib/systems/render_points.js b/lib/systems/render_points.js
new file mode 100644 (file)
index 0000000..f7fd410
--- /dev/null
@@ -0,0 +1,114 @@
+import { System } from '@serpentity/serpentity';
+import { Graphics } from 'pixi.js';
+
+import Config from '../config';
+import PointsNode from '../nodes/points';
+
+const internals = {
+  kNoPixiError: 'No pixi application passed to render system. Make sure you set the `application` key in the config object when initializing.',
+  redBar: new Graphics(),
+  blueBar: new Graphics()
+};
+
+/**
+ * Renders points assuming there exist a "red" and a "blue" score
+ *
+ * @extends {external:Serpentity.System}
+ * @class RenderPointsSystem
+ * @param {object} config a configuration object to extend.
+ */
+export default class RenderPointsSystem extends System {
+
+  constructor(config = {}) {
+
+    super();
+
+    /**
+     * The node collection of points keepers
+     *
+     * @property {external:Serpentity.NodeCollection} pointsKeepers
+     * @instance
+     * @memberof RenderPointsSystem
+     */
+    this.pointsKeepers = null;
+
+    /**
+     * The pixi engine we will use to render
+     *
+     * @property {external:PixiJs.Application} application
+     * @instance
+     * @memberof RenderPointsSystem
+     */
+    this.application = config.application;
+
+    if (!this.application) {
+      throw new Error(internals.kNoPixiError);
+    }
+  }
+
+  /**
+   * Initializes system when added. Requests renderable nodes and
+   * attaches to event listeners to add / remove them to pixi stage
+   *
+   * @function added
+   * @memberof RenderPointsSystem
+   * @instance
+   * @param {external:Serpentity.Engine} engine the serpentity engine to
+   * which we are getting added
+   */
+  added(engine) {
+
+    this.pointsKeepers = engine.getNodes(PointsNode);
+    this.application.stage.addChild(internals.blueBar);
+    this.application.stage.addChild(internals.redBar);
+  }
+
+  /**
+   * Clears system resources when removed.
+   *
+   * @function removed
+   * @instance
+   * @memberof RenderPointsSystem
+   */
+  removed() {
+
+    this.pointsKeepers = null;
+    this.application.stage.removeChild(internals.blueBar);
+    this.application.stage.removeChild(internals.redBar);
+  }
+
+  /**
+   * Runs on every update of the loop. Updates the bars
+   *
+   * @function update
+   * @instance
+   * @param {Number} currentFrameDuration the duration of the current
+   * frame
+   * @memberof RenderPointsSystem
+   */
+  update(currentFrameDuration) {
+
+    for (const pointsKeeper of this.pointsKeepers) {
+
+      const redPoints = pointsKeeper.points.red;
+      const bluePoints = pointsKeeper.points.blue;
+
+      const redBarHeight = redPoints * Config.verticalResolution / Config.maxPoints;
+      const blueBarHeight = bluePoints * Config.verticalResolution / Config.maxPoints;
+
+      internals.redBar.removeChildren();
+      const redBar = new Graphics();
+      redBar.lineStyle(20, 0xeaacac, 0.75)
+        .moveTo(Config.horizontalResolution, 0)
+        .lineTo(Config.horizontalResolution, redBarHeight);
+      internals.redBar.addChild(redBar);
+
+      internals.blueBar.removeChildren();
+      const blueBar = new Graphics();
+      blueBar.lineStyle(20, 0x87c5ea, 0.75)
+        .moveTo(0, 0)
+        .lineTo(0, blueBarHeight);
+      internals.blueBar.addChild(blueBar);
+    }
+  }
+};
diff --git a/lib/systems/render_winner.js b/lib/systems/render_winner.js
new file mode 100644 (file)
index 0000000..53f3602
--- /dev/null
@@ -0,0 +1,114 @@
+import { System } from '@serpentity/serpentity';
+import { Text } from 'pixi.js';
+
+import Config from '../config';
+import WinnerNode from '../nodes/winner';
+
+const internals = {
+  kNoPixiError: 'No pixi application passed to render system. Make sure you set the `application` key in the config object when initializing.'
+};
+
+/**
+ * Renders a winner if there is one
+ *
+ * @extends {external:Serpentity.System}
+ * @class RenderWinnerSystem
+ * @param {object} config a configuration object to extend.
+ */
+export default class RenderWinnerSystem extends System {
+
+  constructor(config = {}) {
+
+    super();
+
+    /**
+     * The keepers of the winner
+     *
+     * @property {external:Serpentity.NodeCollection} pointsKeepers
+     * @instance
+     * @memberof RenderWinnerSystem
+     */
+    this.winnerKeepers = null;
+
+    /**
+     * The pixi engine we will use to render
+     *
+     * @property {external:PixiJs.Application} application
+     * @instance
+     * @memberof RenderWinnerSystem
+     */
+    this.application = config.application;
+
+    if (!this.application) {
+      throw new Error(internals.kNoPixiError);
+    }
+  }
+
+  /**
+   * Initializes system when added. Requests winner keepers
+   *
+   * @function added
+   * @memberof RenderWinnerSystem
+   * @instance
+   * @param {external:Serpentity.Engine} engine the serpentity engine to
+   * which we are getting added
+   */
+  added(engine) {
+
+    this.winnerKeepers = engine.getNodes(WinnerNode);
+  }
+
+  /**
+   * Clears system resources when removed.
+   *
+   * @function removed
+   * @instance
+   * @memberof RenderWinnerSystem
+   */
+  removed() {
+
+    this.pointsKeepers = null;
+    if (internals.winnerText) {
+      this.application.stage.removeChild(internals.winnerText);
+      internals.winnerText = null;
+    }
+  }
+
+  /**
+   * Runs on every update of the loop. Updates the bars
+   *
+   * @function update
+   * @instance
+   * @param {Number} currentFrameDuration the duration of the current
+   * frame
+   * @memberof RenderWinnerSystem
+   */
+  update(currentFrameDuration) {
+
+    // Right now this is final, once a winner is rendered you would need
+    // to restart the whole system.
+    if (internals.winnerText) {
+      return;
+    }
+
+    for (const winnerKeeper of this.winnerKeepers) {
+      const winner = winnerKeeper.winner.winner;
+      if (winner) {
+        const message = `${winner} has won`;
+        internals.winnerText = new Text(message, {
+          fontFamily: 'Arial',
+          fontSize: 96,
+          fontWeight: 'bold',
+          fill: 0xffffff,
+          align: 'center'
+        });
+        internals.winnerText.position.x = Config.horizontalResolution / 2;
+        internals.winnerText.position.y = Config.verticalResolution / 2;
+        internals.winnerText.anchor.set(0.5);
+        this.application.stage.addChild(internals.winnerText);
+        return;
+      }
+    }
+  }
+};
+