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_namespace
"org.nitlanguage.memory"
31 app_version
(0, 2, git_revision
)
36 import mnit
::opengles1
37 import app
::data_store
41 # A figure to click on
43 # The place, starting from 0.
44 # Will be used to derive the display place.
47 # The color of the figure
50 # The shape of the figure
53 # The sound of the figure
56 # x-coordinate on the display
58 # y-coordinate on the display
60 # width on the display
65 # Event time to live (from 1.0 downto 0.0)
68 # Is there a big error on the button?
71 # The initial position (according to shuffle)
72 var from
: Pos is noinit
74 # The current path if shuffling
75 var path
: nullable BPath = null
77 # The second path if hard shuffling
78 var path2
: nullable BPath = null
80 # Is there an hard shuffling?
83 # The optional text on the button (in the menu)
84 var text
: nullable Image = null
86 # The color of the text
87 var text_color
: nullable Color = null
89 # The high score on the menu button
93 fun blit_on
(display
: Display)
101 if path
!= null then ttl
= path
.duration
112 path
.update
(pos
, ttl
)
117 if path2
!= null then
128 if not app
.player
then
132 color
.set
(display
, p
)
133 display
.blit_centered
(shape
, x
, y
)
136 text
.scale
= shape
.scale
137 text_color
.set
(display
, p
)
138 display
.blit_centered
(text
, x
, y
- h
/8.0)
140 app
.blit_number
(text_max
, app
.scale
, x
, y
+ h
/8.0, true)
143 if display
isa Opengles1Display then
147 app
.drawing
.error
.scale
= app
.scale
148 display
.blit_centered
(app
.drawing
.error
, x
, y
)
153 return "{place},{color},{shape},{sound}"
157 fun has
(x
,y
: Float): Bool
159 return (self.x
- x
).abs
*2.0 <= w
and (self.y
- y
).abs
*2.0 <= h
162 # Return a new pos centered on the button
163 fun to_pos
: Pos do return new Pos(x
, y
)
166 # A flying number to display rank of clicked button
171 # The original position
174 # The color of the number
177 # Time to live (downto 0.0)
180 # Draw on the display
181 fun blit_on
(display
: Display)
185 if ttl
< 0.0 then return
187 var p
= 1.0 - (ttl
/ 5.0)
189 var y
= pos
.y
- (p
* 40.0) * app
.scale
191 color
.set
(display
, p
)
192 app
.blit_number
(value
, app
.scale
* 2.0 * (1.0 + p
), pos
.x
, y
, true)
193 if display
isa Opengles1Display then
201 # red (from 0.0 to 1.0)
203 # green (from 0.0 to 1.0)
205 # blue (from 0.0 to 1.0)
208 # Globally change the color of the display.
209 # The color will be used for the next blit operations.
210 # The color of the display has to be reseted manually (see `Opengles1Display::reset_color`).
211 fun set
(display
: Display, p
: Float)
213 if display
isa Opengles1Display then
214 display
.color
(p
.lerp
(r
,1.0),p
.lerp
(g
,1.0),p
.lerp
(b
,1.0),p
.lerp
(1.0,0.0))
219 # A point in the display coordinates
225 redef fun to_s
do return "({x},{y})"
228 # A cubic Bézier path between two points with two handles.
232 # The handle of the origin point
234 # The handle of the destination point
236 # The destination point
238 # The duration on the path
241 # Update the coordinates of `cursor` for an absolute time to destination `ttd`
242 fun update
(cursor
: Pos, ttd
: Float)
244 var p
= 1.0 - ttd
/ duration
254 var bx
= p
.cerp
(from
.x
, from_handle
.x
, to_handle
.x
, to
.x
)
255 var by
= p
.cerp
(from
.y
, from_handle
.y
, to_handle
.y
, to
.y
)
263 # # Assets and resources
265 # All the images assets
266 var drawing
= new DrawingImages
268 # Array of all available colors for the figures
269 var colors
= new Array[Color]
271 # Array of all available shapes for the figures
272 var shapes
= new Array[Image]
274 # Array of all available sounds for the figures
275 var sounds
= new Array[Sound]
277 # The sound to play on error (error)
278 var snd_penalty
: Sound is noautoinit
280 # The sound of other ui element
281 var snd_click
: Sound is noautoinit
286 colors
.add
new Color(0.9, 0.6, 0.0)
287 colors
.add
new Color(0.6, 0.0, 0.9)
288 colors
.add
new Color(0.6, 0.5, 0.4)
289 colors
.add
new Color(1.0, 0.0, 0.0)
290 colors
.add
new Color(1.0, 1.0, 0.0)
291 colors
.add
new Color(1.0, 0.0, 1.0)
292 colors
.add
new Color(0.0, 1.0, 0.0)
293 colors
.add
new Color(0.0, 1.0, 1.0)
294 colors
.add
new Color(0.0, 0.0, 1.0)
296 drawing
.load_all
(self)
298 shapes
.add drawing
.circle
299 shapes
.add drawing
.rect
300 shapes
.add drawing
.cross
301 shapes
.add drawing
.penta
302 shapes
.add drawing
.star
303 shapes
.add drawing
.triangle
304 shapes
.add drawing
.heart
305 shapes
.add drawing
.diamond
306 shapes
.add drawing
.moon
307 shapes
.add drawing
.spiral
309 number_images
= new NumberImages(drawing
.n
)
312 sounds
.add
new Sound("bing.wav")
313 sounds
.add
new Sound("boing.wav")
314 sounds
.add
new Sound("cymbal.wav")
315 sounds
.add
new Sound("dart.wav")
316 sounds
.add
new Sound("duh.wav")
317 sounds
.add
new Sound("grunt.wav")
318 sounds
.add
new Sound("honkhonk.wav")
319 sounds
.add
new Sound("line_end.wav")
320 sounds
.add
new Sound("squishy-hit.wav")
321 sounds
.add
new Sound("woodthunk.wav")
322 sounds
.add
new Sound("whip.wav")
324 snd_penalty
= new Sound("penalty.wav")
325 snd_click
= new Sound("click.wav")
327 # Force load the sounds. Required because bug #1728
328 for s
in sounds
do s
.load
331 is_menu
= data_store
["game"] != true
332 mode
= data_int
("mode") or else 0
333 current_level
= data_int
("level") or else 0
335 max_levels
[0] = data_int
("max_0") or else 0
336 max_levels
[1] = data_int
("max_1") or else 0
337 max_levels
[2] = data_int
("max_2") or else 0
339 print
"max_levels: {max_levels}"
341 reload
= new Button(-1, new Color(1.0,1.0,1.0), drawing
.reload
, snd_click
)
350 # Get a positive numeric value from the store
351 fun data_int
(name
: String): nullable Int
353 var x
= data_store
[name
]
354 if x
isa Int then return x
else return null
357 # # Level information
359 # Number of buttons for the next game
362 # Length of the memory sequence for the next game
366 var hard_deal
= false
368 # No shuffle (0), easy shuffle (1), or hard shuffle (2)?
371 # Is a new deal make on replay?
372 # If true, a new set of figures and a new sequence is produced
373 # If false, the same is reused.
374 var deal_on_replay
= true
376 # Current buttons in the game
377 var buttons
= new Array[Button]
379 # The sequence of the buttons to memorize
380 var level
= new Array[Button]
382 # The number of errors (crosses) in the current level. (in [0..3])
385 # Is the player playing?
386 # If false it means that the game is showing the sequence to memorize
389 # Next button on the level (to show or guess according to `player`)
392 # Time to live before the next event
395 # Are we in the menu?
398 # In the end of game, is this a win of a lose?
401 # Reset everything and create a menu
409 data_store
["game"] = false
416 for i
in [0..size
[ do
417 var b
= new Button(i
, colors
[i
], shapes
[i
], sounds
[i
])
419 b
.text
= drawing
.hard
[i
]
420 b
.text_color
= colors
[3+i
]
421 b
.text_max
= max_levels
[i
]
428 # The current mode: easy (0), medium (1), hard (2)
431 # The current level (from 0)
432 var current_level
= 0
434 # Hight scores of each mode
435 var max_levels
: Array[Int] = [0, 0, 0]
437 # Reset everything and create a new game using `mode` and `level`
440 print
"Next game: mode={mode} level={current_level}"
441 data_store
["game"] = true
442 data_store
["mode"] = mode
443 data_store
["level"] = current_level
444 if max_levels
[mode
] < current_level
then
445 max_levels
[mode
] = current_level
446 data_store
["max_{mode}"] = current_level
452 deal_on_replay
= false
455 else if mode
== 1 then
458 deal_on_replay
= true
464 deal_on_replay
= true
468 for i
in [0..current_level
[ do
470 if length
> size
+ 2 then
474 if size
> 16 then size
= 16
480 # Reset the buttons and deal a new game using `size` and `length`
492 if not hard_deal
then
493 # With the easy deal, each button is easily distinguishable
494 for i
in [0..size
[ do
495 var b
= new Button(i
, colors
[i
%colors
.length
], shapes
[i
%shapes
.length
], sounds
[i
%sounds
.length
])
499 # With the hard deal, use overlapping combinations of colors and shapes
500 var sqrt
= size
.to_f
.sqrt
501 var ncol
= sqrt
.floor
.to_i
502 var nsha
= sqrt
.ceil
.to_i
503 while ncol
*nsha
< size
do ncol
+= 1
505 # Randomly swap the numbers of colors/shapes
512 # Deal combinations (up to `size`)
513 for i
in [0..ncol
[ do
514 for j
in [0..nsha
[ do
515 if buttons
.length
>= size
then break
516 var b
= new Button(buttons
.length
, colors
[i
], shapes
[j
], sounds
.rand
)
521 # A last shuffle to break the colors/shapes grid
525 # Deal the level (i.e. sequence to memorize)
526 # To increase distribution, determinate a maximum number of repetition
528 var rep
= (length
.to_f
/ size
.to_f
).ceil
.to_i
529 var pool
= buttons
* rep
533 for i
in [0..length
[ do
537 print
"newgame size={size} length={length}"
543 # Cause a replay on the same level
544 # On easy mode, the same level is replayed exactly
545 # On other modes, a new deal is made
548 if deal_on_replay
then
555 # Reset the state of the scene and start with `fly_in`
568 # # Placement and moves
570 # Locations used to place buttons on the screen
571 private var locations
: Array[Array[Float]] = [
574 [0.0, 1.0, 0.0, 1.0],
575 [0.0, 1.0, 2.0, 0.5, 1.5],
576 [0.0, 1.0, 2.0, 0.0, 1.0, 2.0],
577 [0.5, 1.5, 0.0, 1.0, 2.0, 0.5, 1.5],
578 [0.0, 1.0, 2.0, 0.0, 2.0, 0.0, 1.0, 2.0],
579 [0.0, 1.0, 2.0, 0.0, 1.0, 2.0, 0.0, 1.0, 2.0],
580 [0.5, 1.5, 2.5, 0.0, 1.0, 2.0, 3.0, 0.5, 1.5, 2.5],
581 [0.0, 1.0, 2.0, 3.0, 0.0, 1.5, 3.0, 0.0, 1.0, 2.0, 3.0],
582 [0.0, 1.0, 2.0, 3.0, 0.0, 1.0, 2.0, 3.0, 0.0, 1.0, 2.0, 3.0],
583 [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],
584 [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],
585 [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],
586 [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]]
589 # The scale of the figures.
590 # According to the screen dimensions and the number of figures
593 # The scale of the UI
594 # According to the screen dimensions
597 # Compute then location on the display for each button
599 # The method can be called when there is a change in the buttons (or the display).
600 fun locate
(display
: Display)
602 # The locations depend of the number of buttons (from 2 to 9)
603 var n
= buttons
.length
604 var locs
= locations
[n-2
]
605 var columns
= if n
<= 4 then 2 else if n
<= 9 then 3 else 4
606 var rows
= if n
<= 2 then 1 else if n
<= 6 then 2 else if n
<= 12 then 3 else 4
608 # Compute basic dimensions according to the screen
609 var slotw
= display
.width
/ columns
610 var sloth
= display
.height
/ rows
611 var subw
= slotw
- slotw
/5
612 var subh
= sloth
- sloth
/5
614 # Compute the figure scale
615 var img
= drawing
.circle
616 var xs
= subw
.to_f
/ img
.width
.to_f
617 var ys
= subh
.to_f
/ img
.height
.to_f
620 # Compute the UI scale
621 xs
= display
.width
.to_f
/ img
.width
.to_f
622 ys
= display
.height
.to_f
/ img
.height
.to_f
623 ui_scale
= xs
.min
(ys
) / 4.0
636 b
.x
= (col
+ 0.5) * slotw
.to_f
637 b
.y
= (row
+ 0.5) * sloth
.to_f
640 b
.w
= (img
.width
.to_f
* scale
)
641 b
.h
= (img
.height
.to_f
* scale
)
646 left
.x
= -150.0 * scale
647 left
.y
= (display
.height
/ 2).to_f
648 right
.x
= display
.width
.to_f
+ 150.0 * scale
654 var reload
= self.reload
655 drawing
.reload
.scale
= ui_scale
656 reload
.x
= display
.width
.to_f
- (drawing
.reload
.width
.to_f
/ 2.0 * 1.2 ) * ui_scale
657 reload
.y
= drawing
.reload
.height
.to_f
/ 2.0 * 1.2 * ui_scale
658 reload
.w
= drawing
.reload
.width
.to_f
* ui_scale
659 reload
.h
= drawing
.reload
.height
.to_f
* ui_scale
663 # The origin point of the cursor on the left
664 var left
= new Pos(0.0, 0.0)
666 # The destination point of the cursor on the right
667 var right
= new Pos(0.0, 0.0)
669 # The current cursor position
670 var cursor
= new Pos(0.0, 0.0)
672 # The current cursor path
673 var path
: nullable BPath = null
675 # The current flying numbers
676 var numbers
= new Array[Number]
679 var reload
: Button is noautoinit
681 # Safe point for a cursor on the i-th button of the level
682 fun path_pos
(i
: Int): Pos
684 if i
< 0 then return left
685 if i
>= level
.length
then return right
686 return level
[i
].to_pos
689 # A random point outside of the screen
690 fun far_away
(display
: Display): Pos
692 var a
= (2.0*pi
).rand
693 var w
= display
.width
.to_f
/ 2.0
694 var h
= display
.height
.to_f
/ 2.0
695 var x
= w
+ a
.cos
* w
* 1.8
696 var y
= h
+ a
.sin
* h
* 1.8
700 # Create a BPath between two point with some nice handle values
701 fun new_path
(from
, to
: Pos, ttl
: Float): BPath
703 var a
= atan2
(to
.y-from
.y
, to
.x-from
.x
)
704 a
+= pi
* (2.0.rand
- 1.0)
705 var radius
= 300.0 * scale
706 var fh
= new Pos(from
.x
+ a
.cos
*radius
, from
.y
+ a
.sin
*radius
)
707 #var th = new Pos(to.x - a.cos*radius, to.y - a.sin*radius)
708 var path
= new BPath(from
, fh
, to
, to
, ttl
)
712 # Initial placement of buttons
713 fun fly_in
(display
: Display)
716 var from
= far_away
(display
)
718 var path
= new_path
(from
, to
, 5.0)
725 # Final leaving of buttons
726 fun fly_out
(display
: Display)
730 var to
= far_away
(display
)
733 var path
= new_path
(from
, to
, 5.0)
741 # Randomly permute the content of `buttons` such that no element appears in its original position.
744 # The simplest algorithm is to shuffle until no buttons is at the same place
745 # This is also quite efficient and converges extremely quickly
750 for i
in [0..size
[ do
751 if i
== buttons
[i
].place
then
759 # Shuffling the place of each button on the screen
760 fun shuffle
(display
: Display)
772 #print "shuffle move {b.place}: {from} -> {to}"
773 b
.path
= new_path
(from
, to
, 5.0)
779 # Shuffle the place of each button in a hard way
780 fun hard_shuffle
(display
: Display)
793 var midx
= display
.width
.to_f
/ 2.0
794 var midy
= display
.height
.to_f
/ 2.0
795 var mid
= new Pos(midx
, midy
)
796 #print "shuffle move {b.place}: {from} -> {to}"
797 b
.path
= new_path
(from
, mid
, 5.0)
798 b
.path2
= new_path
(mid
, to
, 5.0)
804 # Setup the next cursor path
807 if is_menu
then return
808 var from
= path_pos
(cpt-1
)
809 var to
= path_pos
(cpt
)
810 #print "cursor {cpt-1}->{cpt}: {from} -> {to}"
811 path
= new_path
(from
, to
, 4.0)
817 # Main loop, drawing and inputs
819 # Flag used to ask for a (re-)computation of the display layout
820 private var first_frame
= true
822 redef fun frame_core
(display
)
833 display
.clear
(1.0, 1.0, 1.0)
836 # This is a crappy ad hoc organic implementation
839 if path
!= null then path
.update
(cursor
, ttl
)
843 # Menu animation is over
846 # Level place animation is over
849 else if cpt
< level
.length
then
850 # The cursor is playing
854 numbers
.add
new Number(cpt
+1, b
.to_pos
, b
.color
)
857 else if cpt
== level
.length
then
858 # The cursor is out, run the shuffle
860 if shuffling
== 1 then
862 else if shuffling
> 1 then
863 hard_shuffle
(display
)
867 # The shuffling is over, start playing
872 else if ttl
> 0.0 then
876 if cpt
== level
.length
then
892 # Display each button
897 # Display flying numbers
904 drawing
.cursor
.scale
= scale
905 display
.blit
(drawing
.cursor
, cursor
.x
, cursor
.y
)
909 blit_number
(current_level
, ui_scale
, 10.0 * scale
, 10.0 * scale
)
910 reload
.blit_on
(display
)
914 # Blit a number somewhere
915 fun blit_number
(number
: Int, scale
: Float, x
, y
: Float, centered
: nullable Bool)
917 for img
in number_images
.imgs
do img
.scale
= scale
918 display
.blit_number
(number_images
, number
, x
.to_i
, y
.to_i
, centered
)
921 # Images with the numbers
922 private var number_images
: NumberImages is noautoinit
924 # A player click on a button
925 fun action
(b
: Button)
936 if cpt
>= level
.length
then return
937 if b
== level
[cpt
] then
940 numbers
.add
new Number(cpt
+1, b
.to_pos
, b
.color
)
943 if cpt
>= level
.length
then
970 if ie
isa QuitEvent then
976 if ie
isa PointerEvent and ie
.depressed
then
979 if b
.has
(ie
.x
, ie
.y
) then
987 if reload
.has
(ie
.x
, ie
.y
) then
997 if ie
isa KeyEvent and ie
.is_down
then
1000 if c
== "4" or c
== "escape" then
1001 # 4 is *back* on android
1003 # quit = true # broken
1015 if c
== "[+]" or c
== "=" then
1020 else if c
== "[-]" or c
== "-" then
1024 else if c
== "[*]" or c
== "]" then
1028 else if c
== "[/]" or c
== "[" then
1032 else if c
== "space" or c
== "82" then
1033 # 82 is *menu* on android
1040 print
"got keydown: `{c}`"