@@ -1,263 +0,0 @@
1−
2−{----- Overview ------------------------------------------------------
3−
4− This game displays some of the strengths of Functional Reactive
5− Programming (FRP). By the end of this file we will have written an
6− entire GUI/game without any imperative code! No global mutable state,
7− no flipping pixels, no destructive updates. In fact, Elm disallows all
8− of these things at the language level. So good design and safe coding
9− practices are a requirement, not just self-inforced suggestions.
10−
11− This code neatly divides Pong into three major parts: modeling the
12− game, updating the game, and viewing the game. It may be helpful to
13− think of it as a functional variation on the Model-View-Controller
14− paradigm.
15−
16− The code for Pong is structured as follows:
17−
18− 1. MODEL:
19− First we need to define Pong. We do this by modelling Pong with
20− simple data structures. We need two categories of model:
21− - Inputs to the game. For Pong, this is keyboard input from
22− users and clock-ticks from the frame rate manager.
23− - A model of the game itself: paddles, ball, score, etc.
24− Without a model of the game we would have nothing to update
25− or display!
26− These models are the basis for the next two sections, holding
27− all of the information about Pong that we will need.
28−
29− 2. UPDATE:
30− When new inputs come in, we need to update the current state
31− of the game. Without updates, this version of Pong would be very
32− very boring! This section defines a number of 'step functions'
33− that step the game forward based on our inputs. By separating
34− this from the model and display code, we can change how the game
35− works (how it steps forward) without changing anything else: the
36− underlying model and the display code need not be touched.
37−
38− 3. VIEW:
39− Finally, we need a display function that defines the user's view
40− of the game. This code is separate from the game logic, so like
41− the update logic, it can be modified without affecting any other
42− part of the program. We can also define many different views
43− of the same underlying model. In Pong there's not much need for
44− this, but as your model becomes more complex this may be very
45− useful!
46−
47− If you would like to make a game or larger application in Elm, use
48− this structure! Maybe even use this file as a starting point for
49− playing around with your own ideas.
50−
51− Let's get started!
52−
53−-----------------------------------------------------------------------}
54−
55−
56−module Pong where
57−
58−import JavaScript
59−
60−
61−-- Set the frames per second (FPS) to 60, calculate the deltas (the
62−-- difference between the two latest times, the amount of time since
63−-- the last frame), and convert the time into a number of seconds.
64−
65−delta = lift inSeconds (fps 60)
66−
67−
68−------------------------------------------------------------------------
69−------ Modelling User Input ------
70−------------------------------------------------------------------------
71−
72−-- During gameplay, all keyboard input is about the position of the
73−-- two paddles. So the keyboard input can be reduced to two directions,
74−-- each represented by an integer in {-1,0,1}. Furthermore, the SPACE
75−-- key is used to start the game between rounds, so we also need a
76−-- boolean value to represent whether it is pressed.
77−
78−data KeyInput = KeyInput Bool Int Int
79−
80−defaultKeyInput = KeyInput False 0 0
81−
82−keyInput = lift3 KeyInput Keyboard.space
83− (lift .y Keyboard.wasd)
84− (lift .y Keyboard.arrows)
85−
86−
87−------------------------------------------------------------------------
88−------ Combining all inputs ------
89−------------------------------------------------------------------------
90−
91−-- The inputs to this game include a timestep (which we extracted from
92−-- JavaScript) and the keyboard input from the users.
93−
94−data Input = Input Float KeyInput
95−
96−-- Combine both kinds of inputs and filter out keyboard events. We only
97−-- want the game to refresh on clock-ticks, not key presses too.
98−
99−input = sampleOn delta (lift2 Input delta keyInput)
100−
101−
102−
103−------------------------------------------------------------------------
104−------ Modelling Pong / a State Machine ------
105−------------------------------------------------------------------------
106−
107−-- Pong has two obvious components: the ball and two paddles.
108−
109−data Paddle = Paddle Float -- y-position
110−data Ball = Ball (Float,Float) (Float,Float) -- position and velocity
111−
112−-- But we also want to keep track of the current score and whether
113−-- the ball is currently in play. This will allow us to have rounds
114−-- of play rather than just having the ball move around continuously.
115−
116−data Score = Score Int Int
117−data State = Play | BetweenRounds
118−
119−-- Together, this information makes up the state of the game. We model
120−-- Pong by using the inputs (defined above) to update the state of the
121−-- game!
122−
123−data GameState = GameState State Score Ball Paddle Paddle
124−
125−
126−-- I have chosen to parameterize the size of the board, so it can
127−-- be changed with minimal effort.
128−
129−gameWidth = 600
130−gameHeight = 400
131−halfWidth = gameWidth / 2
132−halfHeight = gameHeight / 2
133−
134−
135−-- Before we can update anything, we must first define the default
136−-- configuration of the game. In our case we want to start between
137−-- rounds with a score of zero to zero.
138−
139−defaultGame = GameState BetweenRounds
140− (Score 0 0)
141− (Ball (halfWidth, halfHeight) (150,150))
142− (Paddle halfHeight)
143− (Paddle halfHeight)
144−
145−
146−
147−------------------------------------------------------------------------
148−------ Stepping from State to State ------
149−------------------------------------------------------------------------
150−
151−-- Now to step the game from one state to another. We can break this up
152−-- into smaller components.
153−
154−-- First, we define a step function for updating the position of
155−-- paddles. It depends on our timestep and a desired direction (given
156−-- by keyboard input).
157−
158−stepPaddle delta dir (Paddle y) =
159− Paddle $ clamp 20 (gameHeight-20) (y + toFloat dir * 200 * delta)
160−
161−
162−-- We must also step the ball forward. This is more complicated due to
163−-- the many kinds of collisions that can happen. All together, this
164−-- function figures out the new velocity of the ball based on
165−-- collisions with the top and bottom borders and collisions with the
166−-- paddles. This new velocity is used to calculate a new position.
167−
168−-- This function also determines whether a point has been scored and
169−-- who receives the point. Thus, its output is a new Ball and points
170−-- to be added to each player.
171−
172−makePositive n = if n > 0 then n else 0-n
173−makeNegative n = if n > 0 then 0-n else n
174−within epsilon n x = x > n - epsilon && x < n + epsilon
175−
176−stepVelocity velocity lowerCollision upperCollision =
177− if lowerCollision then makePositive velocity else
178− if upperCollision then makeNegative velocity else velocity
179−
180−stepBall delta (Ball (x,y) (vx,vy)) (Paddle y1) (Paddle y2) =
181− let hitPaddle1 = within 20 y1 y && within 8 25 x
182− hitPaddle2 = within 20 y2 y && within 8 (gameWidth - 25) x
183− vx' = stepVelocity vx hitPaddle1 hitPaddle2
184− vy' = stepVelocity vy (y < 7) (y > gameHeight - 7)
185− scored = x > gameWidth || x < 0
186− x' = if scored then halfWidth else x + vx' * delta
187− y' = if scored then halfHeight else y + vy' * delta
188− in ( Ball (x',y') (vx',vy')
189− , if x > gameWidth then 1 else 0
190− , if x < 0 then 1 else 0 )
191−
192−
193−-- Finally, we define a step function for the entire game. This steps from state to
194−-- state based on the inputs to the game.
195−
196−stepGame (Input delta (KeyInput space dir1 dir2))
197− (GameState state (Score s1 s2) ball paddle1 paddle2) =
198− let (ball',s1',s2') = if state == Play then stepBall delta ball paddle1 paddle2
199− else (ball, 0, 0)
200− state' = case state of
201− Play -> if s1' /= s2' then BetweenRounds else state
202− BetweenRounds -> if space then Play else state
203− in GameState state'
204− (Score (s1+s1') (s2+s2'))
205− ball'
206− (stepPaddle delta dir1 paddle1)
207− (stepPaddle delta dir2 paddle2)
208−
209−
210−-- Now we put it all together. We have a signal of inputs that changes whenever there
211−-- is a clock tick. This input signal carries the all the information we need about
212−-- the keyboard. We also have a step function that steps from one game-state to the
213−-- next based on some inputs.
214−
215−-- The `gameState` signal steps forward every time a new input comes in. It starts
216−-- as the default game and progresses based on user behavior.
217−
218−gameState = foldp stepGame defaultGame input
219−
220−
221−
222−------------------------------------------------------------------------
223−------ Displaying the Game ------
224−------------------------------------------------------------------------
225−
226−-- These functions take a GameState and turn it into something a user
227−-- can see and understand. It is totally independent of how the game
228−-- updates, it only needs to know the current game state. This allows us
229−-- to change how the game looks without changing any of the logic of the
230−-- game.
231−
232−
233−-- This function displays the current score and directions.
234−
235−scoreBoard w inPlay p1 p2 =
236− let code = text . monospace . toText
237− stack top bottom = flow down [ code " ", code top, code bottom ]
238− msg = width w . centeredText . monospace $ toText "Press SPACE to begin"
239− board = flow right [ stack "W" "S", spacer 20 1
240− , text . Text.height 4 . toText $ show p1 ++ " " ++ show p2
241− , spacer 20 1, stack "↑" "↓" ]
242− score = container w (heightOf board) midTop board
243− in if inPlay then score else score `above` msg
244−
245−
246−-- This function displays the entire GameState.
247−
248−display (w,h) (GameState state (Score p1 p2) (Ball pos _) (Paddle y1) (Paddle y2)) =
249− layers
250− [ let pongGreen = rgb 60 100 60 in
251− container w h middle $ collage gameWidth gameHeight
252− [ filled pongGreen (rect gameWidth gameHeight (halfWidth,halfHeight))
253− , filled white (oval 15 15 pos) -- ball
254− , filled white (rect 10 40 ( 20, y1)) -- first paddle
255− , filled white (rect 10 40 (gameWidth - 20, y2)) -- second paddle
256− ]
257− , scoreBoard w (state == Play) p1 p2
258− ]
259−
260−-- We can now define a view of the game (a signal of Elements) that changes
261−-- as the GameState changes. This is what the users will see.
262−
263−main = lift2 display Window.dimensions gameState