elm / core

Elm's core libraries
http://package.elm-lang.org/packages/elm/core/latest
BSD 3-Clause "New" or "Revised" License
2.8k stars 359 forks source link

Uncaught TypeError: Cannot read property 'ctor' of undefined #167

Closed TheSeamau5 closed 9 years ago

TheSeamau5 commented 9 years ago

Seems like I got a runtime error.

Chrome is pointing at the stepperHelp function in Native.Graphics.Collage. Somehow, something weird got through there and the compiler was silent.

screen shot 2015-02-11 at 2 48 18 am

TheSeamau5 commented 9 years ago

Here is the full code of the little project that's causing this:

Model.elm

module Shooter.Model where

import Array (..)

----------
-- MATH --
----------

type alias Point =
  { x : Float
  , y : Float
  }

add : Point -> Point -> Point
add p q =
  Point (p.x + q.x) (p.y + q.y)

square : Float -> Float
square x = x * x

distanceSquared : Point -> Point -> Float
distanceSquared p q =
  square (p.x - q.x) + square (p.y - q.y)

distance : Point -> Point -> Float
distance p q =
  sqrt (distanceSquared p q)

------------
-- ENTITY --
------------

type alias Entity a =
  { a | position : Point
      , velocity : Point
      , radius : Float
  }

collide : Entity a -> Entity b -> Bool
collide entity1 entity2 =
  square (entity1.radius + entity2.radius) <=
  distanceSquared entity1.position entity2.position

move : Entity a -> Entity a
move entity =
  { entity | position <- entity.position `add` entity.velocity }

setVelocity : Point -> Entity a -> Entity a
setVelocity velocity entity =
  { entity | velocity <- velocity }

-------------------
-- GAME ENTITIES --
-------------------

type alias Bullet = Entity { attack : Int }

type alias Ship a =
  Entity { a | health : Int
             , maxHealth : Int }

type alias Hero = Ship {}

hero : Hero
hero =
  { position = Point 200 10
  , velocity = Point 0 0
  , radius = 20
  , health = 10
  , maxHealth = 10
  }

type alias Enemy = Ship {}

enemy : Enemy
enemy =
  { position = Point 200 300
  , velocity = Point 0 0
  , radius = 20
  , health = 1
  , maxHealth = 1
  }

type alias GameState =
  { hero : Hero
  , enemies : Array Enemy
  , bullets : Array Bullet
  }

gameState : GameState
gameState =
  { hero = hero
  , enemies = push enemy empty
  , bullets = empty
  }

isAlive : Ship a -> Bool
isAlive ship =
  ship.health > 0

hit : Bullet -> Ship a -> Ship a
hit bullet ship =
  { ship | health <- ship.health - bullet.attack }

createHeroBullet : Hero -> Int -> Bullet
createHeroBullet hero attack =
  let bulletStartingPosition =
        hero.position `add` Point 0 ((hero.radius / 2) + 2)

      bulletVelocity =
        Point 0 2

      bulletRadius =
        5 * attack

  in
    { position = bulletStartingPosition
    , velocity = bulletVelocity
    , radius = bulletRadius
    , attack = attack
    }

createEnemyBullet : Enemy -> Int -> Bullet
createEnemyBullet enemy attack =
  let bulletStartingPosition =
        enemy.position `add` Point 0 -((enemy.radius / 2) + 2)

      bulletVelocity =
        Point 0 -2

      bulletRadius =
        5 * attack

  in
    { position = bulletStartingPosition
    , velocity = bulletVelocity
    , radius = bulletRadius
    , attack = attack
    }

Input.elm

module Shooter.Input where

import Shooter.Model (..)

import Keyboard
import Signal (..)
import Time (..)

type alias Input =
  { arrows : Point
  , space : Bool
  }

toInput : { x : Int, y : Int} -> Bool -> Input
toInput arrows space =
  { arrows =
    { x = toFloat arrows.x
    , y = toFloat arrows.y
    }
  , space = space
  }

userInput : Signal Input
userInput =
  map2 toInput Keyboard.arrows Keyboard.space

timeDelta : Signal Time
timeDelta =
  fps 60

Render.elm

module Shooter.Render where

import Graphics.Element (..)
import Graphics.Collage (..)
import Color (..)
import Shooter.Model as Model
import Array (..)

renderHero : Model.Hero -> Form
renderHero hero =
  move (hero.position.x, hero.position.y) <|
    filled blue <|
      circle hero.radius

renderEnemy : Model.Enemy -> Form
renderEnemy enemy =
  move (enemy.position.x, enemy.position.y) <|
    filled red <|
      circle enemy.radius

renderBullet : Model.Bullet -> Form
renderBullet bullet =
  move (bullet.position.x, bullet.position.y) <|
    filled green <|
      circle bullet.radius

render : Model.GameState -> Element
render game =
  let enemyForms =
        map renderEnemy game.enemies
      bulletForms =
        map renderBullet game.bullets
      heroForm =
        renderHero game.hero
  in
    collage 400 400
      (toList
        (push heroForm (enemyForms `append` bulletForms)))

Update.elm

module Shooter.Update where

import Shooter.Input (..)
import Shooter.Model (..)

import Array (..)

pipe : (a -> b -> b) -> Array a -> b -> b
pipe f array b =
  case get 0 array of
    Nothing -> b
    Just a ->
      pipe f (slice 1 (length array) array) (f a b)

update : Input -> GameState -> GameState
update input game =
  let bullets =
        if input.space == True
        then
          push (createHeroBullet game.hero 1) game.bullets
        else
          game.bullets
      heroVelocity =
        input.arrows
      applyBullets entity =
        pipe hit
             (filter (collide entity) game.bullets)
             entity
      hero =
        setVelocity heroVelocity
                    (move
                      (applyBullets game.hero))
      enemies =
        map move
            (filter isAlive
                    (map applyBullets game.enemies))

  in
    { game | hero <- hero
           , enemies <- enemies
           , bullets <- bullets
    }

Shooter.elm (the main file)

import Shooter.Model (..)
import Shooter.Input (..)
import Shooter.Update (..)
import Shooter.Render (..)

import Signal (..)

main =
  map render
      (foldp update gameState userInput)
TheSeamau5 commented 9 years ago

I have a sneaking suspicion that it has to do with me using Arrays alongside all this collage stuff... I'm not sure... I have to further investigate

TheSeamau5 commented 9 years ago

Same bug, smaller code:

Model.elm

module Platformer.Model where

import Array (..)

type alias Point =
  { x : Float
  , y : Float
  }

type alias Entity a =
  { a | position : Point
      , velocity : Point
      , dimensions : Point
  }

type alias Hero =
  Entity { isGrounded : Bool }

hero : Hero
hero =
  { position = Point 0 0
  , velocity = Point 0 0
  , dimensions = Point 10 10
  , isGrounded = True
  }

type alias Platform =
  Entity {}

type alias Game =
  { hero : Hero
  , platforms : Array Platform
  }

game : Game
game =
  { hero = hero
  , platforms = empty
  }

Render.elm

module Platformer.Render where

import Platformer.Model (Game, Hero, Platform)

import Graphics.Collage (..)
import Graphics.Element (..)
import Color (..)
import Array (..)

renderHero : Hero -> Form
renderHero hero =
  move (hero.position.x, hero.position.y) <|
    filled green <|
      rect hero.dimensions.x hero.dimensions.y

renderPlatform : Platform -> Form
renderPlatform platform =
  move (platform.position.x, platform.position.y) <|
    filled blue <|
      rect platform.dimensions.x platform.dimensions.y

render : Game -> Element
render game =
  collage 400 400
    (toList
      (push (renderHero game.hero)
            (map renderPlatform game.platforms)))

Platformer.elm

import Platformer.Model (..)
import Platformer.Render (..)

main = render game
evancz commented 9 years ago

Is it possible to get it down to one file? To properly debug, it's best if we can get it down to as small as possible. Have we gotten there?

TheSeamau5 commented 9 years ago

Interesting, when you switch to lists, everything works. So my suspicion seems warranted.

Model.elm

module Platformer.Model where

import List (..)

type alias Point =
  { x : Float
  , y : Float
  }

type alias Entity a =
  { a | position : Point
      , velocity : Point
      , dimensions : Point
  }

type alias Hero =
  Entity { isGrounded : Bool }

hero : Hero
hero =
  { position = Point 0 0
  , velocity = Point 0 0
  , dimensions = Point 10 10
  , isGrounded = True
  }

type alias Platform =
  Entity {}

type alias Game =
  { hero : Hero
  , platforms : List Platform
  }

game : Game
game =
  { hero = hero
  , platforms = []
  }

Render.elm

module Platformer.Render where

import Platformer.Model (Game, Hero, Platform)

import Graphics.Collage (..)
import Graphics.Element (..)
import Color (..)
import List (..)

renderHero : Hero -> Form
renderHero hero =
  move (hero.position.x, hero.position.y) <|
    filled green <|
      rect hero.dimensions.x hero.dimensions.y

renderPlatform : Platform -> Form
renderPlatform platform =
  move (platform.position.x, platform.position.y) <|
    filled blue <|
      rect platform.dimensions.x platform.dimensions.y

render : Game -> Element
render game =
  collage 400 400
    ((::) (renderHero game.hero)
          (map renderPlatform game.platforms))

Platformer.elm

import Platformer.Model (..)
import Platformer.Render (..)

main = render game
TheSeamau5 commented 9 years ago

Nope, I haven't gotten there, but I feel like I'm almost there.

TheSeamau5 commented 9 years ago

AHA!

import Graphics.Collage (filled, square, Form, collage)
import Graphics.Element (Element)
import Array (..)
import Color (green)

renderBox : (Float, Float) -> Form
renderBox (x,y) =
  filled green <|
    square 10

scene : Element
scene =
  collage 400 400
    (toList
      (map renderBox empty))

main : Element
main = scene

There seems to be an issue with mapping renderBox on empty

TheSeamau5 commented 9 years ago

Could it be related to this:

When I asText an empty list

import Text (asText)

main =
  asText []

I get this:

screen shot 2015-02-11 at 11 00 45 pm

which is expected.

But if I toList an empty array and asText it

import Text (asText)
import Array (..)

main =
  asText
    (toList
      (map (\x -> x * x) empty))

I get this:

screen shot 2015-02-11 at 11 02 03 pm

which is cryptic. I'd expect to just get the empty list with nothing inside.

In all those cases I was mapping a function over an empty array that was getting converted back to a list. Maybe that could be the problem?

evancz commented 9 years ago

Quite sketchy. I am not very familiar with the Array implementation, so I would not be terribly surprised if there was some bug there.

TheSeamau5 commented 9 years ago

Yeah it is.

I think I've found it, the bug is with the Array.map function.

If I convert an empty array to a list and ask for its length:

import Text (asText)
import Array (..)
import List

main =
  asText
    (List.length
      (toList empty))

I get:

screen shot 2015-02-11 at 11 10 34 pm

But, if I first map a function on an empty array and then call toList and ask for its length:

import Text (asText)
import Array (..)
import List

main =
  asText
    (List.length
      (toList
        (map (\x -> x * x) empty)))

I get:

screen shot 2015-02-11 at 11 09 28 pm

TheSeamau5 commented 9 years ago

I'm not familiar with the Array implementation either, but at least now you know where to look :D

TheSeamau5 commented 9 years ago

So, I totally just found a quickfix to it and everything works! Yay!

The commit: fbea1ba

TheSeamau5 commented 9 years ago

Solved in: https://github.com/elm-lang/core/pull/171