-
Notifications
You must be signed in to change notification settings - Fork 0
/
Main.elm
395 lines (294 loc) · 8.86 KB
/
Main.elm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
module Main exposing (..)
import Browser
import Browser.Events
import Debug
import Html exposing (Html, div, text)
import Html.Attributes exposing (style)
import Json.Decode as Decode exposing (Decoder)
import List.Extra
import Random
import Result exposing (Result(..), andThen)
import Svg exposing (image, rect, svg)
import Svg.Attributes exposing (fill, height, stroke, viewBox, width, x, xlinkHref, y)
import Time
type alias Coord =
( Int, Int )
type alias Snake =
List Coord
type Direction
= Up
| Down
| Left
| Right
keyDecoder : Decoder Msg
keyDecoder =
Decode.field "key" Decode.string
|> Decode.andThen keyToMsg
keyToMsg : String -> Decoder Msg
keyToMsg s =
case s of
"ArrowLeft" ->
Decode.succeed (KeyPress Left)
"ArrowRight" ->
Decode.succeed (KeyPress Right)
"ArrowUp" ->
Decode.succeed (KeyPress Up)
"ArrowDown" ->
Decode.succeed (KeyPress Down)
_ ->
Decode.fail ("Not interested in " ++ s)
type alias Model =
{ interval : Int
, nextDirection : Direction
, direction : Direction
, snake : Snake
, food : Maybe Coord
}
type Msg
= Tick Time.Posix
| DecrementInterval Time.Posix
| KeyPress Direction
| SetFood Coord
type alias Update =
( Model, Cmd Msg )
config =
{ boxSize = 20
, xBound = 40
, yBound = 30
, initialSnakeLength = 5
, initialInterval = 100
, lowestInterval = 50
, decrementInterval = 20 * 1000
, decrementIntervalBy = 5
}
snakeHead : Snake -> Maybe Coord
snakeHead =
List.Extra.last
snakeBody : Snake -> Maybe (List Coord)
snakeBody =
List.Extra.init
any : List Bool -> Bool
any =
List.foldl (||) False
randomCoord : Int -> Int -> Random.Generator Coord
randomCoord xBound yBound =
Random.map2
(\x y -> ( x, y ))
(Random.int 0 (xBound - 1))
(Random.int 0 (yBound - 1))
intersects : Coord -> Coord -> Bool
intersects a b =
a == b
-- MODEL
init : ( Model, Cmd Msg )
init =
( { interval = config.initialInterval
, nextDirection = Right
, direction = Right
, food = Nothing
, snake = List.map (\x -> ( config.initialSnakeLength + x, config.initialSnakeLength )) (List.range 0 config.initialSnakeLength)
}
, Random.generate SetFood (randomCoord config.xBound config.yBound)
)
-- UPDATE
setDirection : Update -> Result Update Update
setDirection ( model, cmd ) =
Ok ( { model | direction = model.nextDirection }, cmd )
moveSnake : Update -> Result Update Update
moveSnake ( model, cmd ) =
let
updateDirection direction =
case direction of
Up ->
Tuple.mapSecond
(\y ->
if y - 1 < 0 then
config.yBound - 1
else
y - 1
)
Left ->
Tuple.mapFirst
(\x ->
if x - 1 < 0 then
config.xBound - 1
else
x - 1
)
Down ->
Tuple.mapSecond
(\y ->
if y + 1 >= config.yBound then
0
else
y + 1
)
Right ->
Tuple.mapFirst
(\x ->
if x + 1 >= config.xBound then
0
else
x + 1
)
move direction snake_ =
case snakeHead snake_ of
Just head ->
snake_ ++ [ updateDirection direction head ]
Nothing ->
snake_
in
Ok ( { model | snake = move model.nextDirection model.snake }, cmd )
checkCollision : Update -> Result Update Update
checkCollision ( model, cmd ) =
let
body__ =
snakeBody model.snake
head__ =
snakeHead model.snake
in
case ( body__, head__ ) of
( Just b, Just h ) ->
if any (List.map (\part -> intersects part h) b) then
Err init
else
Ok ( model, cmd )
_ ->
Ok ( model, cmd )
eatFood : Update -> Result Update Update
eatFood ( model, cmd ) =
case ( model.food, snakeHead model.snake ) of
( Just food, Just head ) ->
if intersects food head then
Ok ( model, Random.generate SetFood (randomCoord config.xBound config.yBound) )
else
Ok ( { model | snake = List.drop 1 model.snake }, Cmd.none )
_ ->
Ok ( model, Cmd.none )
nextDirection : Direction -> Direction -> Direction
nextDirection prev next =
case ( prev, next ) of
( Down, Up ) ->
Down
( Up, Down ) ->
Up
( Right, Left ) ->
Right
( Left, Right ) ->
Left
_ ->
next
update : Msg -> Model -> Update
update msg model =
case msg of
Tick _ ->
case
( model, Cmd.none )
|> setDirection
|> andThen moveSnake
|> andThen checkCollision
|> andThen eatFood
of
Ok result ->
result
Err result ->
result
DecrementInterval _ ->
( { model
| interval =
if model.interval == config.lowestInterval then
model.interval
else
model.interval - config.decrementIntervalBy
}
, Cmd.none
)
KeyPress direction ->
( { model | nextDirection = nextDirection model.direction direction }, Cmd.none )
SetFood foodCoord ->
( { model | food = Just foodCoord }, Cmd.none )
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions { interval } =
Sub.batch
[ Time.every (toFloat interval) Tick
, Time.every (toFloat config.decrementInterval) DecrementInterval
, Browser.Events.onKeyDown keyDecoder
]
-- VIEWS
view : Model -> Html Msg
view model =
let
width_ =
Debug.toString (config.xBound * config.boxSize)
height_ =
Debug.toString (config.yBound * config.boxSize)
viewBox_ =
"0 0 " ++ width_ ++ " " ++ height_
backgroundView =
rect [ width width_, height height_, fill "#F8F5F0" ] []
snakeView =
snake model.snake
khinkaliView =
case khinkali model.food of
Just view_ ->
backgroundView :: view_ :: snakeView
Nothing ->
backgroundView :: snakeView
in
div
[ style "height" "100vh"
, style "display" "flex"
, style "flex-direction" "column"
, style "align-items" "center"
, style "justify-content" "center"
]
[ div [ style "margin-bottom" "10px", style "font-family" "monospace", style "font-size" "20px", style "font-weight" "bold" ]
[ text ("Score: " ++ Debug.toString ((List.length model.snake - config.initialSnakeLength - 1) * 10))
]
, svg
[ width width_
, height height_
, viewBox viewBox_
]
khinkaliView
]
snake : Snake -> List (Html Msg)
snake =
let
body ( x1, y2 ) =
rect
[ x (Debug.toString (x1 * config.boxSize))
, y (Debug.toString (y2 * config.boxSize))
, width (Debug.toString config.boxSize)
, height (Debug.toString config.boxSize)
, fill "#C54D48"
, stroke "white"
, Svg.Attributes.strokeWidth "1"
]
[]
in
List.map body
khinkali : Maybe Coord -> Maybe (Html Msg)
khinkali coord =
let
imageMapper ( x_, y_ ) =
image
[ x (Debug.toString (x_ * config.boxSize))
, y (Debug.toString (y_ * config.boxSize))
, width (Debug.toString config.boxSize)
, height (Debug.toString config.boxSize)
, xlinkHref "khinkali.jpg"
]
[]
in
Maybe.map imageMapper coord
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = \_ -> init
, view = view
, update = update
, subscriptions = subscriptions
}