1 # This file is part of NIT (http://www.nitlanguage.org).
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
15 # A game of memory using shapes and colors
19 # * [X] Various shapes, colors and sounds
20 # * [X] 3 difficulty modes
21 # * [X] Saved high scores
22 # * [ ] Level selection
24 # The remaining issues are
26 # * Crappy event system
27 # * Crappy UI element placement
29 app_name
("Memorize Shapes and Colors")
30 app_version
(0, 1, git_revision
)
35 import mnit
::opengles1
36 import app
::data_store
40 # A figure to click on
42 # The place, starting from 0.
43 # Will be used to derive the display place.
46 # The color of the figure
49 # The shape of the figure
52 # The sound of the figure
55 # x-coordinate on the display
57 # y-coordinate on the display
59 # width on the display
64 # Event time to live (from 1.0 downto 0.0)
67 # Is there a big error on the button?
70 # The initial position (according to shuffle)
71 var from
: Pos is noinit
73 # The current path if shuffling
74 var path
: nullable BPath = null
76 # The second path if hard shuffling
77 var path2
: nullable BPath = null
79 # Is there an hard shuffling?
82 # The optional text on the button (in the menu)
83 var text
: nullable Image = null
85 # The color of the text
86 var text_color
: nullable Color = null
88 # The high score on the menu button
92 fun blit_on
(display
: Display)
100 if path
!= null then ttl
= path
.duration
111 path
.update
(pos
, ttl
)
116 if path2
!= null then
127 if not app
.player
then
131 color
.set
(display
, p
)
132 display
.blit_centered
(shape
, x
, y
)
135 text
.scale
= shape
.scale
136 text_color
.set
(display
, p
)
137 display
.blit_centered
(text
, x
, y
- h
/8.0)
139 app
.blit_number
(text_max
, app
.scale
, x
, y
+ h
/8.0)
142 if display
isa Opengles1Display then
146 app
.drawing
.error
.scale
= app
.scale
147 display
.blit_centered
(app
.drawing
.error
, x
, y
)
152 return "{place},{color},{shape},{sound}"
156 fun has
(x
,y
: Float): Bool
158 return (self.x
- x
).abs
*2.0 <= w
and (self.y
- y
).abs
*2.0 <= h
161 # Return a new pos centered on the button
162 fun to_pos
: Pos do return new Pos(x
, y
)
167 # red (from 0.0 to 1.0)
169 # green (from 0.0 to 1.0)
171 # blue (from 0.0 to 1.0)
174 # Globally change the color of the display.
175 # The color will be used for the next blit operations.
176 # The color of the display has to be reseted manually (see `Opengles1Display::reset_color`).
177 fun set
(display
: Display, p
: Float)
179 if display
isa Opengles1Display then
180 display
.color
(p
.lerp
(r
,1.0),p
.lerp
(g
,1.0),p
.lerp
(b
,1.0),p
.lerp
(1.0,0.0))
185 # A point in the display coordinates
191 redef fun to_s
do return "({x},{y})"
194 # A cubic Bézier path between two points with two handles.
198 # The handle of the origin point
200 # The handle of the destination point
202 # The destination point
204 # The duration on the path
207 # Update the coordinates of `cursor` for an absolute time to destination `ttd`
208 fun update
(cursor
: Pos, ttd
: Float)
210 var p
= 1.0 - ttd
/ duration
220 var bx
= p
.cerp
(from
.x
, from_handle
.x
, to_handle
.x
, to
.x
)
221 var by
= p
.cerp
(from
.y
, from_handle
.y
, to_handle
.y
, to
.y
)
229 # # Assets and resources
231 # All the images assets
232 var drawing
= new DrawingImages
234 # Array of all available colors for the figures
235 var colors
= new Array[Color]
237 # Array of all available shapes for the figures
238 var shapes
= new Array[Image]
240 # Array of all available sounds for the figures
241 var sounds
= new Array[Sound]
243 # The sound to play on error (error)
244 var snd_penalty
: Sound is noautoinit
246 # The sound of other ui element
247 var snd_click
: Sound is noautoinit
252 colors
.add
new Color(0.9, 0.6, 0.0)
253 colors
.add
new Color(0.6, 0.0, 0.9)
254 colors
.add
new Color(0.6, 0.5, 0.4)
255 colors
.add
new Color(1.0, 0.0, 0.0)
256 colors
.add
new Color(1.0, 1.0, 0.0)
257 colors
.add
new Color(1.0, 0.0, 1.0)
258 colors
.add
new Color(0.0, 1.0, 0.0)
259 colors
.add
new Color(0.0, 1.0, 1.0)
260 colors
.add
new Color(0.0, 0.0, 1.0)
262 drawing
.load_all
(self)
264 shapes
.add drawing
.circle
265 shapes
.add drawing
.rect
266 shapes
.add drawing
.cross
267 shapes
.add drawing
.penta
268 shapes
.add drawing
.star
269 shapes
.add drawing
.triangle
270 shapes
.add drawing
.heart
271 shapes
.add drawing
.diamond
272 shapes
.add drawing
.moon
273 shapes
.add drawing
.spiral
275 number_images
= new NumberImages(drawing
.n
)
278 sounds
.add
new Sound("bing.wav")
279 sounds
.add
new Sound("boing.wav")
280 sounds
.add
new Sound("cymbal.wav")
281 sounds
.add
new Sound("dart.wav")
282 sounds
.add
new Sound("duh.wav")
283 sounds
.add
new Sound("grunt.wav")
284 sounds
.add
new Sound("honkhonk.wav")
285 sounds
.add
new Sound("line_end.wav")
286 sounds
.add
new Sound("squishy-hit.wav")
287 sounds
.add
new Sound("woodthunk.wav")
288 sounds
.add
new Sound("whip.wav")
290 snd_penalty
= new Sound("penalty.wav")
291 snd_click
= new Sound("click.wav")
293 # Force load the sounds. Required because bug #1728
294 for s
in sounds
do s
.load
297 is_menu
= data_store
["game"] != true
298 mode
= data_int
("mode") or else 0
299 current_level
= data_int
("level") or else 0
301 max_levels
[0] = data_int
("max_0") or else 0
302 max_levels
[1] = data_int
("max_1") or else 0
303 max_levels
[2] = data_int
("max_2") or else 0
305 print
"max_levels: {max_levels}"
307 reload
= new Button(-1, new Color(1.0,1.0,1.0), drawing
.reload
, snd_click
)
316 # Get a positive numeric value from the store
317 fun data_int
(name
: String): nullable Int
319 var x
= data_store
[name
]
320 if x
isa Int then return x
else return null
323 # # Level information
325 # Number of buttons for the next game
328 # Length of the memory sequence for the next game
332 var hard_deal
= false
334 # No shuffle (0), easy shuffle (1), or hard shuffle (2)?
337 # Is a new deal make on replay?
338 # If true, a new set of figures and a new sequence is produced
339 # If false, the same is reused.
340 var deal_on_replay
= true
342 # Current buttons in the game
343 var buttons
= new Array[Button]
345 # The sequence of the buttons to memorize
346 var level
= new Array[Button]
348 # The number of errors (crosses) in the current level. (in [0..3])
351 # Is the player playing?
352 # If false it means that the game is showing the sequence to memorize
355 # Next button on the level (to show or guess according to `player`)
358 # Time to live before the next event
361 # Are we in the menu?
364 # In the end of game, is this a win of a lose?
367 # Reset everything and create a menu
375 data_store
["game"] = false
382 for i
in [0..size
[ do
383 var b
= new Button(i
, colors
[i
], shapes
[i
], sounds
[i
])
385 b
.text
= drawing
.hard
[i
]
386 b
.text_color
= colors
[3+i
]
387 b
.text_max
= max_levels
[i
]
394 # The current mode: easy (0), medium (1), hard (2)
397 # The current level (from 0)
398 var current_level
= 0
400 # Hight scores of each mode
401 var max_levels
: Array[Int] = [0, 0, 0]
403 # Reset everything and create a new game using `mode` and `level`
406 print
"Next game: mode={mode} level={current_level}"
407 data_store
["game"] = true
408 data_store
["mode"] = mode
409 data_store
["level"] = current_level
410 if max_levels
[mode
] < current_level
then
411 max_levels
[mode
] = current_level
412 data_store
["max_{mode}"] = current_level
418 deal_on_replay
= false
421 else if mode
== 1 then
424 deal_on_replay
= true
430 deal_on_replay
= true
434 for i
in [0..current_level
[ do
436 if length
> size
+ 2 then
440 if size
> 16 then size
= 16
446 # Reset the buttons and deal a new game using `size` and `length`
458 if not hard_deal
then
459 # With the easy deal, each button is easily distinguishable
460 for i
in [0..size
[ do
461 var b
= new Button(i
, colors
[i
%colors
.length
], shapes
[i
%shapes
.length
], sounds
[i
%sounds
.length
])
465 # With the hard deal, use overlapping combinations of colors and shapes
466 var sqrt
= size
.to_f
.sqrt
467 var ncol
= sqrt
.floor
.to_i
468 var nsha
= sqrt
.ceil
.to_i
469 while ncol
*nsha
< size
do ncol
+= 1
471 # Randomly swap the numbers of colors/shapes
478 # Deal combinations (up to `size`)
479 for i
in [0..ncol
[ do
480 for j
in [0..nsha
[ do
481 if buttons
.length
>= size
then break
482 var b
= new Button(buttons
.length
, colors
[i
], shapes
[j
], sounds
.rand
)
487 # A last shuffle to break the colors/shapes grid
491 # Deal the level (i.e. sequence to memorize)
492 # To increase distribution, determinate a maximum number of repetition
494 var rep
= (length
.to_f
/ size
.to_f
).ceil
.to_i
495 var pool
= buttons
* rep
499 for i
in [0..length
[ do
503 print
"newgame size={size} length={length}"
509 # Cause a replay on the same level
510 # On easy mode, the same level is replayed exactly
511 # On other modes, a new deal is made
514 if deal_on_replay
then
521 # Reset the state of the scene and start with `fly_in`
533 # # Placement and moves
535 # Locations used to place buttons on the screen
536 private var locations
: Array[Array[Float]] = [
539 [0.0, 1.0, 0.0, 1.0],
540 [0.0, 1.0, 2.0, 0.5, 1.5],
541 [0.0, 1.0, 2.0, 0.0, 1.0, 2.0],
542 [0.5, 1.5, 0.0, 1.0, 2.0, 0.5, 1.5],
543 [0.0, 1.0, 2.0, 0.0, 2.0, 0.0, 1.0, 2.0],
544 [0.0, 1.0, 2.0, 0.0, 1.0, 2.0, 0.0, 1.0, 2.0],
545 [0.5, 1.5, 2.5, 0.0, 1.0, 2.0, 3.0, 0.5, 1.5, 2.5],
546 [0.0, 1.0, 2.0, 3.0, 0.0, 1.5, 3.0, 0.0, 1.0, 2.0, 3.0],
547 [0.0, 1.0, 2.0, 3.0, 0.0, 1.0, 2.0, 3.0, 0.0, 1.0, 2.0, 3.0],
548 [0.5, 1.5, 2.5, 0.0, 1.5, 3.0, 0.0, 1.0, 2.0, 3.0, 0.5, 1.5, 2.5],
549 [0.5, 1.5, 2.5, 0.0, 1.0, 2.0, 3.0, 0.0, 1.0, 2.0, 3.0, 0.5, 1.5, 2.5],
550 [0.5, 1.5, 2.5, 0.0, 1.0, 2.0, 3.0, 0.0, 1.0, 2.0, 3.0, 0.0, 1.0, 2.0, 3.0],
551 [0.0, 1.0, 2.0, 3.0, 0.0, 1.0, 2.0, 3.0, 0.0, 1.0, 2.0, 3.0, 0.0, 1.0, 2.0, 3.0]]
554 # The scale of the figures.
555 # According to the screen dimensions and the number of figures
558 # The scale of the UI
559 # According to the screen dimensions
562 # Compute then location on the display for each button
564 # The method can be called when there is a change in the buttons (or the display).
565 fun locate
(display
: Display)
567 # The locations depend of the number of buttons (from 2 to 9)
568 var n
= buttons
.length
569 var locs
= locations
[n-2
]
570 var columns
= if n
<= 4 then 2 else if n
<= 9 then 3 else 4
571 var rows
= if n
<= 2 then 1 else if n
<= 6 then 2 else if n
<= 12 then 3 else 4
573 # Compute basic dimensions according to the screen
574 var slotw
= display
.width
/ columns
575 var sloth
= display
.height
/ rows
576 var subw
= slotw
- slotw
/5
577 var subh
= sloth
- sloth
/5
579 # Compute the figure scale
580 var img
= drawing
.circle
581 var xs
= subw
.to_f
/ img
.width
.to_f
582 var ys
= subh
.to_f
/ img
.height
.to_f
585 # Compute the UI scale
586 xs
= display
.width
.to_f
/ img
.width
.to_f
587 ys
= display
.height
.to_f
/ img
.height
.to_f
588 ui_scale
= xs
.min
(ys
) / 4.0
601 b
.x
= (col
+ 0.5) * slotw
.to_f
602 b
.y
= (row
+ 0.5) * sloth
.to_f
605 b
.w
= (img
.width
.to_f
* scale
)
606 b
.h
= (img
.height
.to_f
* scale
)
611 left
.x
= -150.0 * scale
612 left
.y
= (display
.height
/ 2).to_f
613 right
.x
= display
.width
.to_f
+ 150.0 * scale
619 var reload
= self.reload
620 drawing
.reload
.scale
= ui_scale
621 reload
.x
= display
.width
.to_f
- (drawing
.reload
.width
.to_f
/ 2.0 * 1.2 ) * ui_scale
622 reload
.y
= drawing
.reload
.height
.to_f
/ 2.0 * 1.2 * ui_scale
623 reload
.w
= drawing
.reload
.width
.to_f
* ui_scale
624 reload
.h
= drawing
.reload
.height
.to_f
* ui_scale
628 # The origin point of the cursor on the left
629 var left
= new Pos(0.0, 0.0)
631 # The destination point of the cursor on the right
632 var right
= new Pos(0.0, 0.0)
634 # The current cursor position
635 var cursor
= new Pos(0.0, 0.0)
637 # The current cursor path
638 var path
: nullable BPath = null
641 var reload
: Button is noautoinit
643 # Safe point for a cursor on the i-th button of the level
644 fun path_pos
(i
: Int): Pos
646 if i
< 0 then return left
647 if i
>= level
.length
then return right
648 return level
[i
].to_pos
651 # A random point outside of the screen
652 fun far_away
(display
: Display): Pos
654 var a
= (2.0*pi
).rand
655 var w
= display
.width
.to_f
/ 2.0
656 var h
= display
.height
.to_f
/ 2.0
657 var x
= w
+ a
.cos
* w
* 1.8
658 var y
= h
+ a
.sin
* h
* 1.8
662 # Create a BPath between two point with some nice handle values
663 fun new_path
(from
, to
: Pos, ttl
: Float): BPath
665 var a
= atan2
(to
.y-from
.y
, to
.x-from
.x
)
666 a
+= pi
* (2.0.rand
- 1.0)
667 var radius
= 300.0 * scale
668 var fh
= new Pos(from
.x
+ a
.cos
*radius
, from
.y
+ a
.sin
*radius
)
669 #var th = new Pos(to.x - a.cos*radius, to.y - a.sin*radius)
670 var path
= new BPath(from
, fh
, to
, to
, ttl
)
674 # Initial placement of buttons
675 fun fly_in
(display
: Display)
678 var from
= far_away
(display
)
680 var path
= new_path
(from
, to
, 5.0)
687 # Final leaving of buttons
688 fun fly_out
(display
: Display)
692 var to
= far_away
(display
)
695 var path
= new_path
(from
, to
, 5.0)
703 # Randomly permute the content of `buttons` such that no element appears in its original position.
706 # The simplest algorithm is to shuffle until no buttons is at the same place
707 # This is also quite efficient and converges extremely quickly
712 for i
in [0..size
[ do
713 if i
== buttons
[i
].place
then
721 # Shuffling the place of each button on the screen
722 fun shuffle
(display
: Display)
734 #print "shuffle move {b.place}: {from} -> {to}"
735 b
.path
= new_path
(from
, to
, 5.0)
741 # Shuffle the place of each button in a hard way
742 fun hard_shuffle
(display
: Display)
755 var midx
= display
.width
.to_f
/ 2.0
756 var midy
= display
.height
.to_f
/ 2.0
757 var mid
= new Pos(midx
, midy
)
758 #print "shuffle move {b.place}: {from} -> {to}"
759 b
.path
= new_path
(from
, mid
, 5.0)
760 b
.path2
= new_path
(mid
, to
, 5.0)
766 # Setup the next cursor path
769 if is_menu
then return
770 var from
= path_pos
(cpt-1
)
771 var to
= path_pos
(cpt
)
772 #print "cursor {cpt-1}->{cpt}: {from} -> {to}"
773 path
= new_path
(from
, to
, 4.0)
779 # Main loop, drawing and inputs
781 # Flag used to ask for a (re-)computation of the display layout
782 private var first_frame
= true
784 redef fun frame_core
(display
)
795 display
.clear
(1.0, 1.0, 1.0)
798 # This is a crappy ad hoc organic implementation
801 if path
!= null then path
.update
(cursor
, ttl
)
805 # Menu animation is over
808 # Level place animation is over
811 else if cpt
< level
.length
then
812 # The cursor is playing
818 else if cpt
== level
.length
then
819 # The cursor is out, run the shuffle
821 if shuffling
== 1 then
823 else if shuffling
> 1 then
824 hard_shuffle
(display
)
828 # The shuffling is over, start playing
833 else if ttl
> 0.0 then
837 if cpt
== level
.length
then
853 # Display each button
860 drawing
.cursor
.scale
= scale
861 display
.blit
(drawing
.cursor
, cursor
.x
, cursor
.y
)
865 blit_number
(current_level
, ui_scale
, 10.0 * scale
, 10.0 * scale
)
866 reload
.blit_on
(display
)
870 # Blit a number somewhere
871 fun blit_number
(number
: Int, scale
: Float, x
, y
: Float)
873 for img
in number_images
.imgs
do img
.scale
= scale
874 display
.blit_number
(number_images
, number
, x
.to_i
, y
.to_i
)
877 # Images with the numbers
878 private var number_images
: NumberImages is noautoinit
880 # A player click on a button
881 fun action
(b
: Button)
892 if cpt
>= level
.length
then return
893 if b
== level
[cpt
] then
898 if cpt
>= level
.length
then
925 if ie
isa QuitEvent then
931 if ie
isa PointerEvent and ie
.depressed
then
934 if b
.has
(ie
.x
, ie
.y
) then
942 if reload
.has
(ie
.x
, ie
.y
) then
952 if ie
isa KeyEvent and ie
.is_down
then
955 if c
== "4" or c
== "escape" then
956 # 4 is *back* on android
958 # quit = true # broken
970 if c
== "[+]" or c
== "=" then
975 else if c
== "[-]" or c
== "-" then
979 else if c
== "[*]" or c
== "]" then
983 else if c
== "[/]" or c
== "[" then
987 else if c
== "space" or c
== "82" then
988 # 82 is *menu* on android
995 print
"got keydown: `{c}`"