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, 1, 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)
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
)
168 # red (from 0.0 to 1.0)
170 # green (from 0.0 to 1.0)
172 # blue (from 0.0 to 1.0)
175 # Globally change the color of the display.
176 # The color will be used for the next blit operations.
177 # The color of the display has to be reseted manually (see `Opengles1Display::reset_color`).
178 fun set
(display
: Display, p
: Float)
180 if display
isa Opengles1Display then
181 display
.color
(p
.lerp
(r
,1.0),p
.lerp
(g
,1.0),p
.lerp
(b
,1.0),p
.lerp
(1.0,0.0))
186 # A point in the display coordinates
192 redef fun to_s
do return "({x},{y})"
195 # A cubic Bézier path between two points with two handles.
199 # The handle of the origin point
201 # The handle of the destination point
203 # The destination point
205 # The duration on the path
208 # Update the coordinates of `cursor` for an absolute time to destination `ttd`
209 fun update
(cursor
: Pos, ttd
: Float)
211 var p
= 1.0 - ttd
/ duration
221 var bx
= p
.cerp
(from
.x
, from_handle
.x
, to_handle
.x
, to
.x
)
222 var by
= p
.cerp
(from
.y
, from_handle
.y
, to_handle
.y
, to
.y
)
230 # # Assets and resources
232 # All the images assets
233 var drawing
= new DrawingImages
235 # Array of all available colors for the figures
236 var colors
= new Array[Color]
238 # Array of all available shapes for the figures
239 var shapes
= new Array[Image]
241 # Array of all available sounds for the figures
242 var sounds
= new Array[Sound]
244 # The sound to play on error (error)
245 var snd_penalty
: Sound is noautoinit
247 # The sound of other ui element
248 var snd_click
: Sound is noautoinit
253 colors
.add
new Color(0.9, 0.6, 0.0)
254 colors
.add
new Color(0.6, 0.0, 0.9)
255 colors
.add
new Color(0.6, 0.5, 0.4)
256 colors
.add
new Color(1.0, 0.0, 0.0)
257 colors
.add
new Color(1.0, 1.0, 0.0)
258 colors
.add
new Color(1.0, 0.0, 1.0)
259 colors
.add
new Color(0.0, 1.0, 0.0)
260 colors
.add
new Color(0.0, 1.0, 1.0)
261 colors
.add
new Color(0.0, 0.0, 1.0)
263 drawing
.load_all
(self)
265 shapes
.add drawing
.circle
266 shapes
.add drawing
.rect
267 shapes
.add drawing
.cross
268 shapes
.add drawing
.penta
269 shapes
.add drawing
.star
270 shapes
.add drawing
.triangle
271 shapes
.add drawing
.heart
272 shapes
.add drawing
.diamond
273 shapes
.add drawing
.moon
274 shapes
.add drawing
.spiral
276 number_images
= new NumberImages(drawing
.n
)
279 sounds
.add
new Sound("bing.wav")
280 sounds
.add
new Sound("boing.wav")
281 sounds
.add
new Sound("cymbal.wav")
282 sounds
.add
new Sound("dart.wav")
283 sounds
.add
new Sound("duh.wav")
284 sounds
.add
new Sound("grunt.wav")
285 sounds
.add
new Sound("honkhonk.wav")
286 sounds
.add
new Sound("line_end.wav")
287 sounds
.add
new Sound("squishy-hit.wav")
288 sounds
.add
new Sound("woodthunk.wav")
289 sounds
.add
new Sound("whip.wav")
291 snd_penalty
= new Sound("penalty.wav")
292 snd_click
= new Sound("click.wav")
294 # Force load the sounds. Required because bug #1728
295 for s
in sounds
do s
.load
298 is_menu
= data_store
["game"] != true
299 mode
= data_int
("mode") or else 0
300 current_level
= data_int
("level") or else 0
302 max_levels
[0] = data_int
("max_0") or else 0
303 max_levels
[1] = data_int
("max_1") or else 0
304 max_levels
[2] = data_int
("max_2") or else 0
306 print
"max_levels: {max_levels}"
308 reload
= new Button(-1, new Color(1.0,1.0,1.0), drawing
.reload
, snd_click
)
317 # Get a positive numeric value from the store
318 fun data_int
(name
: String): nullable Int
320 var x
= data_store
[name
]
321 if x
isa Int then return x
else return null
324 # # Level information
326 # Number of buttons for the next game
329 # Length of the memory sequence for the next game
333 var hard_deal
= false
335 # No shuffle (0), easy shuffle (1), or hard shuffle (2)?
338 # Is a new deal make on replay?
339 # If true, a new set of figures and a new sequence is produced
340 # If false, the same is reused.
341 var deal_on_replay
= true
343 # Current buttons in the game
344 var buttons
= new Array[Button]
346 # The sequence of the buttons to memorize
347 var level
= new Array[Button]
349 # The number of errors (crosses) in the current level. (in [0..3])
352 # Is the player playing?
353 # If false it means that the game is showing the sequence to memorize
356 # Next button on the level (to show or guess according to `player`)
359 # Time to live before the next event
362 # Are we in the menu?
365 # In the end of game, is this a win of a lose?
368 # Reset everything and create a menu
376 data_store
["game"] = false
383 for i
in [0..size
[ do
384 var b
= new Button(i
, colors
[i
], shapes
[i
], sounds
[i
])
386 b
.text
= drawing
.hard
[i
]
387 b
.text_color
= colors
[3+i
]
388 b
.text_max
= max_levels
[i
]
395 # The current mode: easy (0), medium (1), hard (2)
398 # The current level (from 0)
399 var current_level
= 0
401 # Hight scores of each mode
402 var max_levels
: Array[Int] = [0, 0, 0]
404 # Reset everything and create a new game using `mode` and `level`
407 print
"Next game: mode={mode} level={current_level}"
408 data_store
["game"] = true
409 data_store
["mode"] = mode
410 data_store
["level"] = current_level
411 if max_levels
[mode
] < current_level
then
412 max_levels
[mode
] = current_level
413 data_store
["max_{mode}"] = current_level
419 deal_on_replay
= false
422 else if mode
== 1 then
425 deal_on_replay
= true
431 deal_on_replay
= true
435 for i
in [0..current_level
[ do
437 if length
> size
+ 2 then
441 if size
> 16 then size
= 16
447 # Reset the buttons and deal a new game using `size` and `length`
459 if not hard_deal
then
460 # With the easy deal, each button is easily distinguishable
461 for i
in [0..size
[ do
462 var b
= new Button(i
, colors
[i
%colors
.length
], shapes
[i
%shapes
.length
], sounds
[i
%sounds
.length
])
466 # With the hard deal, use overlapping combinations of colors and shapes
467 var sqrt
= size
.to_f
.sqrt
468 var ncol
= sqrt
.floor
.to_i
469 var nsha
= sqrt
.ceil
.to_i
470 while ncol
*nsha
< size
do ncol
+= 1
472 # Randomly swap the numbers of colors/shapes
479 # Deal combinations (up to `size`)
480 for i
in [0..ncol
[ do
481 for j
in [0..nsha
[ do
482 if buttons
.length
>= size
then break
483 var b
= new Button(buttons
.length
, colors
[i
], shapes
[j
], sounds
.rand
)
488 # A last shuffle to break the colors/shapes grid
492 # Deal the level (i.e. sequence to memorize)
493 # To increase distribution, determinate a maximum number of repetition
495 var rep
= (length
.to_f
/ size
.to_f
).ceil
.to_i
496 var pool
= buttons
* rep
500 for i
in [0..length
[ do
504 print
"newgame size={size} length={length}"
510 # Cause a replay on the same level
511 # On easy mode, the same level is replayed exactly
512 # On other modes, a new deal is made
515 if deal_on_replay
then
522 # Reset the state of the scene and start with `fly_in`
534 # # Placement and moves
536 # Locations used to place buttons on the screen
537 private var locations
: Array[Array[Float]] = [
540 [0.0, 1.0, 0.0, 1.0],
541 [0.0, 1.0, 2.0, 0.5, 1.5],
542 [0.0, 1.0, 2.0, 0.0, 1.0, 2.0],
543 [0.5, 1.5, 0.0, 1.0, 2.0, 0.5, 1.5],
544 [0.0, 1.0, 2.0, 0.0, 2.0, 0.0, 1.0, 2.0],
545 [0.0, 1.0, 2.0, 0.0, 1.0, 2.0, 0.0, 1.0, 2.0],
546 [0.5, 1.5, 2.5, 0.0, 1.0, 2.0, 3.0, 0.5, 1.5, 2.5],
547 [0.0, 1.0, 2.0, 3.0, 0.0, 1.5, 3.0, 0.0, 1.0, 2.0, 3.0],
548 [0.0, 1.0, 2.0, 3.0, 0.0, 1.0, 2.0, 3.0, 0.0, 1.0, 2.0, 3.0],
549 [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],
550 [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],
551 [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],
552 [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]]
555 # The scale of the figures.
556 # According to the screen dimensions and the number of figures
559 # The scale of the UI
560 # According to the screen dimensions
563 # Compute then location on the display for each button
565 # The method can be called when there is a change in the buttons (or the display).
566 fun locate
(display
: Display)
568 # The locations depend of the number of buttons (from 2 to 9)
569 var n
= buttons
.length
570 var locs
= locations
[n-2
]
571 var columns
= if n
<= 4 then 2 else if n
<= 9 then 3 else 4
572 var rows
= if n
<= 2 then 1 else if n
<= 6 then 2 else if n
<= 12 then 3 else 4
574 # Compute basic dimensions according to the screen
575 var slotw
= display
.width
/ columns
576 var sloth
= display
.height
/ rows
577 var subw
= slotw
- slotw
/5
578 var subh
= sloth
- sloth
/5
580 # Compute the figure scale
581 var img
= drawing
.circle
582 var xs
= subw
.to_f
/ img
.width
.to_f
583 var ys
= subh
.to_f
/ img
.height
.to_f
586 # Compute the UI scale
587 xs
= display
.width
.to_f
/ img
.width
.to_f
588 ys
= display
.height
.to_f
/ img
.height
.to_f
589 ui_scale
= xs
.min
(ys
) / 4.0
602 b
.x
= (col
+ 0.5) * slotw
.to_f
603 b
.y
= (row
+ 0.5) * sloth
.to_f
606 b
.w
= (img
.width
.to_f
* scale
)
607 b
.h
= (img
.height
.to_f
* scale
)
612 left
.x
= -150.0 * scale
613 left
.y
= (display
.height
/ 2).to_f
614 right
.x
= display
.width
.to_f
+ 150.0 * scale
620 var reload
= self.reload
621 drawing
.reload
.scale
= ui_scale
622 reload
.x
= display
.width
.to_f
- (drawing
.reload
.width
.to_f
/ 2.0 * 1.2 ) * ui_scale
623 reload
.y
= drawing
.reload
.height
.to_f
/ 2.0 * 1.2 * ui_scale
624 reload
.w
= drawing
.reload
.width
.to_f
* ui_scale
625 reload
.h
= drawing
.reload
.height
.to_f
* ui_scale
629 # The origin point of the cursor on the left
630 var left
= new Pos(0.0, 0.0)
632 # The destination point of the cursor on the right
633 var right
= new Pos(0.0, 0.0)
635 # The current cursor position
636 var cursor
= new Pos(0.0, 0.0)
638 # The current cursor path
639 var path
: nullable BPath = null
642 var reload
: Button is noautoinit
644 # Safe point for a cursor on the i-th button of the level
645 fun path_pos
(i
: Int): Pos
647 if i
< 0 then return left
648 if i
>= level
.length
then return right
649 return level
[i
].to_pos
652 # A random point outside of the screen
653 fun far_away
(display
: Display): Pos
655 var a
= (2.0*pi
).rand
656 var w
= display
.width
.to_f
/ 2.0
657 var h
= display
.height
.to_f
/ 2.0
658 var x
= w
+ a
.cos
* w
* 1.8
659 var y
= h
+ a
.sin
* h
* 1.8
663 # Create a BPath between two point with some nice handle values
664 fun new_path
(from
, to
: Pos, ttl
: Float): BPath
666 var a
= atan2
(to
.y-from
.y
, to
.x-from
.x
)
667 a
+= pi
* (2.0.rand
- 1.0)
668 var radius
= 300.0 * scale
669 var fh
= new Pos(from
.x
+ a
.cos
*radius
, from
.y
+ a
.sin
*radius
)
670 #var th = new Pos(to.x - a.cos*radius, to.y - a.sin*radius)
671 var path
= new BPath(from
, fh
, to
, to
, ttl
)
675 # Initial placement of buttons
676 fun fly_in
(display
: Display)
679 var from
= far_away
(display
)
681 var path
= new_path
(from
, to
, 5.0)
688 # Final leaving of buttons
689 fun fly_out
(display
: Display)
693 var to
= far_away
(display
)
696 var path
= new_path
(from
, to
, 5.0)
704 # Randomly permute the content of `buttons` such that no element appears in its original position.
707 # The simplest algorithm is to shuffle until no buttons is at the same place
708 # This is also quite efficient and converges extremely quickly
713 for i
in [0..size
[ do
714 if i
== buttons
[i
].place
then
722 # Shuffling the place of each button on the screen
723 fun shuffle
(display
: Display)
735 #print "shuffle move {b.place}: {from} -> {to}"
736 b
.path
= new_path
(from
, to
, 5.0)
742 # Shuffle the place of each button in a hard way
743 fun hard_shuffle
(display
: Display)
756 var midx
= display
.width
.to_f
/ 2.0
757 var midy
= display
.height
.to_f
/ 2.0
758 var mid
= new Pos(midx
, midy
)
759 #print "shuffle move {b.place}: {from} -> {to}"
760 b
.path
= new_path
(from
, mid
, 5.0)
761 b
.path2
= new_path
(mid
, to
, 5.0)
767 # Setup the next cursor path
770 if is_menu
then return
771 var from
= path_pos
(cpt-1
)
772 var to
= path_pos
(cpt
)
773 #print "cursor {cpt-1}->{cpt}: {from} -> {to}"
774 path
= new_path
(from
, to
, 4.0)
780 # Main loop, drawing and inputs
782 # Flag used to ask for a (re-)computation of the display layout
783 private var first_frame
= true
785 redef fun frame_core
(display
)
796 display
.clear
(1.0, 1.0, 1.0)
799 # This is a crappy ad hoc organic implementation
802 if path
!= null then path
.update
(cursor
, ttl
)
806 # Menu animation is over
809 # Level place animation is over
812 else if cpt
< level
.length
then
813 # The cursor is playing
819 else if cpt
== level
.length
then
820 # The cursor is out, run the shuffle
822 if shuffling
== 1 then
824 else if shuffling
> 1 then
825 hard_shuffle
(display
)
829 # The shuffling is over, start playing
834 else if ttl
> 0.0 then
838 if cpt
== level
.length
then
854 # Display each button
861 drawing
.cursor
.scale
= scale
862 display
.blit
(drawing
.cursor
, cursor
.x
, cursor
.y
)
866 blit_number
(current_level
, ui_scale
, 10.0 * scale
, 10.0 * scale
)
867 reload
.blit_on
(display
)
871 # Blit a number somewhere
872 fun blit_number
(number
: Int, scale
: Float, x
, y
: Float)
874 for img
in number_images
.imgs
do img
.scale
= scale
875 display
.blit_number
(number_images
, number
, x
.to_i
, y
.to_i
)
878 # Images with the numbers
879 private var number_images
: NumberImages is noautoinit
881 # A player click on a button
882 fun action
(b
: Button)
893 if cpt
>= level
.length
then return
894 if b
== level
[cpt
] then
899 if cpt
>= level
.length
then
926 if ie
isa QuitEvent then
932 if ie
isa PointerEvent and ie
.depressed
then
935 if b
.has
(ie
.x
, ie
.y
) then
943 if reload
.has
(ie
.x
, ie
.y
) then
953 if ie
isa KeyEvent and ie
.is_down
then
956 if c
== "4" or c
== "escape" then
957 # 4 is *back* on android
959 # quit = true # broken
971 if c
== "[+]" or c
== "=" then
976 else if c
== "[-]" or c
== "-" then
980 else if c
== "[*]" or c
== "]" then
984 else if c
== "[/]" or c
== "[" then
988 else if c
== "space" or c
== "82" then
989 # 82 is *menu* on android
996 print
"got keydown: `{c}`"