diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,8 @@
+# Changelog
+
+This project adheres to [Haskell PVP](https://pvp.haskell.org/).
+
+
+## 0.1.0.2 [2018-01-01]
+
+- Initial release
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Olivier Sohn (c) 2017 - 2018
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Olivier Sohn nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,74 @@
+# What is it?
+
+It's a terminal ascii game where you fly a ship through numbers. The goal is to shoot
+the numbers whose sum will be equal to the level's target. It's easy at the beginning,
+but higher levels have more and more numbers, and less and less space to navigate through them!
+The last level is level 12: I never reached it, but I hope somebody will :).
+
+# Demos
+
+## No walls, square world
+
+[![asciicast](https://asciinema.org/a/151434.png)](https://asciinema.org/a/151434)
+
+## Random walls, rectangular world
+
+[![asciicast](https://asciinema.org/a/151404.png)](https://asciinema.org/a/151404)
+
+# Configurability
+
+The game can be configured in "world shape" (square, rectangle) and "kind of walls"
+(none, deterministic, random).
+
+![Configuration snapshot](images/config.png?raw=true "Configuration")
+
+You can define your own keyboard mapping by modifying the 'eventFromKey' function
+defined [here](src/Game/Event.hs), the default mapping being:
+- ship acceleration : 's' 'e' 'd' 'f'
+- laser shots       : 'j' 'i' 'k' 'l'
+
+# Supported Platforms / Terminals:
+
+|OS       |Support|
+|---------|-------|
+|OS X     |Yes    |
+|Linux    |Yes    |
+|Windows  |No (see [this](https://ghc.haskell.org/trac/ghc/ticket/7353)) |
+
+Your terminal window should have a dimension of at least {height = 42, width = 146}.
+If it is too small, the program will fail with the following error message:
+
+```
+From game thread:
+
+Minimum terminal size : Window {height = 42, width = 146}.
+Current terminal size : Window {height = 22, width = 165}.
+The current terminal size doesn't match the minimum size,
+please adjust your terminal size and restart the executable.
+```
+
+# Version history
+- 2.1 :
+  - New animations :
+    - With colors (8-bit)
+    - Also between levels
+    - Using physics and gravity
+    - "Terminal size"-aware
+- 2.0 :
+  - World is configurable (square or rectangle, with or without random walls)
+  - Explosion animations
+  - Optimized rendering (delta rendering)
+- 1.0 :
+  - The world is a square. (Note : ship acceleration was 'w' 'a' 's' 'd' at that time)
+
+# Build
+
+You can build and run using [stack](https://docs.haskellstack.org):
+
+`stack build --pedantic && stack exec hamazed-exe`
+
+# Credits
+
+## Delta rendering
+
+The initial idea for delta rendering is based on [code written by Rafael Ibraim](https://gist.github.com/ibraimgm/40e307d70feeb4f117cd)
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,9 @@
+module Main where
+
+import           Control.Monad(void)
+
+import           Imj.Game.Hamazed( run )
+
+main :: IO ()
+main =
+  void run
diff --git a/imj-game-hamazed.cabal b/imj-game-hamazed.cabal
new file mode 100644
--- /dev/null
+++ b/imj-game-hamazed.cabal
@@ -0,0 +1,106 @@
+name:                imj-game-hamazed
+version:             0.1.0.2
+Category:            Animation, Game, Graphics, Education, Application
+Synopsis:            A game with flying numbers and 8-bit color animations.
+Description:         In Hamazed, you are a 'BattleShip' pilot surrounded by flying 'Number's.
+                     .
+
+                     Your mission is to shoot exactly the 'Number's whose sum will equate the
+                     current 'Level' 's /target number/.
+                     .
+
+                     The higher the 'Level' (1..12), the more 'Number's are flying around (up-to 16).
+                     And the smaller the 'World' gets.
+                     .
+
+                     Good luck !
+homepage:            https://github.com/OlivierSohn/hamazed/blob/master/imj-game-hamazed//README.md
+bug-reports:         https://github.com/OlivierSohn/hamazed/issues/
+license:             BSD3
+license-file:        LICENSE
+author:              Olivier Sohn
+maintainer:          olivier.sohn@gmail.com
+copyright:           2017 - 2018 Olivier Sohn
+build-type:          Simple
+extra-source-files:  README.md CHANGELOG.md
+cabal-version:       >=1.10
+
+Tested-With: GHC == 8.0.2, GHC == 8.2.2
+
+library
+  hs-source-dirs:      src
+  other-modules:
+  exposed-modules:     Imj.Game.Hamazed
+                     , Imj.Game.Hamazed.Color
+                     , Imj.Game.Hamazed.Env
+                     , Imj.Game.Hamazed.KeysMaps
+                     , Imj.Game.Hamazed.Level
+                     , Imj.Game.Hamazed.Level.Types
+                     , Imj.Game.Hamazed.Parameters
+                     , Imj.Game.Hamazed.Infos
+                     , Imj.Game.Hamazed.Loop.Create
+                     , Imj.Game.Hamazed.Loop.Deadlines
+                     , Imj.Game.Hamazed.Loop.Event.Priorities
+                     , Imj.Game.Hamazed.Loop.Event.Types
+                     , Imj.Game.Hamazed.Loop.Event
+                     , Imj.Game.Hamazed.Loop.Render
+                     , Imj.Game.Hamazed.Loop.Run
+                     , Imj.Game.Hamazed.Loop.Timing
+                     , Imj.Game.Hamazed.Loop.Update
+                     , Imj.Game.Hamazed.Types
+                     , Imj.Game.Hamazed.World
+                     , Imj.Game.Hamazed.World.Create
+                     , Imj.Game.Hamazed.World.InTerminal
+                     , Imj.Game.Hamazed.World.Number
+                     , Imj.Game.Hamazed.World.Render
+                     , Imj.Game.Hamazed.World.Ship
+                     , Imj.Game.Hamazed.World.Size
+                     , Imj.Game.Hamazed.World.Space
+                     , Imj.Game.Hamazed.World.Space.Types
+                     , Imj.Game.Hamazed.World.Types
+  build-depends:       base >= 4.8 && < 4.11
+                     , containers >= 0.5.9.2
+                     , matrix >= 0.3.5.0
+                     , mtl >= 2.2.1 && < 2.3
+                     , terminal-size >= 0.3.2.1 && < 0.3.3
+                     , text ==1.2.*
+                     , vector >= 0.12.0.1 && < 0.12.1
+                     , imj-animation ==0.1.*
+                     , imj-animation ==0.1.*
+                     , imj-base ==0.1.*
+                     , imj-prelude ==0.1.*
+
+  ghc-options:       -Wall -fpedantic-bottoms -Wredundant-constraints
+                     -fexcess-precision -optc-ffast-math
+  default-language:    Haskell2010
+
+executable imj-game-hamazed-exe
+  hs-source-dirs:      app
+  main-is:             Main.hs
+  ghc-options:       -Wall -fpedantic-bottoms -Wredundant-constraints
+                     -threaded -rtsopts -with-rtsopts=-maxN4
+                     -fexcess-precision -optc-ffast-math
+  build-depends:       base >= 4.8 && < 4.11
+                     , imj-game-hamazed
+                     , imj-prelude ==0.1.*
+  default-language:    Haskell2010
+
+test-suite imj-game-hamazed-test
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  other-modules:       Test.Imj.Render
+  main-is:             Spec.hs
+  build-depends:       base >= 4.8 && < 4.11
+                     , imj-base ==0.1.*
+                     , imj-game-hamazed
+                     , mtl >= 2.2.1 && < 2.3
+                     , text ==1.2.*
+  ghc-options:       -Wall -fpedantic-bottoms -Wredundant-constraints
+                     -threaded -rtsopts -with-rtsopts=-maxN4
+                     -fexcess-precision -optc-ffast-math
+  default-language:    Haskell2010
+
+source-repository head
+  type:     git
+  location: https://github.com/OlivierSohn/hamazed/
+  subdir:   imj-game-hamazed
diff --git a/src/Imj/Game/Hamazed.hs b/src/Imj/Game/Hamazed.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed.hs
@@ -0,0 +1,84 @@
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE LambdaCase #-}
+
+
+module Imj.Game.Hamazed
+      ( -- * The game
+        {-| In Hamazed, you are a 'BattleShip' pilot surrounded by flying 'Number's.
+
+        Your mission is to shoot exactly the 'Number's whose sum will equate the
+        current 'Level' 's /target number/.
+
+        The higher the 'Level' (1..12), the more 'Number's are flying around (up-to 16).
+        And the smaller the 'World' gets.
+
+        Good luck !
+
+        /Note that to adapt the keyboard layout, you can modify 'eventFromKey'./
+        -}
+          run
+        -- * Game loop
+        {-| Hamazed is a /synchronous/, /event-driven/ program. Its /simplified/ main loop is:
+
+        * 'getNextDeadline'
+
+            * \(deadline\) = the next foreseen 'Deadline'.
+
+        * 'getEventForMaybeDeadline'
+
+            * \(event\) =
+
+                * a key-press occuring /before/ \(deadline\) expires
+                * or the \(deadline\) event
+
+        * 'update'
+
+            * Update 'GameState' according to \(event\)
+
+        * 'render'
+
+            * Render (using "Imj.Graphics.Render.Delta" to avoid
+            <https://en.wikipedia.org/wiki/Screen_tearing screen tearing>).
+        -}
+      , getNextDeadline
+      , getEventForMaybeDeadline
+      , update
+      , render
+        -- * Deadlines
+      , Deadline(..)
+      , DeadlineType(..)
+        -- * Events
+      , Event(..)
+      , ActionTarget(..)
+      , MetaAction(..)
+        -- * GameState
+        {-| 'GameState' has two fields of type 'World' : during 'Level' transitions,
+        we render the /old/ 'World' while using the /new/ 'World' 's
+        dimensions to animate the UI accordingly (see "Imj.Graphics.UI.Animation"). -} -- TODO this could be done differently
+      , GameState(..)
+        -- * Environment
+        {- | -}
+      , module Imj.Game.Hamazed.Env
+        -- * Keyboard layout
+      , eventFromKey
+        -- * Reexport
+      , module Imj.Game.Hamazed.World
+      , UIAnimation(..)
+      ) where
+
+import           Imj.Prelude
+
+import           Imj.Game.Hamazed.Color
+import           Imj.Game.Hamazed.Env
+import           Imj.Game.Hamazed.KeysMaps
+import           Imj.Game.Hamazed.Level
+import           Imj.Game.Hamazed.Level.Types
+import           Imj.Game.Hamazed.Loop.Deadlines
+import           Imj.Game.Hamazed.Loop.Event
+import           Imj.Game.Hamazed.Loop.Render
+import           Imj.Game.Hamazed.Loop.Run
+import           Imj.Game.Hamazed.Loop.Timing
+import           Imj.Game.Hamazed.Loop.Update
+import           Imj.Game.Hamazed.Parameters
+import           Imj.Game.Hamazed.Types
+import           Imj.Game.Hamazed.World
diff --git a/src/Imj/Game/Hamazed/Color.hs b/src/Imj/Game/Hamazed/Color.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/Color.hs
@@ -0,0 +1,86 @@
+{-# LANGUAGE NoImplicitPrelude #-}
+
+{-| This module defines the colors of every game element, except animations. -}
+
+module Imj.Game.Hamazed.Color (
+  -- * Ship colors
+    shipColor
+  , shipColors
+  , shipColorSafe
+  , shipColorsSafe
+  -- * Numbers colors
+  , numberColor
+  -- * Materials colors
+  , wallColors
+  , airColors
+  -- * UI colors
+  , worldFrameColors
+  , ammoColor
+  , bracketsColor
+  -- ** Text colors
+  , configColors
+  , messageColor
+  , neutralMessageColor
+  -- * Reexports
+  , module Imj.Graphics.Color
+  ) where
+
+import           Imj.Prelude
+
+import           Imj.Game.Hamazed.Level.Types
+import           Imj.Graphics.Color
+
+configColors :: LayeredColor
+configColors = LayeredColor (gray 0) (gray 8)
+
+wallColors :: LayeredColor
+wallColors = LayeredColor (gray 0) (gray 3)
+
+airColors :: LayeredColor
+airColors = LayeredColor black white
+
+neutralMessageColor :: LayeredColor
+neutralMessageColor = onBlack $ gray 10
+
+ammoColor :: Color8 Foreground
+ammoColor = gray 14
+
+bracketsColor :: Color8 Foreground
+bracketsColor = worldFrameFgColor
+
+messageColor :: GameStops -> LayeredColor
+messageColor Won      = onBlack $ rgb 4 3 1
+messageColor (Lost _) = onBlack $ gray 6
+
+shipColors :: LayeredColor
+shipColors = LayeredColor shipBgColor shipColor
+
+shipColorsSafe :: LayeredColor
+shipColorsSafe = LayeredColor shipBgColorSafe shipColorSafe
+
+shipColor :: Color8 Foreground
+shipColor = rgb 5 4 4
+
+shipColorSafe :: Color8 Foreground
+shipColorSafe = rgb 5 0 0
+
+shipBgColor :: Color8 Background
+shipBgColor = black
+
+shipBgColorSafe :: Color8 Background
+shipBgColorSafe = rgb 1 0 0
+
+-- | Cycles through the 6 colors of the cube delimited in RGB space by
+-- (5,4,1) and (5,5,3).
+numberColor :: Int -> LayeredColor
+numberColor i = onBlack $ rgb r g b
+  where
+    r = 5
+    g = fromIntegral $ 4 + (0 + quot i 2) `mod` 2 -- [0..1] , slow changes
+    b = fromIntegral $ 1 + (0 + quot i 1) `mod` 3 -- [0..2] , 2x faster changes
+
+worldFrameFgColor :: Color8 Foreground
+worldFrameFgColor = rgb 2 1 1
+
+worldFrameColors :: LayeredColor
+worldFrameColors = LayeredColor black worldFrameFgColor
diff --git a/src/Imj/Game/Hamazed/Env.hs b/src/Imj/Game/Hamazed/Env.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/Env.hs
@@ -0,0 +1,38 @@
+-- Environment of the Hamazed game, also used as an example in "Imj.Graphics.Render.Delta" :
+-- /from a MonadIO, MonadReader YourEnv monad/
+
+{-# OPTIONS_HADDOCK hide #-}
+
+module Imj.Game.Hamazed.Env(
+         Env
+       , createEnv
+       ) where
+
+import           Imj.Graphics.Class.Draw(Draw(..))
+import           Imj.Graphics.Class.Render(Render(..))
+import           Imj.Graphics.Render.Delta(newDefaultEnv, DeltaEnv)
+
+
+-- | The environment of <https://github.com/OlivierSohn/hamazed Hamazed> program
+newtype Env = Env {
+    _envDeltaEnv :: DeltaEnv
+}
+
+-- | Forwards to the 'Draw' instance of 'DeltaEnv'.
+instance Draw Env where
+  drawChar'      (Env a) = drawChar'      a
+  drawChars'     (Env a) = drawChars'     a
+  drawTxt'       (Env a) = drawTxt'       a
+  drawStr'       (Env a) = drawStr'       a
+  {-# INLINE drawChar' #-}
+  {-# INLINE drawChars' #-}
+  {-# INLINE drawTxt' #-}
+  {-# INLINE drawStr' #-}
+-- | Forwards to the 'Render' instance of 'DeltaEnv'.
+instance Render Env where
+  renderToScreen' (Env a) = renderToScreen' a
+  {-# INLINE renderToScreen' #-}
+
+-- | Constructor of 'Env'
+createEnv :: IO Env
+createEnv = Env <$> newDefaultEnv
diff --git a/src/Imj/Game/Hamazed/Infos.hs b/src/Imj/Game/Hamazed/Infos.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/Infos.hs
@@ -0,0 +1,69 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Imj.Game.Hamazed.Infos(
+        mkInfos
+      , mkLeftInfo
+      , InfoType(..)
+      ) where
+
+import           Imj.Prelude
+
+import           Data.Char( intToDigit )
+import           Data.List( length, foldl' )
+import           Data.Text(pack, singleton)
+
+import           Imj.Game.Hamazed.Color
+import           Imj.Game.Hamazed.Level.Types
+import           Imj.Graphics.Text.ColorString
+
+data InfoType = Normal | ColorAnimated
+
+mkLevelCS :: InfoType -> Int -> [ColorString]
+mkLevelCS t level =
+  let txt c = colored "Level " white <> colored (pack (show level)) c <> colored (" of " <> pack (show lastLevel)) white
+  in case t of
+    Normal -> [txt white]
+    ColorAnimated -> [txt red, txt white]
+
+mkAmmoCS :: InfoType -> Int -> [ColorString]
+mkAmmoCS _ ammo =
+  let s = colored (singleton '[') bracketsColor
+       <> colored (pack $ replicate ammo '.') ammoColor
+       <> colored (singleton ']') bracketsColor
+   in [s]
+
+mkObjectiveCS :: InfoType -> Int -> [ColorString]
+mkObjectiveCS t target =
+  let txt c = colored "Objective : " white <> colored (pack (show target)) c
+  in case t of
+    Normal -> [txt white]
+    ColorAnimated -> [txt red, txt white]
+
+
+mkShotNumbersCS :: InfoType -> [Int] -> [ColorString]
+mkShotNumbersCS _ nums =
+  let lastIndex = length nums - 1
+      first = colored (singleton '[') bracketsColor
+      last_ = colored (singleton ']') bracketsColor
+      middle = snd $ foldl' (\(i,s) n -> let num = intToDigit n
+                                             t = case i of
+                                                  0 -> singleton num
+                                                  _ -> pack [num, ' ']
+                                         in (i-1, s <> colored' t (numberColor n))) (lastIndex, first) nums
+
+  in [middle <> last_]
+
+mkLeftInfo :: InfoType -> Int -> [Int] -> [[ColorString]]
+mkLeftInfo t ammo shotNums =
+  [mkAmmoCS t ammo, mkShotNumbersCS t shotNums]
+
+mkUpDownInfo :: InfoType -> Level -> ([ColorString], [ColorString])
+mkUpDownInfo t (Level level target _) =
+  (mkObjectiveCS t target, mkLevelCS t level)
+
+mkInfos :: InfoType -> Int -> [Int] -> Level -> (([ColorString], [ColorString]), [[ColorString]])
+mkInfos t ammo shotNums level =
+  (mkUpDownInfo t level, mkLeftInfo t ammo shotNums)
diff --git a/src/Imj/Game/Hamazed/KeysMaps.hs b/src/Imj/Game/Hamazed/KeysMaps.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/KeysMaps.hs
@@ -0,0 +1,31 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE LambdaCase #-}
+
+module Imj.Game.Hamazed.KeysMaps
+    ( eventFromKey
+    ) where
+
+import           Imj.Prelude
+
+import           Imj.Game.Hamazed.Loop.Event.Types
+import           Imj.Geo.Discrete.Types
+import           Imj.Input.Types
+
+
+-- | Maps a 'Key' (pressed by the player) to an 'Event'.
+eventFromKey :: Key -> Maybe Event
+eventFromKey = \case
+  Escape -> Just $ Interrupt Quit
+  AlphaNum c -> case c of
+    'k' -> Just $ Action Laser Down
+    'i' -> Just $ Action Laser Up
+    'j' -> Just $ Action Laser LEFT
+    'l' -> Just $ Action Laser RIGHT
+    'd' -> Just $ Action Ship Down
+    'e' -> Just $ Action Ship Up
+    's' -> Just $ Action Ship LEFT
+    'f' -> Just $ Action Ship RIGHT
+    _   -> Nothing
+  _ -> Nothing
diff --git a/src/Imj/Game/Hamazed/Level.hs b/src/Imj/Game/Hamazed/Level.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/Level.hs
@@ -0,0 +1,128 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE LambdaCase #-}
+
+module Imj.Game.Hamazed.Level
+    ( renderLevelMessage
+    , renderLevelState
+    , messageDeadline
+    , getEventForMaybeDeadline
+    ) where
+
+import           Imj.Prelude
+
+import           Control.Monad.IO.Class(MonadIO)
+import           Control.Monad.Reader.Class(MonadReader)
+
+import           System.Timeout( timeout )
+
+import           Imj.Game.Hamazed.Color
+import           Imj.Game.Hamazed.Level.Types
+import           Imj.Game.Hamazed.Loop.Event.Priorities
+import           Imj.Game.Hamazed.Loop.Event.Types
+import           Imj.Game.Hamazed.KeysMaps
+import           Imj.Geo.Discrete
+import           Imj.Graphics.Render
+import           Imj.Input.NonBlocking
+import           Imj.Input.Blocking
+import           Imj.Input.Types
+import           Imj.Timing
+
+eventFromKey' :: Level -> Key -> Maybe Event
+eventFromKey' (Level n _ finished) key =
+  case finished of
+    Nothing -> eventFromKey key
+    Just (LevelFinished stop _ ContinueMessage) -> Just $
+      case stop of
+        Won -> if n < lastLevel
+                 then
+                   StartLevel (succ n)
+                 else
+                   EndGame
+        (Lost _) -> StartLevel firstLevel
+    _ -> Nothing -- between level end and proposal to continue
+
+messageDeadline :: Level -> SystemTime -> Maybe Deadline
+messageDeadline (Level _ _ mayLevelFinished) t =
+  maybe Nothing
+  (\(LevelFinished _ timeFinished messageType) ->
+    case messageType of
+      InfoMessage ->
+        let finishedSinceSeconds = diffSystemTime t timeFinished
+            delay = 2
+            nextMessageStep = addToSystemTime (delay - finishedSinceSeconds) t
+        in  Just $ Deadline (KeyTime nextMessageStep) DisplayContinueMessage
+      ContinueMessage -> Nothing)
+  mayLevelFinished
+
+-- | Returns a /player event/ or the 'Event' associated to the 'Deadline' if the
+-- 'Deadline' expired before the /player/ could press a 'Key'.
+getEventForMaybeDeadline :: Level
+                         -- ^ Current level
+                         -> Maybe Deadline
+                         -- ^ May contain a 'Deadline'
+                         -> SystemTime
+                         -- ^ Current time
+                         -> IO (Maybe Event)
+getEventForMaybeDeadline level mayDeadline curTime =
+  case mayDeadline of
+    (Just (Deadline k@(KeyTime deadline) deadlineType)) -> do
+      let
+        timeToDeadlineMicros = diffTimeSecToMicros $ diffSystemTime deadline curTime
+      eventWithinDurationMicros level timeToDeadlineMicros k deadlineType
+    Nothing -> eventFromKey' level <$> getKeyThenFlush
+
+eventWithinDurationMicros :: Level -> Int -> KeyTime -> DeadlineType -> IO (Maybe Event)
+eventWithinDurationMicros level durationMicros k step =
+  (\case
+    Just key -> eventFromKey' level key
+    _ -> Just $ Timeout (Deadline k step)
+    ) <$> getCharWithinDurationMicros durationMicros step
+
+getCharWithinDurationMicros :: Int -> DeadlineType -> IO (Maybe Key)
+getCharWithinDurationMicros durationMicros step =
+  if durationMicros < 0
+    -- overdue
+    then
+      if playerEventPriority > deadlinePriority step
+        then
+          tryGetKeyThenFlush
+        else
+          return Nothing
+    else
+      timeout durationMicros getKeyThenFlush
+
+{-# INLINABLE renderLevelState #-}
+renderLevelState :: (Draw e, MonadReader e m, MonadIO m)
+                 => Coords Pos
+                 -> Int
+                 -> LevelFinished
+                 -> m ()
+renderLevelState s level (LevelFinished stop _ messageState) = do
+  let topLeft = translateInDir RIGHT s
+      stopMsg = case stop of
+        (Lost reason) -> "You Lose (" <> reason <> ")"
+        Won           -> "You Win!"
+  drawTxt stopMsg topLeft (messageColor stop)
+  when (messageState == ContinueMessage) $
+    drawTxt
+      (if level == lastLevel
+        then
+          "You reached the end of the game!"
+        else
+          let action = case stop of
+                            (Lost _) -> "restart"
+                            Won      -> "continue"
+          in "Hit a key to " <> action <> " ...")
+      (move 2 Down topLeft) neutralMessageColor
+
+
+{-# INLINABLE renderLevelMessage #-}
+renderLevelMessage :: (Draw e, MonadReader e m, MonadIO m)
+                   => Level
+                   -> Coords Pos
+                   -> m ()
+renderLevelMessage (Level level _ levelState) rightMiddle =
+  mapM_ (renderLevelState rightMiddle level) levelState
diff --git a/src/Imj/Game/Hamazed/Level/Types.hs b/src/Imj/Game/Hamazed/Level/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/Level/Types.hs
@@ -0,0 +1,48 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+{-# LANGUAGE NoImplicitPrelude #-}
+
+module Imj.Game.Hamazed.Level.Types
+    ( Level(..)
+    , LevelFinished(..)
+    , MessageState(..)
+    , GameStops(..)
+    , firstLevel
+    , lastLevel
+    ) where
+
+
+import           Imj.Prelude
+
+import           Imj.Timing
+
+data Level = Level {
+    _levelNumber :: !Int
+    -- ^ From 1 to 12
+  , _levelTarget :: !Int
+    -- ^ The /target number/
+  , _levelStatus :: !(Maybe LevelFinished)
+}
+
+data LevelFinished = LevelFinished {
+    _levelFinishedResult :: !GameStops
+    -- ^ Lost or won
+  , _levelFinishedWhen :: !SystemTime
+  , _levelFinishedCurrentMessage :: !MessageState
+}
+
+data MessageState = InfoMessage
+                  | ContinueMessage
+                  deriving(Eq, Show)
+
+data GameStops = Lost Text
+               -- ^ 'Text' is the reason why the 'Level' was lost.
+               | Won
+
+-- | 12
+lastLevel :: Int
+lastLevel = 12
+
+-- | 1
+firstLevel :: Int
+firstLevel = 1
diff --git a/src/Imj/Game/Hamazed/Loop/Create.hs b/src/Imj/Game/Hamazed/Loop/Create.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/Loop/Create.hs
@@ -0,0 +1,60 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+{-# LANGUAGE NoImplicitPrelude #-}
+
+module Imj.Game.Hamazed.Loop.Create
+        ( mkInitialState
+        ) where
+
+import           Imj.Prelude
+
+import           Control.Monad.IO.Class(MonadIO)
+
+import           Imj.Game.Hamazed.Color
+import           Imj.Game.Hamazed.Infos
+import           Imj.Game.Hamazed.Parameters
+import           Imj.Game.Hamazed.Types
+import           Imj.Game.Hamazed.World.Create
+import           Imj.Game.Hamazed.World.Size
+import           Imj.Game.Hamazed.World.InTerminal
+import           Imj.Graphics.UI.Animation
+import           Imj.Graphics.UI.Colored
+import           Imj.Timing
+
+mkInitialState :: (MonadIO m)
+               => GameParameters
+               -> Int
+               -> Maybe GameState
+               -> m (Either String GameState)
+mkInitialState (GameParameters shape wallType) levelNumber mayState = do
+  let numbers = [1..(3+levelNumber)] -- more and more numbers as level increases
+      target = sum numbers `quot` 2
+      newLevel = Level levelNumber target Nothing
+      newSize = worldSizeFromLevel levelNumber shape
+      newAmmo = 10
+      newShotNums = []
+      make ew = do
+        newWorld <- mkWorld ew newSize wallType numbers newAmmo
+        t <- liftIO getSystemTime
+        let (curWorld, level, ammo, shotNums) =
+              maybe
+              (newWorld, newLevel, 0, [])
+              (\(GameState _ w@(World _ (BattleShip _ curAmmo _ _) _ _ _)
+                           _ curShotNums curLevel _) ->
+                  (w, curLevel, curAmmo, curShotNums))
+                mayState
+            curInfos = mkInfos Normal ammo shotNums level
+            newInfos = mkInfos ColorAnimated newAmmo newShotNums newLevel
+            uiAnimation =
+              mkUIAnimation
+                (Colored worldFrameColors $ mkWorldContainer curWorld, curInfos)
+                (Colored worldFrameColors $ mkWorldContainer newWorld, newInfos)
+                t
+            gameDeadline =
+              if isFinished uiAnimation
+                then
+                  Just $ KeyTime t
+                else
+                  Nothing
+        return $ Right $ GameState gameDeadline curWorld newWorld newShotNums newLevel uiAnimation
+  mkInTerminal newSize >>= either (return . Left) make
diff --git a/src/Imj/Game/Hamazed/Loop/Deadlines.hs b/src/Imj/Game/Hamazed/Loop/Deadlines.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/Loop/Deadlines.hs
@@ -0,0 +1,113 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE LambdaCase #-}
+
+module Imj.Game.Hamazed.Loop.Deadlines
+    ( getNextDeadline
+    ) where
+
+import           Imj.Prelude
+
+import           Data.List( minimumBy, find )
+import           Data.Maybe( catMaybes )
+
+import           Imj.Game.Hamazed.Level
+import           Imj.Game.Hamazed.Loop.Event.Types
+import           Imj.Game.Hamazed.Types
+import           Imj.Graphics.UI.Animation
+import           Imj.Graphics.Animation.Design.Update
+import           Imj.Timing
+
+
+{- | Returns the next 'Deadline' to handle.
+
+We prefer having time-accurate game motions for central items of the game
+(the 'BattleShip', the 'Number's) than having time-accurate explosive 'Animation's.
+
+Hence, when multiple overdue deadlines are competing, the following priorities apply
+(higher number = higher priority):
+
+\[
+\newcommand\T{\Rule{0pt}{.5em}{.3em}}
+  \begin{array}{|c|c|c|}
+	\hline
+  \textbf{ Priority } \T & \textbf{ Name     } \T & \textbf{ Description                            } \\\hline
+	\text{ 5 } & \text{ AnimateUI              } \T & \text{ Inter-level animations                   } \\\hline
+	\text{ 4 } & \text{ DisplayContinueMessage } \T & \textit{ Press a key to continue                } \\\hline
+  \text{ 3 } & \text{ MoveFlyingItems        } \T & \text{ Move the BattleShip and Numbers          } \\\hline
+  \text{ 2 } & \textit{ Player event         } \T & \text{ Handle a key-press                       } \\\hline
+  \text{ 1 } & \text{ Animate                } \T & \text{ Update animations (explosions and others)} \\\hline
+	\end{array}
+\]
+
+When no 'Deadline' is overdue, we return the closest one in time, irrespective
+of its priority.
+-}
+{-We /could/ apply priorities for non-overdue deadlines, too. For example if a
+'MoveFlyingItems' very closely follows an 'Animate' (say, 15 millisecond after),
+we could swap their order so as to have a better guarantee that the game motion
+will happen in-time and not be delayed by a potentially heavy animation update.
+But it's very unlikely that it will make a difference, except if updating
+the 'Animation's becomes /very/ slow for some reason.
+-}
+getNextDeadline :: GameState
+                -- ^ Current state
+                -> SystemTime
+                -- ^ The current time.
+                -> Maybe Deadline
+getNextDeadline s t =
+  let l = getDeadlinesByDecreasingPriority s t
+  in  overdueDeadline t l <|> earliestDeadline' l
+
+earliestDeadline' :: [Deadline] -> Maybe Deadline
+earliestDeadline' [] = Nothing
+earliestDeadline' l  = Just $ minimumBy (\(Deadline t1 _) (Deadline t2 _) -> compare t1 t2 ) l
+
+overdueDeadline :: SystemTime -> [Deadline] -> Maybe Deadline
+overdueDeadline t = find (\(Deadline (KeyTime t') _) -> t' < t)
+
+-- | priorities are : uiAnimation > message > game > player key > animation
+getDeadlinesByDecreasingPriority :: GameState -> SystemTime -> [Deadline]
+getDeadlinesByDecreasingPriority s@(GameState _ _ _ _ level _) t =
+  catMaybes [ uiAnimationDeadline s
+            , messageDeadline level t
+            , getMoveFlyingItemsDeadline s
+            , animationDeadline s
+            ]
+
+getMoveFlyingItemsDeadline :: GameState -> Maybe Deadline
+getMoveFlyingItemsDeadline (GameState nextGameStep _ _ _ (Level _ _ levelFinished) _) =
+  maybe
+    (maybe
+      Nothing
+      (\s -> Just $ Deadline s MoveFlyingItems)
+        nextGameStep)
+    (const Nothing)
+      levelFinished
+
+animationDeadline :: GameState -> Maybe Deadline
+animationDeadline (GameState _ world _ _ _ _) =
+  maybe Nothing (\ti -> Just $ Deadline ti Animate) $ earliestAnimationDeadline world
+
+uiAnimationDeadline :: GameState -> Maybe Deadline
+uiAnimationDeadline (GameState _ _ _ _ _ uianim) =
+  maybe
+    Nothing
+    (\deadline -> Just $ Deadline deadline AnimateUI)
+      $ getUIAnimationDeadline uianim
+
+-- | Returns the earliest 'Animation' deadline.
+earliestAnimationDeadline :: World -> Maybe KeyTime
+earliestAnimationDeadline (World _ _ _ animations _) =
+  earliestKeyTime $ map getDeadline animations
+
+
+-- | Returns the earliest deadline
+earliestKeyTime :: [KeyTime] -> Maybe KeyTime
+earliestKeyTime deadlines =
+  if null deadlines
+    then
+      Nothing
+    else
+      Just $ minimum deadlines
diff --git a/src/Imj/Game/Hamazed/Loop/Event.hs b/src/Imj/Game/Hamazed/Loop/Event.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/Loop/Event.hs
@@ -0,0 +1,38 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE LambdaCase #-}
+
+module Imj.Game.Hamazed.Loop.Event
+    ( needsRendering
+    , getEvent
+    -- * Reexports
+    , module Imj.Game.Hamazed.Loop.Event.Types
+    ) where
+
+import           Imj.Prelude
+
+import           Imj.Game.Hamazed.Level
+import           Imj.Game.Hamazed.Loop.Deadlines
+import           Imj.Game.Hamazed.Loop.Event.Types
+import           Imj.Game.Hamazed.Types
+import           Imj.Timing
+
+-- | Tells if after handling an 'Event' we should render or not.
+needsRendering :: Event -> Bool
+needsRendering = \case
+  (Action Ship _) -> False -- When the ship accelerates, nothing changes visually
+  _ -> True
+
+getEvent :: GameState -> IO Event
+getEvent state = do
+  mayEvent <- getEvent' state
+  case mayEvent of
+    Just event -> return event
+    Nothing -> getEvent state
+
+getEvent' :: GameState -> IO (Maybe Event)
+getEvent' state@(GameState _ _ _ _ level _) = do
+  t <- getSystemTime
+  let deadline = getNextDeadline state t
+  getEventForMaybeDeadline level deadline t
diff --git a/src/Imj/Game/Hamazed/Loop/Event/Priorities.hs b/src/Imj/Game/Hamazed/Loop/Event/Priorities.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/Loop/Event/Priorities.hs
@@ -0,0 +1,22 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+{-# LANGUAGE NoImplicitPrelude #-}
+
+module Imj.Game.Hamazed.Loop.Event.Priorities
+        ( deadlinePriority
+        , playerEventPriority
+        ) where
+
+import           Imj.Prelude
+
+import           Imj.Game.Hamazed.Loop.Event.Types
+
+playerEventPriority :: Int
+playerEventPriority = 40
+
+-- Note that if changing priorities here you should also change 'getDeadlinesByDecreasingPriority'
+deadlinePriority :: DeadlineType -> Int
+deadlinePriority AnimateUI              = playerEventPriority + 30
+deadlinePriority DisplayContinueMessage = playerEventPriority + 20
+deadlinePriority MoveFlyingItems        = playerEventPriority + 10
+deadlinePriority Animate                = playerEventPriority - 10
diff --git a/src/Imj/Game/Hamazed/Loop/Event/Types.hs b/src/Imj/Game/Hamazed/Loop/Event/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/Loop/Event/Types.hs
@@ -0,0 +1,65 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+{-# LANGUAGE NoImplicitPrelude #-}
+
+module Imj.Game.Hamazed.Loop.Event.Types
+        ( Event(..)
+        , Deadline(..)
+        , ActionTarget(..)
+        , DeadlineType(..)
+        , MetaAction(..)
+        -- * Reexports (for haddock hyperlinks)
+        , module Imj.Game.Hamazed.World.Types
+        , module Imj.Graphics.Animation.Design.Create
+        ) where
+
+import           Imj.Prelude
+
+import           Imj.Game.Hamazed.Types
+import           Imj.Game.Hamazed.World.Types
+import           Imj.Geo.Discrete
+import           Imj.Graphics.Animation.Design.Create
+import           Imj.Timing
+
+-- | A foreseen game or animation update.
+data Deadline = Deadline {
+    _deadlineTime :: !KeyTime
+  , _deadlineType :: !DeadlineType
+} deriving(Eq, Show)
+
+data Event = Action !ActionTarget !Direction
+           -- ^ A player action on an 'ActionTarget' in a 'Direction'.
+           | Timeout !Deadline
+           -- ^ The 'Deadline' that needs to be handled immediately.
+           | StartLevel !Int
+           -- ^ New level.
+           | EndGame
+           -- ^ End of game.
+           | Interrupt !MetaAction
+           -- ^ A game interruption.
+           deriving(Eq, Show)
+
+data MetaAction = Quit
+                -- ^ The player decided to quit the game.
+                | Configure
+                -- ^ The player wants to configure the game /(Not implemented yet)/
+                | Help
+                -- ^ The player wants to read the help page /(Not implemented yet)/
+                deriving(Eq, Show)
+
+data DeadlineType = MoveFlyingItems
+                  -- ^ Move 'Number's and 'BattleShip' according to their current
+                  -- speeds.
+                  | Animate
+                  -- ^ Update one or more 'Animation's.
+                  | DisplayContinueMessage
+                  -- ^ Show the /Hit a key to continue/ message
+                  | AnimateUI
+                  -- ^ Update the inter-level animation
+                  deriving(Eq, Show)
+
+data ActionTarget = Ship
+                  -- ^ The player wants to accelerate the 'BattleShip'
+                  | Laser
+                  -- ^ The player wants to shoot with the laser.
+                  deriving(Eq, Show)
diff --git a/src/Imj/Game/Hamazed/Loop/Render.hs b/src/Imj/Game/Hamazed/Loop/Render.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/Loop/Render.hs
@@ -0,0 +1,47 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE LambdaCase #-}
+
+module Imj.Game.Hamazed.Loop.Render
+      ( render
+      ) where
+
+import           Imj.Prelude
+
+import           Imj.Game.Hamazed.Level
+import           Imj.Game.Hamazed.Loop.Event
+import           Imj.Game.Hamazed.Types
+import           Imj.Game.Hamazed.World
+import           Imj.Game.Hamazed.World.Space.Types
+import           Imj.Graphics.Animation.Design.Types
+import           Imj.Graphics.Animation.Design.Render
+import           Imj.Graphics.UI.RectContainer
+
+
+-- | Renders the game to the screen, using "Imj.Graphics.Render.Delta" to avoid
+-- <https://en.wikipedia.org/wiki/Screen_tearing screen tearing>.
+{-# INLINABLE render #-}
+render :: (Render e, MonadReader e m, MonadIO m)
+       => GameState -> m ()
+render (GameState _ world@(World _ _ space animations (InTerminal _ curUpperLeft))
+                  _ _ level wa) =
+  renderSpace space curUpperLeft >>=
+    (\worldCorner -> do
+        renderAnimations worldCorner animations
+        -- TODO merge 2 functions below (and no need to pass worldCorner)
+        renderWorld world
+        let (_,_,_,rightMiddle) = getSideCentersAtDistance (mkWorldContainer world) 3 2
+        renderLevelMessage level rightMiddle
+        renderUIAnimation wa -- render it last so that when it animates
+                             -- to reduce, it goes over numbers and ship
+        ) >> renderToScreen
+
+{-# INLINABLE renderAnimations #-}
+renderAnimations :: (Draw e, MonadReader e m, MonadIO m)
+                 => Coords Pos
+                 -> [Animation]
+                 -> m ()
+renderAnimations worldCorner animations = do
+  let renderAnimation a = renderAnim a worldCorner
+  mapM_ renderAnimation animations
diff --git a/src/Imj/Game/Hamazed/Loop/Run.hs b/src/Imj/Game/Hamazed/Loop/Run.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/Loop/Run.hs
@@ -0,0 +1,79 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE LambdaCase #-}
+
+module Imj.Game.Hamazed.Loop.Run
+      ( run
+      ) where
+
+import           Imj.Prelude
+import qualified Prelude (putStrLn)
+
+import           Control.Monad.IO.Class(MonadIO)
+import           Control.Monad.Reader.Class(MonadReader)
+import           Control.Monad.Reader(runReaderT)
+import           System.Info(os)
+
+import           Imj.Game.Hamazed.Env
+import           Imj.Game.Hamazed.Loop.Create
+import           Imj.Game.Hamazed.Loop.Event
+import           Imj.Game.Hamazed.Loop.Render
+import           Imj.Game.Hamazed.Loop.Update
+import           Imj.Game.Hamazed.Parameters
+import           Imj.Game.Hamazed.Types
+import           Imj.Graphics.Render
+import           Imj.Graphics.Render.Delta
+import           Imj.Threading
+
+{- | Runs the Hamazed game.
+
+If your current terminal window is too small, the program will error and
+tell you what is the minimum window size to run the game.
+
+The game doesn't run on Windows, because with GHC,
+<https://ghc.haskell.org/trac/ghc/ticket/7353 IO operations cannot be interrupted on Windows>.
+-}
+run :: IO ()
+run =
+  if os == "mingw32"
+    then
+      Prelude.putStrLn $ "Windows is not currently supported,"
+      ++ " due to this GHC bug: https://ghc.haskell.org/trac/ghc/ticket/7353."
+    else
+      void doRun
+
+doRun :: IO Termination
+doRun =
+  runThenRestoreConsoleSettings
+    (createEnv >>= runAndWaitForTermination . runReaderT gameWorker)
+
+{-# INLINABLE gameWorker #-}
+gameWorker :: (Render e, MonadReader e m, MonadIO m)
+           => m ()
+gameWorker =
+  getGameParameters >>= runGameWorker
+
+
+{-# INLINABLE runGameWorker #-}
+runGameWorker :: (Render e, MonadReader e m, MonadIO m)
+              => GameParameters
+              -> m ()
+runGameWorker params =
+  mkInitialState params firstLevel Nothing
+    >>= \case
+      Left err -> error err
+      Right ew -> loop params ew
+
+{-# INLINABLE loop #-}
+loop :: (Render e, MonadReader e m, MonadIO m)
+     => GameParameters
+     -> GameState
+     -> m ()
+loop params state = do
+  liftIO (getEvent state) >>= \evt -> case evt of
+    (Interrupt _) -> return ()
+    _ -> do
+      newState <- liftIO $ update params state evt
+      when (needsRendering evt) $ render newState
+      loop params newState
diff --git a/src/Imj/Game/Hamazed/Loop/Timing.hs b/src/Imj/Game/Hamazed/Loop/Timing.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/Loop/Timing.hs
@@ -0,0 +1,26 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+{-# LANGUAGE NoImplicitPrelude #-}
+
+-- | This modules handle time constants of the game logic.
+
+module Imj.Game.Hamazed.Loop.Timing
+        ( gameMotionPeriod
+        , module Imj.Timing
+        ) where
+
+import           Imj.Prelude
+
+import           Imj.Timing
+
+gameMotionPeriod :: DiffTime
+gameMotionPeriod =
+  fromIntegral gameMotionPeriodMicros / 1000000
+
+-- using the "incremental" render backend, there is no flicker
+-- using the "full" render backend, flicker starts at 40
+gameMotionPeriodMicros :: Int
+gameMotionPeriodMicros =
+  millis * 1000
+ where
+  millis = 160 -- 20 seems to match screen refresh frequency
diff --git a/src/Imj/Game/Hamazed/Loop/Update.hs b/src/Imj/Game/Hamazed/Loop/Update.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/Loop/Update.hs
@@ -0,0 +1,200 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE LambdaCase #-}
+
+module Imj.Game.Hamazed.Loop.Update
+      ( update
+      ) where
+
+import           Imj.Prelude
+
+import           Data.Maybe( catMaybes, isNothing )
+
+import           Imj.Game.Hamazed.Infos
+import           Imj.Game.Hamazed.Level.Types
+import           Imj.Game.Hamazed.Loop.Create
+import           Imj.Game.Hamazed.Loop.Event
+import           Imj.Game.Hamazed.Loop.Timing
+import           Imj.Game.Hamazed.Parameters
+import           Imj.Game.Hamazed.Types
+import           Imj.Game.Hamazed.World
+import           Imj.Game.Hamazed.World.Number
+import           Imj.Game.Hamazed.World.Ship
+import           Imj.Game.Hamazed.World.Space.Types
+import           Imj.GameItem.Weapon.Laser
+import           Imj.Geo.Continuous
+import           Imj.Geo.Discrete
+import           Imj.Graphics.Animation.Design.Types
+import           Imj.Graphics.Animation.Design.Update
+import           Imj.Graphics.Animation
+import           Imj.Graphics.UI.RectContainer
+import           Imj.Util
+
+
+-- | Updates the state. It needs IO just to generate random numbers in case
+-- 'Event' is 'StartLevel'
+{-# INLINABLE update #-}
+update :: GameParameters
+       -- ^ 'World' creation parameters, used in case the 'Event' is 'StartLevel'.
+       -> GameState
+       -- ^ The current state
+       -> Event
+       -- ^ The 'Event' that should be handled here.
+       -> IO GameState
+update
+ params
+ state@(GameState b world@(World c d space animations e)
+                  futWorld f h@(Level level target mayLevelFinished) anim) = \case
+  StartLevel nextLevel ->
+    mkInitialState params nextLevel (Just state) >>= \case
+      Left err -> error err
+      Right s -> return s
+  (Timeout (Deadline gt AnimateUI)) ->
+    return $ updateAnim gt state
+  (Timeout (Deadline _ DisplayContinueMessage)) ->
+    return $ case mayLevelFinished of
+      Just (LevelFinished stop finishTime _) ->
+        let newLevel = Level level target (Just $ LevelFinished stop finishTime ContinueMessage)
+        in GameState b world futWorld f newLevel anim
+      Nothing -> state
+  (Timeout (Deadline k Animate)) -> do
+    let newAnimations = mapMaybe (\a -> if shouldUpdate a k
+                                                then updateAnimation a
+                                                else Just a) animations
+    return $ GameState b (World c d space newAnimations e) futWorld f h anim
+  (Timeout (Deadline gt MoveFlyingItems)) -> do
+    let movedState = GameState (Just $ addDuration gameMotionPeriod gt) (moveWorld gt world) futWorld f h anim
+    return $ onHasMoved movedState gt
+  Action Laser dir ->
+    if isFinished anim
+      then
+        getSystemTime >>= return . (onLaser state dir)
+      else
+        return $ state
+  Action Ship dir ->
+    return $ accelerateShip' dir state
+  (Interrupt _) ->
+    return state
+  EndGame -> -- TODO instead, go back to game configuration ?
+    return state
+
+
+onLaser :: GameState
+        -> Direction
+        -> SystemTime
+        -> GameState
+onLaser
+  (GameState b world@(World _ (BattleShip posspeed ammo safeTime collisions)
+                     space animations e)
+             futureWorld g (Level i target finished)
+             (UIAnimation (UIEvolutions j upDown left) k l))
+  dir t =
+  let (remainingBalls, destroyedBalls, maybeLaserRay, newAmmo) =
+        laserEventAction dir world
+      outerSpaceAnims_ =
+         if null destroyedBalls
+           then
+             maybe [] (outerSpaceAnims t world) maybeLaserRay
+           else
+            []
+      newAnimations =
+        destroyedNumbersAnimations (Left t) dir world destroyedBalls
+        ++ maybe [] (\r -> laserAnims world r t) maybeLaserRay
+        ++ outerSpaceAnims_
+
+      newWorld = World remainingBalls (BattleShip posspeed newAmmo safeTime collisions)
+                       space (newAnimations ++ animations) e
+      destroyedNumbers = map (\(Number _ n) -> n) destroyedBalls
+      allShotNumbers = g ++ destroyedNumbers
+      newLeft =
+        if null destroyedNumbers && ammo == newAmmo
+          then
+            left
+          else
+            let frameSpace = mkWorldContainer world
+                infos = mkLeftInfo Normal newAmmo allShotNumbers
+                (_, _, leftMiddle, _) = getSideCentersAtDistance frameSpace 3 2
+            in mkTextAnimRightAligned leftMiddle leftMiddle infos 0 -- 0 duration, since animation is over anyway
+      newFinished = finished <|> checkTargetAndAmmo newAmmo (sum allShotNumbers) target t
+      newLevel = Level i target newFinished
+      newAnim = UIAnimation (UIEvolutions j upDown newLeft) k l
+  in assert (isFinished newAnim) $ GameState b newWorld futureWorld allShotNumbers newLevel newAnim
+
+-- | The world has moved, so we update it.
+onHasMoved :: GameState
+           -> KeyTime
+           -> GameState
+onHasMoved
+  (GameState b world@(World balls ship@(BattleShip _ _ safeTime collisions) space animations e)
+             futureWorld shotNums (Level i target finished) anim)
+  keyTime@(KeyTime t) =
+  let newAnimations = shipAnims world keyTime
+      remainingBalls =
+        if isNothing safeTime
+          then
+            filter (`notElem` collisions) balls
+          else
+            balls
+      newWorld = World remainingBalls ship space (newAnimations ++ animations) e
+      finishIfShipCollides =
+        maybe
+          (case map (\(Number _ n) -> n) collisions of
+            [] -> Nothing
+            l  -> Just $ LevelFinished (Lost $ "collision with " <> showListOrSingleton l) t InfoMessage )
+          (const Nothing)
+            safeTime
+      newLevel = Level i target (finished <|> finishIfShipCollides)
+  in assert (isFinished anim) $ GameState b newWorld futureWorld shotNums newLevel anim
+
+
+outerSpaceAnims :: SystemTime
+                -> World
+                -> LaserRay Actual
+                -> [Animation]
+outerSpaceAnims t world@(World _ _ (Space _ sz _) _ _) ray@(LaserRay dir _) =
+  let laserTarget = afterEnd ray
+  in case onOuterBorder laserTarget sz of
+       Just outDir -> outerSpaceAnims' t world laserTarget $ assert (dir == outDir) dir
+       Nothing -> []
+
+outerSpaceAnims' :: SystemTime
+                 -> World
+                 -> Coords Pos
+                 -> Direction
+                 -> [Animation]
+outerSpaceAnims' t@(MkSystemTime _ nanos) world fronteerPoint dir =
+  let char = niceChar $ fromIntegral nanos -- cycle character every nano second
+      speed = scalarProd 0.8 $ speed2vec $ coordsForDirection dir
+      outerSpacePoint = translateInDir dir fronteerPoint
+      interaction = environmentInteraction world TerminalWindow
+  in fragmentsFreeFall speed outerSpacePoint interaction (Speed 1) (Left t) char
+
+
+laserAnims :: World
+           -> LaserRay Actual
+           -> SystemTime
+           -> [Animation]
+laserAnims world ray t =
+  let interaction = environmentInteraction world WorldFrame
+  in catMaybes [laserAnimation ray interaction (Left t)]
+
+
+accelerateShip' :: Direction -> GameState -> GameState
+accelerateShip' dir (GameState c (World wa ship wc wd we) b f g h) =
+  let newShip = accelerateShip dir ship
+      world = World wa newShip wc wd we
+  in GameState c world b f g h
+
+updateAnim :: KeyTime -> GameState -> GameState
+updateAnim kt (GameState _ curWorld futWorld j k (UIAnimation evolutions _ it)) =
+  let nextIt@(Iteration _ nextFrame) = nextIteration it
+      (world, gameDeadline, worldAnimDeadline) =
+        maybe
+          (futWorld , Just kt, Nothing)
+          (\dt ->
+           (curWorld, Nothing, Just $ addDuration (floatSecondsToDiffTime dt) kt))
+          $ getDeltaTime evolutions nextFrame
+      wa = UIAnimation evolutions worldAnimDeadline nextIt
+  in GameState gameDeadline world futWorld j k wa
diff --git a/src/Imj/Game/Hamazed/Parameters.hs b/src/Imj/Game/Hamazed/Parameters.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/Parameters.hs
@@ -0,0 +1,127 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE LambdaCase #-}
+
+module Imj.Game.Hamazed.Parameters(
+        GameParameters(..)
+      , getGameParameters
+      ) where
+
+import           Imj.Prelude
+
+import           Control.Monad.IO.Class(MonadIO)
+import           Control.Monad.Reader.Class(MonadReader)
+
+import           Imj.Game.Hamazed.Color
+import           Imj.Game.Hamazed.World.Create
+import           Imj.Game.Hamazed.World.InTerminal
+import           Imj.Game.Hamazed.World.Render
+import           Imj.Game.Hamazed.World.Size
+import           Imj.Game.Hamazed.World.Space
+import           Imj.Game.Hamazed.World.Types
+import           Imj.Geo.Discrete
+import           Imj.Graphics.Text.Alignment
+import           Imj.Graphics.UI.Animation
+import           Imj.Graphics.UI.Colored
+import           Imj.Input.Blocking
+import           Imj.Input.Types
+import           Imj.Timing
+
+
+data GameParameters = GameParameters {
+    _gameParamsWorldShape :: !WorldShape
+  , _gameParamsWallDistrib :: !WallDistribution
+}
+
+minRandomBlockSize :: Int
+minRandomBlockSize = 6 -- using 4 it once took a very long time (one minute, then I killed the process)
+                       -- 6 has always been ok
+
+initialParameters :: GameParameters
+initialParameters = GameParameters Square None
+
+-- | Displays the configuration UI showing the game creation options,
+-- and returns when the player has finished chosing the options.
+{-# INLINABLE getGameParameters #-}
+getGameParameters :: (Render e, MonadReader e m, MonadIO m)
+                  => m GameParameters
+getGameParameters = update initialParameters
+
+{-# INLINABLE update #-}
+update :: (Render e, MonadReader e m, MonadIO m)
+       => GameParameters
+       -> m GameParameters
+update params = do
+  render' params
+  liftIO getKeyThenFlush >>= \case
+    AlphaNum c ->
+      if c == ' '
+        then
+          return params
+        else
+          update $ updateFromChar c params
+    _ -> return params
+
+updateFromChar :: Char -> GameParameters -> GameParameters
+updateFromChar c p@(GameParameters shape wallType) =
+  case c of
+    '1' -> GameParameters Square wallType
+    '2' -> GameParameters Rectangle2x1 wallType
+    'e' -> GameParameters shape None
+    'r' -> GameParameters shape Deterministic
+    't' -> GameParameters shape (Random $ RandomParameters minRandomBlockSize StrictlyOneComponent)
+    _ -> p
+
+
+{-# INLINABLE dText #-}
+dText :: (Draw e, MonadReader e m, MonadIO m)
+      => Text
+      -> Coords Pos
+      -> m (Coords Pos)
+dText txt pos =
+  drawTxt txt pos configColors >> return (translateInDir Down pos)
+
+{-# INLINABLE dText_ #-}
+dText_ :: (Draw e, MonadReader e m, MonadIO m)
+       => Text
+       -> Coords Pos
+       -> m ()
+dText_ txt pos =
+  void (dText txt pos)
+
+{-# INLINABLE render' #-}
+render' :: (Render e, MonadReader e m, MonadIO m)
+        => GameParameters
+        -> m ()
+render' (GameParameters shape wall) = do
+  let worldSize@(Size (Length rs) (Length cs)) = worldSizeFromLevel 1 shape
+  mkInTerminal worldSize >>= \case
+    Left err -> error err
+    Right rew@(InTerminal _ ul) -> do
+      world@(World _ _ space _ _) <- mkWorld rew worldSize wall [] 0
+      _ <- renderSpace space ul >>=
+        \worldCoords -> do
+          renderWorld world
+          let middle = move (quot cs 2) RIGHT worldCoords
+              middleCenter = move (quot (rs-1) 2 ) Down middle
+              middleLow    = move (rs-1)           Down middle
+              leftMargin = 3
+              left = move (quot (rs-1) 2 - leftMargin) LEFT middleCenter
+          drawAlignedTxt "Game configuration" configColors (mkCentered $ translateInDir Down middle)
+            >>= drawAlignedTxt_ "------------------" configColors
+          drawAlignedTxt_ "Hit 'Space' to start game" configColors (mkCentered $ translateInDir Up middleLow)
+
+          translateInDir Down <$> dText "- World shape" (move 5 Up left)
+            >>= dText "'1' -> width = height"
+              >>= dText_ "'2' -> width = 2 x height"
+          translateInDir Down <$> dText "- World walls" left
+            >>= dText "'e' -> no walls"
+              >>= dText "'r' -> deterministic walls"
+                >>= dText_ "'t' -> random walls"
+
+          t <- liftIO getSystemTime
+          let infos = (Colored worldFrameColors $ mkWorldContainer world, (([""],[""]),[[""],[""]]))
+          renderUIAnimation $ mkUIAnimation infos infos t
+      renderToScreen
diff --git a/src/Imj/Game/Hamazed/Types.hs b/src/Imj/Game/Hamazed/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/Types.hs
@@ -0,0 +1,34 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+{-# LANGUAGE NoImplicitPrelude #-}
+
+module Imj.Game.Hamazed.Types
+    ( GameState(..)
+    -- * Reexports
+    , module Imj.Game.Hamazed.Level.Types
+    , module Imj.Game.Hamazed.World.Types
+    , UIAnimation
+    ) where
+
+import           Imj.Prelude
+
+import           Imj.Game.Hamazed.Level.Types
+import           Imj.Game.Hamazed.Loop.Timing
+import           Imj.Game.Hamazed.World.Types
+import           Imj.Graphics.UI.Animation
+
+
+data GameState = GameState {
+    _gameStateNextMotionStep :: !(Maybe KeyTime)
+    -- ^ When the next 'World' motion update should happen,
+  , _gameStatePreviousWorld :: !World
+    -- ^ The previous 'World'
+  , _gameStateCurrentWorld :: !World
+    -- ^ The current 'World'
+  , _gameStateShotNumbers :: ![Int]
+    -- ^ Which 'Number's were shot
+  , _gameStateLevel :: !Level
+    -- ^ The current 'Level'
+  , _gameStateUIAnimation :: !UIAnimation
+    -- ^ Inter-level animation.
+}
diff --git a/src/Imj/Game/Hamazed/World.hs b/src/Imj/Game/Hamazed/World.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/World.hs
@@ -0,0 +1,233 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+
+module Imj.Game.Hamazed.World
+    (
+      -- * Parameters
+      {-| When the game starts, the player can chose 'World' parameters:
+
+      * 'WorldShape' : square or rectangular 'World' where the width is twice the height
+      * 'WallDistribution' : Should the 'World' have walls, and what kind of walls.
+       -}
+      getGameParameters
+    , GameParameters(..)
+      -- * Level
+      {-| There are 12 levels in Hamazed, numbered from 1 to 12.
+      -}
+    , Level(..)
+      -- ** Level termination
+      {-| [Target number]
+      Each level has a different /target number/ which represents the sum of shot
+      'Number's that should be reached to finish the 'Level'.
+
+       A 'Level' is finished once the sum of shot 'Number's amounts to the /target number/. -}
+    , checkTargetAndAmmo
+    -- * World
+    {- | A 'World' brings together:
+
+    * game elements : 'Space', 'BattleShip' and 'Number's,
+    * rendering elements: 'Animation's,
+    * terminal-awareness : 'InTerminal'
+    -}
+    , World(..)
+    -- ** Create the world
+      {-|
+      The 'World' size decreases with increasing 'Level' numbers.
+
+      'worldSizeFromLevel' gives the 'Size' of the 'World' based on
+      the 'Level' number and the 'WorldShape':
+      -}
+    , worldSizeFromLevel
+      {-|
+      Once we have the 'Size' of the 'World', we can create it using 'mkWorld':
+      -}
+    , mkWorld
+    -- ** Update World
+    -- | Every 'gameMotionPeriod' seconds, the positions of 'BattleShip' and 'Numbers'
+    -- are updated according to their speeds:
+    , gameMotionPeriod
+    , moveWorld
+    -- ** Render World
+    , renderWorld
+    -- ** World utilities
+    -- | 'laserEventAction' returns the effect a laser shot it has on the 'World'.
+    , laserEventAction
+    -- * InTerminal
+    -- | 'InTerminal' allows to place the game in the center of the terminal.
+    , mkInTerminal
+    , InTerminal(..)
+    -- * Space
+    {-| 'Space' describes the environment in which 'Number's and the 'BattleShip'
+    live.
+
+    It can be composed of 'Air', where 'BattleShip' and 'Number's are free to move, and of
+    'Wall'.
+    -}
+    , Space
+    , Material(..)
+      -- ** Simple creation
+    , mkEmptySpace
+    , mkDeterministicallyFilledSpace
+      -- ** Randomized creation
+      {-| 'mkRandomlyFilledSpace' places 'Wall's at random and discards resulting
+      'Space's which have more than one 'Air' connected component.
+
+      This way, the 'BattleShip' is guaranteed to be able to reach any part of
+      the 'Space', and 'Number's.
+
+      Generating a big 'Space' with a very small block size can
+      be computationnaly expensive because the probability to have a single 'Air'
+      connected component drops very fast (probably at least exponentially)
+      towards zero with increasing sizes. -}
+    , mkRandomlyFilledSpace
+    , RandomParameters(..)
+    , Strategy(..)
+      -- ** Collision detection
+      {- | 'location' is the standard collision detection function that considers
+      that being outside the world means being in collision.
+
+      'scopedLocation' prevides more options with the use of 'Boundaries' to
+      defines the collision detection scopes.
+       -}
+    , location
+    , scopedLocation
+    , Boundaries(..)
+      -- ** Collision detection utilities
+    , createRandomNonCollidingPosSpeed
+      -- ** Render
+    , renderSpace
+      -- * Movable items
+      -- | A movable item's 'PosSpeed' is updated using 'updateMovableItem'
+      -- at each 'MoveFlyingItems' event:
+    , updateMovableItem
+      -- ** BattleShip
+    , BattleShip(..)
+    -- *** Accelerate BattleShip
+    -- | The 'BattleShip' is controlled in (discrete) acceleration by the player
+    -- using the keyboard.
+    , accelerateShip
+    -- ** Number
+    -- | 'Number's can be shot by the 'BattleShip' to finish the 'Level'.
+    --
+    -- Number can collide with the 'BattleShip', hence triggering colorfull
+    -- 'Animation' explosions.
+    --
+    -- 'Number's never change speed, except when they rebound on 'Wall's, of course.
+    , Number(..)
+    -- * UI
+    {- | UI elements around the 'World' are:
+
+    * a 'RectContainer' created by 'mkWorldContainer' to visually delimit the 'World'
+    * 'ColorString' information, placed around the 'RectContainer':
+
+        * Up: 'Level' target
+        * Left: remaining ammunitions / shot 'Number's
+        * Down: 'Level' number
+    -}
+    , mkWorldContainer
+    -- ** Inter-level animations
+    , module Imj.Graphics.UI.Animation
+    -- * Secondary types
+    , WallDistribution(..)
+    , WorldShape(..)
+    , LevelFinished(..)
+    , GameStops(..)
+    -- * Reexports
+    , module Imj.Graphics.Render
+    , ColorString
+    ) where
+
+import           Imj.Prelude
+
+import           Control.Monad.IO.Class(MonadIO)
+import           Control.Monad.Reader.Class(MonadReader)
+
+import           Data.Text( pack )
+
+import           Imj.Game.Hamazed.Loop.Event
+import           Imj.Game.Hamazed.Loop.Timing
+import           Imj.Game.Hamazed.Level.Types
+import           Imj.Game.Hamazed.Parameters
+import           Imj.Game.Hamazed.World.Create
+import           Imj.Game.Hamazed.World.InTerminal
+import           Imj.Game.Hamazed.World.Number
+import           Imj.Game.Hamazed.World.Render
+import           Imj.Game.Hamazed.World.Size
+import           Imj.Game.Hamazed.World.Space
+import           Imj.GameItem.Weapon.Laser
+import           Imj.Geo.Discrete
+import           Imj.Graphics.Render
+import           Imj.Graphics.Text.ColorString
+import           Imj.Graphics.UI.Animation
+
+-- | Note that the position of the 'BattleShip' remains unchanged.
+accelerateShip :: Direction -> BattleShip -> BattleShip
+accelerateShip dir (BattleShip (PosSpeed pos speed) ba bb bc) =
+  let newSpeed = translateInDir dir speed
+  in BattleShip (PosSpeed pos newSpeed) ba bb bc
+
+-- | Moves elements of game logic ('Number's, 'BattleShip').
+--
+-- Note that 'Animation's are not updated.
+moveWorld :: KeyTime
+          -- ^ The current time
+          -> World
+          -> World
+moveWorld (KeyTime curTime) (World balls (BattleShip shipPosSpeed ammo safeTime _) size anims e) =
+  let newSafeTime = case safeTime of
+        (Just t) -> if curTime > t
+                      then
+                        Nothing
+                      else
+                        safeTime
+        _        -> Nothing
+      newBalls = map (\(Number ps n) -> Number (updateMovableItem size ps) n) balls
+      newPosSpeed@(PosSpeed pos _) = updateMovableItem size shipPosSpeed
+      collisions = getColliding pos newBalls
+      newShip = BattleShip newPosSpeed ammo newSafeTime collisions
+  in World newBalls newShip size anims e
+
+-- TODO use Number Live Number Dead
+-- | Computes the effect of an laser shot on the 'World'.
+laserEventAction :: Direction
+                 -- ^ The direction of the laser shot
+                 -> World
+                 -> ([Number], [Number], Maybe (LaserRay Actual), Int)
+                 -- ^ 'Number's still alive, 'Number's destroyed, maybe an actual laser ray, Ammo left.
+laserEventAction dir (World balls (BattleShip (PosSpeed shipCoords _) ammo _ _) space _ _) =
+  let (maybeLaserRayTheoretical, newAmmo) =
+        if ammo > 0 then
+          (LaserRay dir <$> shootLaserWithOffset shipCoords dir Infinite (`location` space), pred ammo)
+        else
+          (Nothing, ammo)
+
+      ((remainingBalls, destroyedBalls), maybeLaserRay) =
+         maybe
+           ((balls,[]), Nothing)
+           (\r -> computeActualLaserShot balls (\(Number (PosSpeed pos _) _) -> pos) r DestroyFirstObstacle)
+             maybeLaserRayTheoretical
+
+  in (remainingBalls, destroyedBalls, maybeLaserRay, newAmmo)
+
+
+checkTargetAndAmmo :: Int
+                   -- ^ Remaining ammo
+                   -> Int
+                   -- ^ The current sum of all shot 'Numbers'
+                   -> Int
+                   -- ^ The 'Level' 's target number.
+                   -> SystemTime
+                   -- ^ The current time
+                   -> Maybe LevelFinished
+checkTargetAndAmmo ammo sumNumbers target t =
+    maybe Nothing (\stop -> Just $ LevelFinished stop t InfoMessage) allChecks
+  where
+    allChecks = checkSum <|> checkAmmo
+
+    checkSum = case compare sumNumbers target of
+      LT -> Nothing
+      EQ -> Just Won
+      GT -> Just $ Lost $ pack $ show sumNumbers ++ " is bigger than " ++ show target
+    checkAmmo
+      | ammo <= 0 = Just $ Lost $ pack "no ammo left"
+      | otherwise = Nothing
diff --git a/src/Imj/Game/Hamazed/World/Create.hs b/src/Imj/Game/Hamazed/World/Create.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/World/Create.hs
@@ -0,0 +1,86 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+{-# LANGUAGE NoImplicitPrelude #-}
+
+module Imj.Game.Hamazed.World.Create
+        ( mkWorld
+        , updateMovableItem
+        ) where
+
+import           Imj.Prelude
+
+import           Control.Monad.IO.Class(MonadIO, liftIO)
+
+import           Imj.Game.Hamazed.World.Number
+import           Imj.Game.Hamazed.World.Ship
+import           Imj.Game.Hamazed.World.Space
+import           Imj.Game.Hamazed.World.Types
+import           Imj.Geo.Discrete
+import           Imj.Physics.Discrete.Collision
+import           Imj.Timing
+
+mkWorld :: (MonadIO m)
+        => InTerminal
+        -- ^ Tells where to draw the 'World' from
+        -> Size
+        -- ^ The dimensions
+        -> WallDistribution
+        -- ^ How the 'Wall's should be constructed
+        -> [Int]
+        -- ^ The numbers for which we will create 'Number's.
+        -> Int
+        -- ^ Ammunition : how many laser shots are available.
+        -> m World
+mkWorld e s walltype nums ammo = do
+  space <- case walltype of
+    None          -> return $ mkEmptySpace s
+    Deterministic -> return $ mkDeterministicallyFilledSpace s
+    Random rParams    -> liftIO $ mkRandomlyFilledSpace rParams s
+  t <- liftIO getSystemTime
+  balls <- mapM (createRandomNumber space) nums
+  ship@(PosSpeed pos _) <- liftIO $ createShipPos space balls
+  return $ World balls (BattleShip ship ammo (Just $ addToSystemTime 5 t) (getColliding pos balls)) space [] e
+
+
+-- | Updates 'PosSpeed' of a movable item, according to 'Space'.
+updateMovableItem :: Space
+                  -- ^ The surrounding 'Space' will be taken into account for collisions.
+                  -> PosSpeed
+                  -- ^ The current position and speed of the moving item.
+                  -> PosSpeed
+                  -- ^ The updated position and speed.
+updateMovableItem space ps@(PosSpeed pos _) =
+  let (newPs@(PosSpeed newPos _), collision) =
+        mirrorSpeedAndMoveToPrecollisionIfNeeded (`location` space) ps
+  in  case collision of
+        PreCollision ->
+          if pos /= newPos
+            then
+              newPs
+            else
+              -- Precollision position is the same as the previous position, we try to move
+              doBallMotionUntilCollision space newPs
+        NoCollision  -> doBallMotion newPs
+
+-- if we ever change this, we should chek other places where we use sumPosSpeed
+-- to use this function instead
+doBallMotion :: PosSpeed -> PosSpeed
+doBallMotion (PosSpeed pos speed) =
+  PosSpeed (sumPosSpeed pos speed) speed
+
+-- | Changes the position until a collision is found.
+--   Doesn't change the speed
+doBallMotionUntilCollision :: Space -> PosSpeed -> PosSpeed
+doBallMotionUntilCollision space (PosSpeed pos speed) =
+  let trajectory = bresenham $ mkSegment pos $ sumPosSpeed pos speed
+      newPos = maybe (last trajectory) snd $ firstCollision (`location` space) trajectory
+  in PosSpeed newPos speed
+
+
+createRandomNumber :: (MonadIO m)
+                   => Space
+                   -> Int
+                   -> m Number
+createRandomNumber space i = do
+  ps <- liftIO $ createRandomNonCollidingPosSpeed space
+  return $ Number ps i
diff --git a/src/Imj/Game/Hamazed/World/InTerminal.hs b/src/Imj/Game/Hamazed/World/InTerminal.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/World/InTerminal.hs
@@ -0,0 +1,64 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+{-# LANGUAGE NoImplicitPrelude #-}
+
+module Imj.Game.Hamazed.World.InTerminal
+    ( mkInTerminal
+    ) where
+
+import           Imj.Prelude
+
+import           Control.Monad.IO.Class(MonadIO)
+
+import qualified System.Console.Terminal.Size as Terminal( Window(..), size )
+
+import           Imj.Game.Hamazed.World.Size
+import           Imj.Game.Hamazed.World.Types
+import           Imj.Geo.Discrete.Types
+
+-- | Minimal margin between the upper left corner of the console
+--   and upper left corner of the world
+minimalWorldMargin :: Int
+minimalWorldMargin = 4
+
+-- | Will compute the position of the 'World' so as to display it in the
+-- center of the terminal window.
+{-# INLINABLE mkInTerminal #-}
+mkInTerminal :: (MonadIO m)
+                => Size
+                -- ^ Measures the dimensions of the /inner/ content of the 'World',
+                -- excluding the outer frame.
+                -> m (Either String InTerminal)
+mkInTerminal s = do
+  mayTermSize <- liftIO Terminal.size
+  return $ InTerminal mayTermSize <$> worldUpperLeftToCenterIt' s mayTermSize
+
+
+worldUpperLeftToCenterIt' :: Size -> Maybe (Terminal.Window Int) -> Either String (Coords Pos)
+worldUpperLeftToCenterIt' worldSize mayTermSize =
+  case mayTermSize of
+    Just termSize@(Terminal.Window h w)  ->
+      let (Size rs cs) = maxWorldSize
+          heightMargin = 2 * (1 {-outer walls-} + 1 {-1 line above and below-})
+          widthMargin = 2 * (1 {-outer walls-} + 4 {-brackets, spaces-} + 16 * 2 {-display all numbers-})
+          minSize@(Terminal.Window minh minw) =
+            Terminal.Window (fromIntegral rs + heightMargin)
+                            (fromIntegral cs + widthMargin)
+      in if h < minh || w < minw
+            then
+              Left $  "\nMinimum terminal size : " ++ show minSize
+                  ++ ".\nCurrent terminal size : " ++ show termSize
+                  ++ ".\nThe current terminal size doesn't match the minimum size,"
+                  ++  "\nplease adjust your terminal size and restart the executable"
+                  ++ ".\n"
+            else
+              Right $ worldUpperLeftFromTermSize termSize worldSize
+    Nothing -> Right $ Coords (Coord minimalWorldMargin) (Coord minimalWorldMargin)
+
+-- | upper left for the /outer/ content of the 'World', i.e including the /outer/
+-- frame.
+worldUpperLeftFromTermSize :: Terminal.Window Int -> Size -> (Coords Pos)
+worldUpperLeftFromTermSize (Terminal.Window h w) (Size rs cs) =
+  let walls = 2 :: Int
+  in toCoords (quot (fromIntegral h-(rs+ fromIntegral walls)) 2)
+              (quot (fromIntegral w-(cs+ fromIntegral walls)) 2)
diff --git a/src/Imj/Game/Hamazed/World/Number.hs b/src/Imj/Game/Hamazed/World/Number.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/World/Number.hs
@@ -0,0 +1,43 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+{-# LANGUAGE NoImplicitPrelude #-}
+
+module Imj.Game.Hamazed.World.Number(
+    getColliding
+  , computeActualLaserShot
+  , destroyedNumbersAnimations
+  ) where
+
+import           Imj.Prelude
+
+import           Data.Char( intToDigit )
+
+import           Imj.Game.Hamazed.Loop.Event
+import           Imj.GameItem.Weapon.Laser
+import           Imj.Geo.Continuous
+import           Imj.Geo.Discrete
+import           Imj.Graphics.Animation
+
+getColliding :: Coords Pos -> [Number] -> [Number]
+getColliding pos =
+  filter (\(Number (PosSpeed pos' _) _) -> pos == pos')
+
+destroyedNumbersAnimations :: Either SystemTime KeyTime
+                           -> Direction -- ^ 'Direction' of the laser shot
+                           -> World -- ^ the 'World' the 'Number's live in
+                           -> [Number]
+                           -> [Animation]
+destroyedNumbersAnimations keyTime dir world =
+  let laserSpeed = speed2vec $ coordsForDirection dir
+  in concatMap (destroyedNumberAnimations keyTime laserSpeed world)
+
+destroyedNumberAnimations :: Either SystemTime KeyTime
+                          -> Vec2 Vel
+                          -> World
+                          -> Number
+                          -> [Animation]
+destroyedNumberAnimations k laserSpeed world (Number (PosSpeed pos _) n) =
+  let char = intToDigit n
+      envFunc = environmentInteraction world WorldFrame
+  in catMaybes ([animatedPolygon n pos envFunc (Speed 1) k char])
+     ++ fragmentsFreeFallThenExplode (scalarProd 0.8 laserSpeed) pos envFunc (Speed 2) k char
diff --git a/src/Imj/Game/Hamazed/World/Render.hs b/src/Imj/Game/Hamazed/World/Render.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/World/Render.hs
@@ -0,0 +1,52 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+{-# LANGUAGE NoImplicitPrelude #-}
+
+module Imj.Game.Hamazed.World.Render
+        ( renderUIAnimation
+        , renderWorld
+        ) where
+
+import           Imj.Prelude
+
+import           Data.Char( intToDigit )
+import           Data.Maybe( isNothing, isJust )
+
+import           Imj.Game.Hamazed.Color
+import           Imj.Game.Hamazed.World.Space.Types
+import           Imj.Game.Hamazed.World.Space
+import           Imj.Game.Hamazed.World.Types
+import           Imj.Geo.Discrete
+import           Imj.Physics.Discrete.Collision
+import           Imj.Graphics.UI.Animation
+
+
+{-# INLINABLE renderWorld #-}
+renderWorld :: (Draw e, MonadReader e m, MonadIO m)
+            => World
+            -> m ()
+renderWorld
+  (World balls (BattleShip (PosSpeed shipCoords _) _ safeTime collisions)
+         space _ (InTerminal _ upperLeft))  = do
+  -- render numbers, including the ones that will be destroyed, if any
+  let s = translateInDir Down $ translateInDir RIGHT upperLeft
+  mapM_ (\b -> renderNumber b space s) balls
+  when ((null collisions || isJust safeTime) && (InsideWorld == location shipCoords space)) $ do
+    let colors =
+          if isNothing safeTime
+            then
+              shipColors
+            else
+              shipColorsSafe
+    drawChar '+' (sumCoords shipCoords s) colors
+
+
+{-# INLINABLE renderNumber #-}
+renderNumber :: (Draw e, MonadReader e m, MonadIO m)
+             => Number
+             -> Space
+             -> Coords Pos
+             -> m ()
+renderNumber (Number (PosSpeed pos _) i) space b =
+  when (location pos space == InsideWorld) $
+    drawChar (intToDigit i) (sumCoords pos b) (numberColor i)
diff --git a/src/Imj/Game/Hamazed/World/Ship.hs b/src/Imj/Game/Hamazed/World/Ship.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/World/Ship.hs
@@ -0,0 +1,56 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE LambdaCase #-}
+
+module Imj.Game.Hamazed.World.Ship
+        ( shipAnims
+        , createShipPos
+        ) where
+
+import           Imj.Prelude
+
+import           Data.Char( intToDigit )
+import           Data.List( foldl' )
+import           Data.Maybe( isNothing )
+
+import           Imj.Game.Hamazed.Loop.Event
+import           Imj.Game.Hamazed.World.Space
+import           Imj.Geo.Discrete
+import           Imj.Geo.Continuous
+import           Imj.Graphics.Animation
+
+{- | If the ship is colliding and not in "safe time", and the event is a gamestep,
+this function creates an animation where the ship and the colliding number explode.
+
+The ship animation will have the initial speed of the number and vice-versa,
+to mimic the rebound due to the collision.
+-}
+shipAnims :: World
+          -> KeyTime
+          -> [Animation]
+shipAnims world@(World _ (BattleShip (PosSpeed shipCoords shipSpeed) _ safeTime collisions) _ _ _) k =
+  if not (null collisions) && isNothing safeTime
+    then
+      -- when number and ship explode, they exchange speeds
+      let collidingNumbersAvgSpeed = foldl' sumCoords zeroCoords $ map (\(Number (PosSpeed _ speed) _) -> speed) collisions
+          numSpeed = scalarProd 0.4 $ speed2vec collidingNumbersAvgSpeed
+          shipSpeed2 = scalarProd 0.4 $ speed2vec shipSpeed
+          (Number _ n) = head collisions
+          interaction = environmentInteraction world WorldFrame
+      in  fragmentsFreeFallThenExplode numSpeed shipCoords interaction (Speed 1) (Right k) '|'
+          ++
+          fragmentsFreeFallThenExplode shipSpeed2 shipCoords interaction (Speed 1) (Right k) (intToDigit n)
+    else
+      []
+
+
+createShipPos :: Space -> [Number] -> IO PosSpeed
+createShipPos space numbers = do
+  let numPositions = map (\(Number (PosSpeed pos _) _) -> pos) numbers
+  candidate@(PosSpeed pos _) <- createRandomNonCollidingPosSpeed space
+  if pos `notElem` numPositions
+    then
+      return candidate
+    else
+      createShipPos space numbers
diff --git a/src/Imj/Game/Hamazed/World/Size.hs b/src/Imj/Game/Hamazed/World/Size.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/World/Size.hs
@@ -0,0 +1,39 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+{-# LANGUAGE NoImplicitPrelude #-}
+
+module Imj.Game.Hamazed.World.Size
+    ( maxWorldSize
+    , worldSizeFromLevel
+    ) where
+
+import           Imj.Prelude
+
+import           Imj.Game.Hamazed.Level.Types
+import           Imj.Game.Hamazed.World.Types
+import           Imj.Geo.Discrete.Types
+
+maxLevelHeight :: Length Height
+maxLevelHeight = 36
+
+maxLevelWidth :: Length Width
+maxLevelWidth = 2 * fromIntegral maxLevelHeight
+
+maxWorldSize :: Size
+maxWorldSize = Size maxLevelHeight maxLevelWidth
+
+heightFromLevel :: Int -> Length Height
+heightFromLevel level =
+  maxLevelHeight + fromIntegral (2 * (firstLevel-level)) -- less and less space as level increases
+
+
+worldSizeFromLevel :: Int
+                   -- ^ 'Level' number
+                   -> WorldShape -> Size
+worldSizeFromLevel level shape =
+  let h = heightFromLevel level
+      -- we need even world dimensions to ease level construction
+      w = fromIntegral $ assert (even h) h * case shape of
+        Square       -> 1
+        Rectangle2x1 -> 2
+  in Size h w
diff --git a/src/Imj/Game/Hamazed/World/Space.hs b/src/Imj/Game/Hamazed/World/Space.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/World/Space.hs
@@ -0,0 +1,337 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE LambdaCase #-}
+
+module Imj.Game.Hamazed.World.Space
+    ( Space
+    , Material(..)
+    , mkEmptySpace
+    , mkDeterministicallyFilledSpace
+    , mkRandomlyFilledSpace
+    , RandomParameters(..)
+    , Strategy(..)
+    , location
+    , scopedLocation
+    , Boundaries(..)
+    , renderSpace
+    , createRandomNonCollidingPosSpeed
+    -- * Reexports
+    , module Imj.Graphics.Render
+    ) where
+
+import           Imj.Prelude
+
+import           Control.Monad.IO.Class(MonadIO)
+import           Control.Monad.Reader.Class(MonadReader)
+import           System.Console.Terminal.Size( Window(..) )
+import           Data.Graph( Graph
+                           , graphFromEdges
+                           , components )
+import           Data.List(length, group, concat, mapAccumL)
+import           Data.Maybe(mapMaybe)
+import           Data.Matrix( getElem
+                            , fromLists
+                            , getMatrixAsVector
+                            , Matrix
+                            , nrows, ncols )
+import           Data.Vector(Vector, slice, (!))
+import           Foreign.C.Types( CInt(..) )
+
+import           Imj.Game.Hamazed.Color
+import           Imj.Game.Hamazed.World.Space.Types
+import           Imj.Geo.Discrete
+import           Imj.Graphics.Render
+import           Imj.Physics.Discrete
+import           Imj.Util
+
+-- | Creates a 'PosSpeed' such that its position is not colliding,
+-- and moves to precollision and mirrors speed if a collision is detected for
+-- the next step (see 'mirrorSpeedAndMoveToPrecollisionIfNeeded').
+createRandomNonCollidingPosSpeed :: Space -> IO PosSpeed
+createRandomNonCollidingPosSpeed space = do
+  pos <- randomNonCollidingPos space
+  dx <- randomSpeed
+  dy <- randomSpeed
+  return $ fst
+    $ mirrorSpeedAndMoveToPrecollisionIfNeeded (`location` space)
+    $ PosSpeed pos (Coords (Coord dx) (Coord dy))
+
+oneRandom :: Int -> Int -> IO Int
+oneRandom a b = do
+  r <- randomRsIO a b
+  return $ head $ take 1 r
+
+randomSpeed :: IO Int
+randomSpeed = oneRandom (-1) 1
+
+randomNonCollidingPos :: Space -> IO (Coords Pos)
+randomNonCollidingPos space@(Space _ worldSize _) = do
+  coords <- randomCoords worldSize
+  case getMaterial coords space of
+    Wall -> randomNonCollidingPos space
+    Air -> return coords
+
+randomInt :: Int -> IO Int
+randomInt sz =
+  oneRandom 0 (sz-1)
+
+randomCoords :: Size -> IO (Coords Pos)
+randomCoords (Size rs cs) = do
+  r <- randomCoord $ fromIntegral rs
+  c <- randomCoord $ fromIntegral cs
+  return $ Coords r c
+
+randomCoord :: Coord a -> IO (Coord a)
+randomCoord (Coord sz) = Coord <$> randomInt sz
+
+forEachRowPure :: Matrix CInt -> Size -> (Coord Row -> (Coord Col -> Material) -> b) -> [b]
+forEachRowPure mat (Size nRows nColumns) f =
+  let rowIndexes = [0..fromIntegral $ nRows-1] -- index of inner row
+      internalRowLength = nInternalColumns
+      rowLength = internalRowLength - 2
+      nInternalColumns = nColumns + 2  -- size of a column in the matrix
+      matAsOneVector = flatten mat -- this is O(1)
+  in map (\rowIdx -> do
+    let internalRowIdx = succ rowIdx
+        startInternalIdx = fromIntegral internalRowIdx * fromIntegral nInternalColumns :: Int
+        startIdx = succ startInternalIdx
+        row = slice startIdx (fromIntegral rowLength) matAsOneVector
+    f rowIdx (\c -> mapInt $ row ! fromIntegral c)) rowIndexes
+
+-- unfortunately I didn't find a Matrix implementation that supports arbitrary types
+-- so I need to map my type on a CInt
+mapMaterial :: Material -> CInt
+mapMaterial Air  = 0
+mapMaterial Wall = 1
+
+mapInt :: CInt -> Material
+mapInt 0 = Air
+mapInt 1 = Wall
+mapInt _ = error "mapInt arg out of bounds"
+
+-- | Creates a rectangular empty space of size specified in parameters.
+mkEmptySpace :: Size -> Space
+mkEmptySpace s =
+  let air  = mapMaterial Air
+  in mkSpaceFromInnerMat s [[air]]
+
+-- | Creates a rectangular deterministic space of size specified in parameters.
+mkDeterministicallyFilledSpace :: Size -> Space
+mkDeterministicallyFilledSpace s@(Size heightEmptySpace widthEmptySpace) =
+  let wall = mapMaterial Wall
+      air  = mapMaterial Air
+
+      w = fromIntegral widthEmptySpace
+      h = fromIntegral heightEmptySpace
+      middleRow = replicate w air
+      collisionRow = replicate 2 air ++ replicate (w-4) wall ++ replicate 2 air
+      ncolls = 8 :: Int
+      nEmpty = h - ncolls
+      n1 = quot nEmpty 2
+      n2 = nEmpty - n1
+      l = replicate n1 middleRow ++ replicate ncolls collisionRow ++ replicate n2 middleRow
+  in mkSpaceFromInnerMat s l
+
+-- | Creates a rectangular random space of size specified in parameters, with a
+-- one-element border. 'IO' is used for random numbers generation.
+mkRandomlyFilledSpace :: RandomParameters -> Size -> IO Space
+mkRandomlyFilledSpace (RandomParameters blockSize strategy) s = do
+  smallWorldMat <- mkSmallWorld s blockSize strategy
+
+  let innerMat = replicateElements blockSize $ map (replicateElements blockSize) smallWorldMat
+  return $ mkSpaceFromInnerMat s innerMat
+
+--  TODO We could measure, on average, how many tries it takes to generate a graph
+--  that meets the requirement for usual values of:
+--  - probability of having air vs. a wall at any cell
+--  - size of the small world
+{- | Generates a random world with the constraint that it should have
+a single "Air" connected component. The function recurses and builds a new random world
+until the constraint is met.
+It might take "a long time" especially if worldsize is big and multFactor is small.
+An interesting problem would be to compute the complexity of this function.
+To do so we need to know the probability to have a unique connected component in
+the random graph defined in the function.
+-}
+mkSmallWorld :: Size
+             -- ^ Size of the big world
+             -> Int
+             -- ^ Pixel width (if 1, the small world will have the same size as the big one)
+             -> Strategy
+             -> IO [[CInt]]
+             -- ^ the "small world"
+mkSmallWorld s@(Size heightEmptySpace widthEmptySpace) multFactor strategy = do
+  let nCols = quot widthEmptySpace $ fromIntegral multFactor
+      nRows = quot heightEmptySpace $ fromIntegral multFactor
+      mkRandomRow _ = take (fromIntegral nCols) <$> rands -- TODO use a Matrix directly
+  smallMat <- mapM mkRandomRow [0..nRows-1]
+
+  let mat = fromLists smallMat
+      graph = graphOfIndex (mapMaterial Air) mat
+  case strategy of
+    StrictlyOneComponent -> case components graph of
+      [_] -> return smallMat -- TODO return Matrix (mat) instead of list of list
+      _   -> mkSmallWorld s multFactor strategy
+
+graphOfIndex :: CInt -> Matrix CInt -> Graph
+graphOfIndex matchIdx mat =
+  let sz@(nRows,nCols) = size mat
+      coords = [Coords (Coord r) (Coord c) | c <-[0..nCols-1], r <- [0..nRows-1], mat `at` (r, c) == matchIdx]
+      edges = map (\c -> (c, c, connectedNeighbours matchIdx c mat sz)) coords
+      (graph, _, _) = graphFromEdges edges
+  in graph
+
+-- these functions adapt the API of matrix to the API of hmatrix
+size :: Matrix a -> (Int, Int)
+size mat = (nrows mat, ncols mat)
+
+flatten :: Matrix a -> Vector a
+flatten = getMatrixAsVector
+
+at :: Matrix a -> (Int, Int) -> a
+at mat (i, j) = getElem (succ i) (succ j) mat -- indexes start at 1 in Data.Matrix
+
+connectedNeighbours :: CInt -> Coords Pos -> Matrix CInt -> (Int, Int) -> [Coords Pos]
+connectedNeighbours matchIdx coords mat (nRows,nCols) =
+  let neighbours = [translateInDir LEFT coords, translateInDir Down coords]
+  in mapMaybe (\other@(Coords (Coord r) (Coord c)) ->
+        if r < 0 || c < 0 || r >= nRows || c >= nCols || mat `at` (r, c) /= matchIdx
+          then
+            Nothing
+          else
+            Just other) neighbours
+
+mkSpaceFromInnerMat :: Size -> [[CInt]] -> Space
+mkSpaceFromInnerMat s innerMatMaybeSmaller =
+  let innerMat = extend s innerMatMaybeSmaller
+      mat = fromLists $ addBorder s innerMat
+  in Space mat s $ matToRenderGroups mat s
+
+extend :: Size -> [[a]] -> [[a]]
+extend (Size rs cs) mat =
+  extend' (fromIntegral rs) $ map (extend' $ fromIntegral cs) mat
+
+extend' :: Int -> [a] -> [a]
+extend' _ [] = error "extend empty list not supported"
+extend' sz l@(_:_) =
+  let len = length l
+      addsTotal = sz - assert (len <= sz) len
+      addsLeft = quot addsTotal 2
+      addsRight = addsTotal - addsLeft
+  in replicate addsLeft (head l) ++ l ++ replicate addsRight (last l)
+
+rands :: IO [CInt]
+rands = randomRsIO 0 1
+
+addBorder :: Size -> [[CInt]] -> [[CInt]]
+addBorder (Size _ widthEmptySpace) l =
+  let nCols = fromIntegral widthEmptySpace + 2 * borderSize
+      wall = mapMaterial Wall
+      wallRow = replicate nCols wall
+      encloseIn b e = b ++ e ++ b
+  in encloseIn (replicate borderSize wallRow) $ map (encloseIn $ replicate borderSize wall) l
+
+borderSize :: Int
+borderSize = 1
+
+matToRenderGroups :: Matrix CInt -> Size -> [RenderGroup]
+matToRenderGroups mat s@(Size _ cs) =
+  concat $
+    forEachRowPure mat s $
+      \row accessMaterial ->
+          snd $ mapAccumL
+                  (\col listMaterials@(material:_) ->
+                     let count = length listMaterials
+                         materialColor = case material of
+                           Wall -> wallColors
+                           Air -> airColors
+                         materialChar = case material of
+                           Wall -> 'Z'
+                           Air -> ' '
+                     in (col + fromIntegral count,
+                         RenderGroup (Coords row col) materialColor materialChar count))
+                  (Coord 0) $ group $ map accessMaterial [0..fromIntegral $ pred cs]
+
+getInnerMaterial :: Coords Pos -> Space -> Material
+getInnerMaterial (Coords (Coord r) (Coord c)) (Space mat _ _) =
+  mapInt $ mat `at` (r+borderSize, c+borderSize)
+
+
+-- | <https://hackage.haskell.org/package/matrix-0.3.5.0/docs/Data-Matrix.html#v:getElem Indices start at 1>:
+-- @Coord 0 0@ corresponds to indexes 1 1 in matrix
+getMaterial :: Coords Pos -> Space -> Material
+getMaterial coords@(Coords r c) space@(Space _ (Size rs cs) _)
+  | r < 0 || c < 0       = Wall
+  | r > fromIntegral(rs-1) || c > fromIntegral(cs-1) = Wall
+  | otherwise = getInnerMaterial coords space
+
+materialToLocation :: Material -> Location
+materialToLocation m = case m of
+  Wall -> OutsideWorld
+  Air  -> InsideWorld
+
+-- | Considers that outside 'Space', everything is 'OutsideWorld'
+location :: Coords Pos -> Space -> Location
+location c s = materialToLocation $ getMaterial c s
+
+-- | Considers that outside 'Space', everything is 'InsideWorld'
+strictLocation :: Coords Pos -> Space -> Location
+strictLocation coords@(Coords r c) space@(Space _ (Size rs cs) _)
+    | r < 0 || c < 0 || r > fromIntegral(rs-1) || c > fromIntegral(cs-1) = InsideWorld
+    | otherwise = materialToLocation $ getInnerMaterial coords space
+
+
+{-# INLINABLE renderSpace #-}
+renderSpace :: (Draw e, MonadReader e m, MonadIO m)
+            => Space
+            -> Coords Pos
+            -- ^ World upper left coordinates w.r.t terminal frame.
+            -> m (Coords Pos)
+renderSpace (Space _ _ renderedWorld) upperLeft = do
+  let worldCoords = move borderSize Down $ move borderSize RIGHT upperLeft
+  mapM_ (renderGroup worldCoords) renderedWorld
+  return worldCoords
+
+{-# INLINABLE renderGroup #-}
+renderGroup :: (Draw e, MonadReader e m, MonadIO m)
+            => Coords Pos
+            -> RenderGroup
+            -> m ()
+renderGroup worldCoords (RenderGroup pos colors char count) =
+  drawChars count char (sumCoords pos worldCoords) colors
+
+scopedLocation :: Space
+               -> Maybe (Window Int)
+               -- ^ The terminal size
+               -> Coords Pos
+               -- ^ The world upper left coordinates w.r.t terminal frame.
+               -> Boundaries
+               -- ^ The scope
+               -> Coords Pos
+               -- ^ The coordinates to test
+               -> Location
+scopedLocation space@(Space _ sz _) mayTermWindow wcc =
+  -- Use a big terminal by default, it will just make animations be updated for longer
+  -- than they should:
+  let worldLocation = (`location` space)
+      worldLocationExcludingBorders = (`strictLocation` space)
+      (Window h w) = fromMaybe (Window {height = 150, width = 400}) mayTermWindow
+      terminalLocation coordsInWorld =
+        let (Coords (Coord r) (Coord c)) = sumCoords coordsInWorld wcc
+        in if r >= 0 && r < h && c >= 0 && c < w
+             then
+               InsideWorld
+             else
+               OutsideWorld
+      productLocations l l' = case l of
+        InsideWorld -> l'
+        OutsideWorld -> OutsideWorld
+  in \case
+    WorldFrame -> worldLocation
+    TerminalWindow -> (\coo-> if containsWithOuterBorder coo sz
+                                then
+                                  OutsideWorld
+                                else
+                                  terminalLocation coo)
+    Both -> (\coo-> productLocations (terminalLocation coo) (worldLocationExcludingBorders coo))
diff --git a/src/Imj/Game/Hamazed/World/Space/Types.hs b/src/Imj/Game/Hamazed/World/Space/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/World/Space/Types.hs
@@ -0,0 +1,70 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+{-# LANGUAGE NoImplicitPrelude #-}
+
+module Imj.Game.Hamazed.World.Space.Types
+    ( Space(..)
+    , Material(..)
+    , RandomParameters(..)
+    , Strategy(..)
+    , RenderGroup(..)
+    , Boundaries(..)
+    , module Imj.Geo.Discrete.Types
+    ) where
+
+import           Imj.Prelude
+
+import           Data.Matrix( Matrix )
+
+import           Foreign.C.Types( CInt(..) )
+
+import           Imj.Geo.Discrete.Types
+import           Imj.Graphics.Color.Types
+
+data Strategy = StrictlyOneComponent
+              -- ^ There should be a single connected component of air.
+              --
+              -- This way, the ship can reach any allowed location without having to go
+              -- through 'Wall's.
+
+-- TODO support a world / air ratio (today it is 50 50)
+-- | Parameters for random walls creation.
+data RandomParameters = RandomParameters {
+    _randomWallsBlockSize :: !Int -- TODO support 'Size' to have non-square blocks
+    -- ^ The size of a square wall block.
+    --
+    -- Note that the smaller the block size, the harder it will be for the algorithm to find
+    -- a random world with a single component of air.
+  , _randomWallsStrategy :: !Strategy
+    -- ^ Space characteristics (only /one connected component/ is available for the moment)
+}
+
+data RenderGroup = RenderGroup {
+    _renderGroupCoords :: !(Coords Pos)
+  , _renderGroupColors :: !LayeredColor
+  , _renderGroupChar :: !Char
+  , _renderGroupCount :: !Int
+}
+
+data Space = Space {
+    _space :: !(Matrix CInt)
+    -- ^ The material matrix.
+  , _spaceSize :: !Size
+    -- ^ The AABB of the space, excluding the outer border.
+  , _spaceRender :: ![RenderGroup]
+    -- ^ How to render the space.
+}
+
+data Material = Air
+              -- ^ In it, ship and numbers can move.
+              | Wall
+              -- ^ Ship and numbers rebound on 'Wall's.
+              deriving(Eq, Show)
+
+data Boundaries = WorldFrame
+                -- ^ Just the world.
+                | TerminalWindow
+                -- ^ The terminal, not the world.
+                | Both
+                -- ^ The terminal.
+                deriving(Show)
diff --git a/src/Imj/Game/Hamazed/World/Types.hs b/src/Imj/Game/Hamazed/World/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Imj/Game/Hamazed/World/Types.hs
@@ -0,0 +1,106 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE LambdaCase #-}
+
+module Imj.Game.Hamazed.World.Types
+        ( World(..)
+        , WallDistribution(..)
+        , WorldShape(..)
+        , BattleShip(..)
+        , Number(..)
+        , Boundaries(..)
+        , mkWorldContainer
+        , InTerminal(..)
+        , environmentInteraction
+        -- * Reexports
+        , module Imj.Iteration
+        , module Imj.Graphics.Text.Animation
+        , module Imj.Physics.Discrete.Types
+        , Terminal.Window
+        , RectContainer(..)
+        ) where
+
+import           Imj.Prelude
+
+import qualified System.Console.Terminal.Size as Terminal( Window(..))
+
+import           Imj.Game.Hamazed.World.Space.Types
+import           Imj.Game.Hamazed.World.Space
+import           Imj.Geo.Discrete
+import           Imj.Graphics.Animation.Design.Types
+import           Imj.Graphics.Text.Animation
+import           Imj.Graphics.UI.RectContainer
+import           Imj.Iteration
+import           Imj.Physics.Discrete.Types
+import           Imj.Physics.Discrete
+import           Imj.Timing
+
+
+data WorldShape = Square
+                -- ^ Width = Height
+                | Rectangle2x1
+                -- ^ Width = 2 * Height
+
+-- | How should walls be created?
+data WallDistribution = None
+              -- ^ No 'Wall's.
+              | Deterministic
+              -- ^ A Rectangular 'Wall' in the middle of the level.
+              | Random !RandomParameters
+              -- ^ 'Wall's are created with an algorithm involving random numbers.
+
+-- | Helper function to create a 'RectContainer' containing a 'World'.
+mkWorldContainer :: World -> RectContainer
+mkWorldContainer (World _ _ (Space _ sz _) _ (InTerminal _ upperLeft)) =
+  RectContainer sz upperLeft
+
+data World = World {
+    _worldNumbers :: ![Number]
+    -- ^ The remaining 'Number's (shot 'Number's are removed from the list)
+  , _worldShip :: !BattleShip
+    -- ^ The player's 'BattleShip'
+  , _worldSpace :: !Space
+    -- ^ The 'Space' in which 'BattleShip' and 'Number's evolve
+  , _worldAnimations :: ![Animation]
+    -- ^ Visual animations. They don't have an influence on the game, they are just here
+    -- for aesthetics.
+  , _worldEmbedded :: !InTerminal
+    -- ^ To know where we should draw the 'World' from, w.r.t terminal frame.
+}
+
+data InTerminal = InTerminal {
+    _inTerminalSize :: !(Maybe (Terminal.Window Int))
+    -- ^ The size of the terminal window
+  , _inTerminalUpperLeft :: !(Coords Pos)
+    -- ^ The 'World' 's 'RectContainer' upper left coordinates,
+    -- w.r.t terminal frame.
+} deriving (Show)
+
+data BattleShip = BattleShip {
+    _shipPosSpeed :: !PosSpeed
+  -- ^ Discrete position and speed.
+  , _shipAmmo :: !Int
+  -- ^ How many laser shots are left.
+  , _shipSafeUntil :: !(Maybe SystemTime)
+  -- ^ At the beginning of each level, the ship is immune to collisions with 'Number's
+  -- for a given time. This field holds the time at which the immunity ends.
+  , _shipCollisions :: ![Number]
+  -- ^ Which 'Number's are currently colliding with the 'BattleShip'.
+} deriving(Show)
+
+
+data Number = Number {
+    _numberPosSpeed :: !PosSpeed
+  -- ^ Discrete position and speed.
+  , _numberNum :: !Int
+  -- ^ Which number it represents (1 to 16).
+} deriving(Eq, Show)
+
+-- | An interaction function taking into account a 'World' and 'Boundaries'
+environmentInteraction :: World -> Boundaries -> Coords Pos -> InteractionResult
+environmentInteraction (World _ _ space _ (InTerminal mayTermWindow upperLeft)) scope =
+  let worldCorner = translate' 1 1 upperLeft
+  in scopedLocation space mayTermWindow worldCorner scope >>> \case
+    InsideWorld  -> Stable
+    OutsideWorld -> Mutation
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,17 @@
+import Control.Monad.Reader(runReaderT)
+
+import           Imj.Graphics.Render
+import           Imj.Graphics.Render.Delta(runThenRestoreConsoleSettings)
+
+import           Imj.Game.Hamazed.Env
+
+import           Test.Imj.Render
+
+main :: IO ()
+main = do
+  putStrLn "" -- for readablilty
+
+  runThenRestoreConsoleSettings $
+    createEnv
+      >>= runReaderT (testSpace >> -- this is it visible in the logs, not in the console
+                      renderToScreen)
diff --git a/test/Test/Imj/Render.hs b/test/Test/Imj/Render.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Imj/Render.hs
@@ -0,0 +1,13 @@
+module Test.Imj.Render(testSpace) where
+
+import Control.Monad( void )
+import Control.Monad.Reader(liftIO)
+
+import Imj.Game.Hamazed.World.Space
+import Imj.Geo.Discrete.Types
+
+testSpace :: (MonadReader e m, Draw e, MonadIO m) => m ()
+testSpace = do
+  let blocksSize = 6
+  s <- liftIO $ mkRandomlyFilledSpace (RandomParameters blocksSize StrictlyOneComponent) (Size 36 72)
+  void (renderSpace s (Coords 0 0))
