Merge: friendz, a new game for contrib
authorJean Privat <jean@pryen.org>
Tue, 22 Jul 2014 02:56:53 +0000 (22:56 -0400)
committerJean Privat <jean@pryen.org>
Tue, 22 Jul 2014 02:56:53 +0000 (22:56 -0400)
Pull-Request: #561
Reviewed-by: Alexis Laferrière <alexis.laf@xymus.net>
Reviewed-by: Alexandre Terrasa <alexandre@moz-code.org>
Reviewed-by: Lucas Bajolet <r4pass@hotmail.com>

23 files changed:
contrib/friendz/Makefile [new file with mode: 0644]
contrib/friendz/README.md [new file with mode: 0644]
contrib/friendz/assets/background.png [new file with mode: 0644]
contrib/friendz/assets/bing.wav [new file with mode: 0644]
contrib/friendz/assets/click.wav [new file with mode: 0644]
contrib/friendz/assets/deltaforce_font.png [new file with mode: 0644]
contrib/friendz/assets/duh.wav [new file with mode: 0644]
contrib/friendz/assets/hitbox.png [new file with mode: 0644]
contrib/friendz/assets/level.wav [new file with mode: 0644]
contrib/friendz/assets/logo.png [new file with mode: 0644]
contrib/friendz/assets/music.ogg [new file with mode: 0644]
contrib/friendz/assets/tiles2.png [new file with mode: 0644]
contrib/friendz/assets/whip.wav [new file with mode: 0644]
contrib/friendz/org.nitlanguage.friendz_android.txt [new file with mode: 0644]
contrib/friendz/src/friendz.nit [new file with mode: 0644]
contrib/friendz/src/friendz_android.nit [new file with mode: 0644]
contrib/friendz/src/friendz_linux.nit [new file with mode: 0644]
contrib/friendz/src/grid.nit [new file with mode: 0644]
contrib/friendz/src/level.nit [new file with mode: 0644]
contrib/friendz/src/solver.nit [new file with mode: 0644]
tests/exec.skip
tests/sav/friendz.res [new file with mode: 0644]
tests/sav/friendz_android.res [new file with mode: 0644]

diff --git a/contrib/friendz/Makefile b/contrib/friendz/Makefile
new file mode 100644 (file)
index 0000000..eb9ce29
--- /dev/null
@@ -0,0 +1,16 @@
+default: linux
+
+linux:
+       mkdir -p bin
+       ../../bin/nitg -o bin/friendz src/friendz_linux.nit
+
+android:
+       mkdir -p bin
+       ../../bin/nitg -o bin/friendz.apk src/friendz_android.nit
+
+doc:
+       mkdir -p doc
+       ../../bin/nitdoc -d doc/ src/friendz.nit src/friendz_linux.nit
+
+clean:
+       rm -rf bin/ doc/
diff --git a/contrib/friendz/README.md b/contrib/friendz/README.md
new file mode 100644 (file)
index 0000000..bd5f5e5
--- /dev/null
@@ -0,0 +1,35 @@
+Chainz of Friendz
+
+A puzzle game
+
+# Objectives
+
+Place monsters to create big chains of friends. You cannot add or
+remove monsters on a metal block. Monsters cannot have more that two
+direct friends. You must build one chain for each type of monster.
+
+# Controls
+
+Select a monster in the right pane and click on the board to add or
+remove the monster. Keep the mouse button down on the map to place
+multiple monsters. Click on a monster on a metal block to automatically
+select the monster.
+
+Keyboard shortcuts:
+
+* 1 to 9 : select a monster
+* 0 : eraser
+* q : metal block (editor only)
+
+# Editor
+
+Once the editor is unlocked you can create your own levels.
+
+# Challenge levels
+
+In challenge level you have to place as much as monster as possible.
+
+# Misc
+
+This game was originally developed for the [Casual Gameplay Design Competition
+\#9](http://jayisgames.com/cgdc9).
diff --git a/contrib/friendz/assets/background.png b/contrib/friendz/assets/background.png
new file mode 100644 (file)
index 0000000..a21aab4
Binary files /dev/null and b/contrib/friendz/assets/background.png differ
diff --git a/contrib/friendz/assets/bing.wav b/contrib/friendz/assets/bing.wav
new file mode 100644 (file)
index 0000000..c7cae5b
Binary files /dev/null and b/contrib/friendz/assets/bing.wav differ
diff --git a/contrib/friendz/assets/click.wav b/contrib/friendz/assets/click.wav
new file mode 100644 (file)
index 0000000..3d07b83
Binary files /dev/null and b/contrib/friendz/assets/click.wav differ
diff --git a/contrib/friendz/assets/deltaforce_font.png b/contrib/friendz/assets/deltaforce_font.png
new file mode 100644 (file)
index 0000000..fb6cc4b
Binary files /dev/null and b/contrib/friendz/assets/deltaforce_font.png differ
diff --git a/contrib/friendz/assets/duh.wav b/contrib/friendz/assets/duh.wav
new file mode 100644 (file)
index 0000000..054e1fe
Binary files /dev/null and b/contrib/friendz/assets/duh.wav differ
diff --git a/contrib/friendz/assets/hitbox.png b/contrib/friendz/assets/hitbox.png
new file mode 100644 (file)
index 0000000..bb48006
Binary files /dev/null and b/contrib/friendz/assets/hitbox.png differ
diff --git a/contrib/friendz/assets/level.wav b/contrib/friendz/assets/level.wav
new file mode 100644 (file)
index 0000000..2d30b6e
Binary files /dev/null and b/contrib/friendz/assets/level.wav differ
diff --git a/contrib/friendz/assets/logo.png b/contrib/friendz/assets/logo.png
new file mode 100644 (file)
index 0000000..6370e2a
Binary files /dev/null and b/contrib/friendz/assets/logo.png differ
diff --git a/contrib/friendz/assets/music.ogg b/contrib/friendz/assets/music.ogg
new file mode 100644 (file)
index 0000000..8dcfa3b
Binary files /dev/null and b/contrib/friendz/assets/music.ogg differ
diff --git a/contrib/friendz/assets/tiles2.png b/contrib/friendz/assets/tiles2.png
new file mode 100644 (file)
index 0000000..24038a9
Binary files /dev/null and b/contrib/friendz/assets/tiles2.png differ
diff --git a/contrib/friendz/assets/whip.wav b/contrib/friendz/assets/whip.wav
new file mode 100644 (file)
index 0000000..b907832
Binary files /dev/null and b/contrib/friendz/assets/whip.wav differ
diff --git a/contrib/friendz/org.nitlanguage.friendz_android.txt b/contrib/friendz/org.nitlanguage.friendz_android.txt
new file mode 100644 (file)
index 0000000..fc0e9dc
--- /dev/null
@@ -0,0 +1,10 @@
+Categories:Nit,Games
+License:WTFPL
+Web Site:http://nitlanguage.org
+Source Code:http://nitlanguage.org/nit.git/tree/HEAD:/contrib/friendz
+Issue Tracker:https://github.com/privat/nit/issues
+
+Summary:Puzzle game
+Description:
+Connect colored monsters to from bit continuous chains of friends.
+.
diff --git a/contrib/friendz/src/friendz.nit b/contrib/friendz/src/friendz.nit
new file mode 100644 (file)
index 0000000..e745040
--- /dev/null
@@ -0,0 +1,1642 @@
+# Monsterz - Chains of Friends
+#
+# 2010-2014 (c) Jean Privat <jean@pryen.org>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the Do What The Fuck You Want To
+# Public License, Version 2, as published by Sam Hocevar. See
+# http://sam.zoy.org/projects/COPYING.WTFPL for more details.
+
+# Full UI for the game
+module friendz is
+       app_name("ChainZ of FriendZ")
+       app_version(0, 1, git_revision)
+end
+
+import mnit
+import realtime
+import solver
+import mnit::tileset
+
+intrude import grid
+intrude import level
+
+redef class Grid
+       # Zoom level in %
+       # higher means more dense grid
+       var ratio = 100
+
+       # Various grid sizes from large to small (16x16, 12x12, 8x8, 6x6)
+       var ratios: Array[Int] = [200, 150, 100, 75]
+
+       redef fun resize(w,h)
+       do
+               super
+               for r in ratios do
+                       if w*100/r <= 8 and h*100/r <= 8 then self.ratio = r
+               end
+       end
+end
+
+#   * ENTITIES ****************************************************************
+
+# A game entity is something that is displayed and may interact with the player.
+abstract class Entity
+       # The associated game
+       var game: Game
+
+       # X left coordinate (in pixel).
+       var x: Int
+
+       # Y top coordinate (in pixel).
+       var y: Int
+
+       # X right coordinate (in pixel).
+       fun x2: Int do return x + width
+
+       # Y bottom coordinate (in pixel).
+       fun y2: Int do return y + height
+
+       # Width
+       var width: Int
+
+       # Height
+       var height: Int
+
+       # Tool tip text (if any)
+       var over: nullable String = null
+
+       # can the entity intercepts drag ang drop events?
+       var draggable = false
+
+       # Draw function. To implement
+       fun draw(ctx: Display) do end
+
+       # Update function. Called each loop. To implement
+       fun update do end
+
+       # Enter function. Called when the cursor enter in the element. To implement
+       fun enter(ev: Event) do end
+
+       # Click function. Called when the player click in the element.
+       # (or activate it with a shortcut).
+       fun click(ev: Event) do end
+
+       # keyboard shortcut do activate the entity, if any
+       var shortcut: nullable String = null
+
+       # Are events received?
+       var enabled = true
+
+       fun bw: Int do return game.bw
+       fun bh: Int do return game.bh
+
+       # Should the entity be redrawn
+       var dirty = true
+end
+
+# TEXT BUTTONS ***********************************************************/
+
+# Button entity displayed as a simple text.
+# if `over1` is null, then the button is a simple pasive label
+# if `over1` is set but `over2` is null, then the button is a normal button
+# if both `over1` and `over2` arew set, then the button is a toggleable button with two states
+class TextButton
+       super Entity
+       var str: String
+       init(game: Game, str: String, x,y: Int, color: nullable String, over, over2: nullable String)
+       do
+               var w = 10 # TODO
+               super(game, x,y,w,24)
+               self.str = str
+               self.color = color or else "purple"
+               self.over = over
+               self.over1 = over
+               self.over2 = over2
+               self.textx = x
+               if self.toggleable then
+                       self.x -= bw/2 + 4
+               end
+       end
+
+       var color: String
+
+       # The description of the button action
+       var over1: nullable String
+       # The description of the state2 button action
+       var over2: nullable String
+
+       # is the button a two-state button
+       fun toggleable: Bool do return over2 != null
+
+       # is the toggleable button in its second state?
+       var toggled = false
+
+       # ttl for highlighting
+       var ttl = 0
+
+       # position of the start of the text
+       # in a toggleable button, there is space for the mark between `x` and `textx`
+       var textx: Int
+
+       redef fun draw(ctx) do
+               var x = self.x
+               if self.toggleable then
+                       var w
+                       if self.toggled or not self.enabled then w = 6 else w = 7
+                       ctx.blit(game.img2[w,0], self.x, self.y)
+               end
+               var c
+               if self.enabled then c = self.color else c = "gray"
+               var c2= null
+               if self.ttl > 0 then c2 = "rgba(255,255,255,{self.ttl/10})"
+               ctx.textx(self.str, self.textx, self.y, self.height, c, c2)
+               self.width = ctx.measureText(self.str, self.height)
+               if self.toggleable then self.width += bw/2 + 4
+       end
+
+       redef fun update
+       do
+               if game.statusbar.over_what != self and self.ttl > 0 then
+                       self.ttl-=1
+                       self.dirty = true
+               end
+       end
+
+       redef fun enter(ev)
+       do
+               if over1 == null then return
+               if not self.enabled then return
+               game.snd_click.replay
+               self.ttl = 10
+               self.dirty = true
+               self.enter2
+       end
+
+       # Called by `enter` do perform additionnal work if the button is active
+       # Specific button should implement this instead of `enter`
+       fun enter2 do end
+
+       redef fun click(ev)
+       do
+               if not self.enabled then
+                       game.snd_bing.replay
+               else
+                       if self.toggleable then
+                               self.toggled = not self.toggled
+                               if self.toggled then self.over = self.over2 else self.over = self.over1
+                               game.statusbar.over_txt = self.over
+                       end
+                       game.snd_whip.replay
+               end
+               self.click2(ev)
+       end
+
+       # Called by `click` do perform additionnal work if the button is active
+       # Specific button should implement this instead of `click`
+       fun click2(ev: Event) do end
+
+end
+
+# LEVEL BUTTONS ***********************************************************/
+
+# button to play a level in the menu screen
+class LevelButton
+       super Entity
+
+       # The associated level to play
+       var level: Level
+
+       init(l: Level)
+       do
+               self.level = l
+               var i = l.number
+               super(l.game, (i%5)*56 + 54, (i/5)*56 + 55, l.game.bw, l.game.bh)
+
+               self.over = self.level.fullname
+               if self.level.get_state >= l.l_won then
+                       if game.levels[9].get_state >= l.l_won then self.over += " --- {self.level.score}/{self.level.par}"
+               else if self.level.get_state >= l.l_open then
+                       if game.levels[9].get_state >= l.l_open then self.over +=  " --- ?/{self.level.par}"
+               end
+               #self.enabled = l.get_state >= l.l_open
+       end
+
+       redef fun draw(ctx)
+       do
+               var l = level
+               var s = self.level.get_state
+               var ix = 5 + l.number % 2
+               var iy = 0
+               if s == l.l_disabled then
+                       ix = 3
+                       iy = 3
+               else if s == l.l_open then
+                       ix = 1
+                       iy = 1
+                       ctx.blit(game.img[ix,iy], self.x, self.y)
+                       ix = 0
+                       iy = 0
+               end
+               ctx.blit(game.img[ix,iy], self.x, self.y)
+
+               if s == l.l_par then
+                       ctx.blit(game.img2[7,0], self.x + bw*5/8, self.y-bh*1/8)
+               end
+               ctx.textx(self.level.name, self.x+5, self.y+5, 24, null, null)
+       end
+
+       redef fun click(ev)
+       do
+               if self.enabled then
+                       game.snd_whip.replay
+                       game.play(self.level)
+               else
+                       game.snd_bing.replay
+                       game.statusbar.set_tmp("Locked level", "red")
+               end
+       end
+
+end
+
+# ACHIEVEMENTS ************************************************************/
+
+# Achievement (monster-like) button in the menu screen
+class Achievement
+       super Button
+
+       # The number of the achievement (0 is first)
+       var number: Int
+
+       # The name of the achievement
+       var name: String
+
+       init(game: Game, i: Int, name: String)
+       do
+               super(game, 5*56 + 54, i*56 + 55, game.bw, game.bh)
+               self.over = name
+               self.number = i
+               self.name = name
+               var l =  game.levels[number*5+4]
+               enabled = l.get_state >= l.l_won
+               if self.enabled then self.over = name + " (unlocked)" else self.over = name + " (locked)"
+       end
+
+       redef fun draw(ctx)
+       do
+               var w
+               if self.enabled then w = 5 else w = 3
+               ctx.blit(game.img[w,self.number+5], self.x, self.y)
+       end
+
+       redef fun click(ev)
+       do
+               if not self.enabled then
+                       game.snd_bing.replay
+                       game.statusbar.set_tmp("Locked achievement!", "red")
+               else
+                       game.snd_whip.replay
+                       self.click2(ev)
+               end
+       end
+
+       fun click2(ev: Event) do
+               # TODO
+       end
+end
+
+
+# BOARD (THE GRID) *******************************************************/
+
+# The board game element.
+class Board
+       super Entity
+       init(game: Game)
+       do
+               super(game, game.xpad, game.ypad, 8*game.bw, 8*game.bh)
+               draggable = true
+       end
+
+       redef fun draw(ctx)
+       do
+               var grid = game.grid
+               var bwr = bw*100/grid.ratio
+               var bhr = bh*100/grid.ratio
+               var w = grid.width
+               var h = grid.height
+               if game.selected_button == game.button_size then
+                       bwr = bw/2
+                       bhr = bh/2
+                       w = game.gw
+                       h = game.gh
+               end
+               self.x = game.xpad+(48*8/2)-w*bwr/2
+               self.y = game.ypad+(48*8/2)-h*bhr/2
+               self.width = w*bwr
+               self.height = h*bhr
+               for i in [0..w[ do
+                       for j in [0..h[ do
+                               var t = grid.grid[i][j]
+                               var dx = i * bwr + self.x
+                               var dy = j * bhr + self.y
+                               if (i+j)%2 == 0 then
+                                       ctx.blit_scaled(game.img[5,0], dx, dy, bwr, bhr)
+                               else
+                                       ctx.blit_scaled(game.img[6,0], dx, dy, bwr, bhr)
+                               end
+                               if t.fixed then
+                                       if t.shape != null and not game.editing then
+                                               #ctx.drawImage(game.img, t.shape.x*bw, (2+t.shape.y)*bh, bw, bh, i * bwr + self.x, j * bhr + self.y, bwr, bhr)
+                                               ctx.blit_scaled(game.img[3,3], dx, dy, bwr, bhr)
+                                       else
+                                               ctx.blit_scaled(game.img[3,3], dx, dy, bwr, bhr)
+                                       end
+                               end
+                               if t.kind>0 then
+                                       var m = grid.monsters[t.kind]
+                                       var s = 0
+                                       if t.blink > 0 then s = 1
+                                       if t.nexts > 2 then s = 3
+                                       if t.nexts == 0 then s = 6
+                                       if m.chain then s = 5
+                                       if t.shocked>0 then s = 2
+                                       ctx.blit_scaled(game.img[s,(4+t.kind)], dx, dy, bwr, bhr)
+                               end
+                               #ctx.textx(t.chain_mark.to_s, dx, dy, 20, "", null)
+                       end
+               end
+               if game.selected_button == game.button_size then
+                       var x0 = self.x
+                       var x1 = (grid.width) * bwr - bwr/2 + self.x
+                       var y0 = self.y
+                       var y1 = (grid.height) * bhr - bhr/2 + self.y
+                       ctx.blit_scaled(game.img2[0,0], x0, y0, bwr/2, bhr/2)
+                       ctx.blit_scaled(game.img2[1,0], x1, y0, bwr/2, bhr/2)
+                       ctx.blit_scaled(game.img2[1,1], x1, y1, bwr/2, bhr/2)
+                       ctx.blit_scaled(game.img2[0,1], x0, y1, bwr/2, bhr/2)
+                       ctx.textx("{grid.width}x{grid.height}",self.x + grid.width*bwr/2,self.y+grid.height*bhr/2,20,"orange",null)
+               end
+       end
+
+       redef fun update
+       do
+               var grid = game.grid
+               for i in [0..grid.width[ do
+                       for j in [0..grid.height[ do
+                               var t = grid.grid[i][j]
+                               if t.kind == 0 then continue
+                               if t.blink > 0 then
+                                       t.blink-=1
+                                       self.dirty=true
+                               end
+                               if t.shocked > 0 then
+                                       t.shocked-=1
+                                       self.dirty=true
+                               else if 100.rand == 0 then
+                                       t.blink = 5
+                                       self.dirty=true
+                               end
+                       end
+               end
+       end
+
+       # Last clicked tile
+       # Uded to filter drag events
+       private var last: nullable Tile = null
+
+       redef fun click(ev)
+       do
+               var grid = game.grid
+               var r = grid.ratio
+               if game.selected_button == game.button_size then r = 200
+               var x = ev.game_x * r / bw / 100
+               var y = ev.game_y * r / bh / 100
+               var t = grid.grid[x][y]
+
+               if ev.drag and last == t then return
+               last = t
+
+               if game.selected_button != game.button_size and (x>=grid.width or y>=grid.height) then return
+
+               # print "{ev.game_x},{ev.game_y},{ev.drag} -> {x},{y}:{t}"
+
+               if game.selected_button != null then
+                       game.selected_button.click_board(ev, t)
+               end
+       end
+end
+
+# BUTTONS *****************************************************************/
+
+# A in-game selectable button for monsters or tools
+class Button
+       super Entity
+
+       # The x tile
+       var imgx: Int = 0
+
+       # The y tile
+       var imgy: Int = 0
+
+       # The associated monster tile
+       # >0 for monsters, <=0 for tools
+       var kind = 0
+
+       redef fun draw(ctx)
+       do
+               ctx.blit(game.img[self.imgx, self.imgy], self.x, self.y)
+               if game.selected_button == self then ctx.blit(game.img[0, 0], self.x, self.y)
+       end
+
+       redef fun click(ev)
+       do
+               var sel = game.selected_button
+               if game.selected_button == game.button_size then game.board.dirty=true
+               if sel != null then sel.dirty=true
+               game.selected_button = self
+               game.snd_click.replay
+       end
+
+       # Current inputed chain
+       # Used for drag
+       private var chain = new Array[Tile]
+
+       # Board click. Called when the player click on the board with the button selected.
+       fun click_board(ev: Event, t: Tile)
+       do
+               game.score.dirty = true
+               if ev.drag and self.kind>0 and not chain.is_empty then
+                       if self.chain.length >= 2 and self.chain[1] == t then
+                               var t2 = self.chain.shift
+                               game.snd_click.replay
+                               if t2.fixed and not game.editing then return
+                               t2.update(0)
+                               return
+                       end
+                       if t.fixed and t.kind == self.kind then
+                               self.chain.unshift(t)
+                               game.snd_click.replay
+                               return
+                       end
+                       if (self.chain[0].x - t.x).abs + (self.chain[0].y - t.y).abs != 1 then return
+                       if t.fixed and not game.editing then
+                               game.snd_bing.replay
+                               return
+                       end
+                       if t.kind != 0 and t.kind != self.kind then
+                               t.shocked = 5
+                               game.snd_duh.replay
+                               return
+                       end
+                       self.chain.unshift(t)
+                       if t.kind == self.kind then return
+                       game.snd_click.replay
+                       t.update(self.kind)
+                       return
+               end
+
+               if t.fixed and not game.editing then
+                       if t.kind == 0 then
+                               game.snd_bing.replay
+                               return
+                       end
+                       if t.kind != self.kind and not ev.drag then
+                               game.buttons[t.kind].click(ev)
+                               game.buttons[t.kind].chain = [t]
+                       else
+                               self.chain = [t]
+                               game.snd_bing.replay
+                       end
+                       return
+               end
+               if t.fixed and game.editing and self == game.button_erase and t.kind == 0 then
+                       t.fixed = false
+                       game.snd_click.replay
+                       return
+               end
+
+               var nkind = 0 # the new kind
+               if ev.drag then
+                       # Here we clean
+                       if t.kind == 0 then return
+                       if self.kind != 0 and t.kind != self.kind then
+                               t.shocked = 5
+                               game.snd_duh.replay
+                               return
+                       end
+                       nkind = 0
+               else if t.kind != self.kind then
+                       nkind = self.kind
+                       self.chain = [t]
+               else if t.kind != 0 then
+                       nkind = 0
+                       self.chain.clear
+               end
+               if nkind == t.kind then return
+               game.snd_click.replay
+               t.update(nkind)
+       end
+end
+
+# A monster button
+class MonsterButton
+       super Button
+
+       # TTL for the monster being angry
+       var angries = 0
+       # TTL for the monster being happy
+       var happy = 0
+       # TTL for the monster being shocked
+       var shocked = 0
+       # TTL for the monster blinking
+       var blink = 0
+
+       init(game: Game, i: Int)
+       do
+               self.game = game
+               var x = 440 + 58 * ((i-1).abs%3)
+               var y = 150 + (bh+5) * ((i-1)/3)
+               super(game, x, y, game.bw, game.bh)
+               if i == 0 then return
+               kind = i
+               imgx = 0
+               imgy = (4+i)
+               over = game.colors[i] + " monster ({i})"
+               shortcut = i.to_s # code for 1 trough 9
+       end
+
+       redef fun click(ev)
+       do
+               super
+               self.shocked = 5
+       end
+
+       redef fun update
+       do
+               if self.happy > 0 then
+                       self.happy-=1
+                       self.dirty=true
+               end
+               if self.shocked > 0 then
+                       self.shocked-=1
+                       self.dirty=true
+               end
+               if self.blink > 0 then
+                       self.blink-=1
+                       self.dirty=true
+               else if 100.rand == 0 then
+                       self.blink = 5
+                       self.dirty=true
+               end
+       end
+
+       redef fun draw(ctx)
+       do
+               var s = self.imgx
+               if self.angries>0 then
+                       s += 3
+               else if self.happy > 5 then
+                       s += 5
+               else if self.shocked > 0 then
+                       s += 5
+               else if self.blink > 0 then
+                       s += 1
+               end
+               ctx.blit(game.img[s, self.imgy], self.x, self.y)
+               if game.selected_button == self then ctx.blit(game.img[0, 0], self.x, self.y)
+       end
+end
+
+# Erase button.
+class EraseButton
+       super Button
+       init(game: Game) do
+               super(game, 440, 92, game.bh, 22+game.bh)
+               imgx = 4
+               imgy = 13
+               kind = 0
+               over = "Eraser (0)"
+               shortcut = "0"
+       end
+end
+
+# Metal (fixed) button.
+class MetalButton
+       super Button
+       init(game: Game)
+       do
+               super(game, 498, 92, game.bh, 20+game.bh)
+               imgx = 3
+               imgy = 3
+               kind = -1
+               over = "Metal block (q)"
+               shortcut = "q"
+       end
+
+       private var fixed = false
+
+       redef fun click_board(ev,t)
+       do
+               if not ev.drag then self.fixed = not t.fixed
+               if t.fixed == self.fixed then return
+               t.fixed = self.fixed
+               game.snd_click.replay
+       end
+end
+
+# Resize button.
+class ResizeButton
+       super Button
+
+       init(game: Game)
+       do
+               super(game,556, 92, game.bh, 20+game.bh)
+               kind = -2
+               over = "Resize the grid"
+       end
+
+       redef fun draw(ctx)
+       do
+               for i in [0..3[ do
+                       for j in [0..3[ do
+                               var x = self.x + i*bw/3
+                               var y = self.y + j*bh/3
+                               ctx.blit_scaled(game.img[5+(i+j)%2,0], x, y, bw/3, bh/3)
+                       end
+               end
+               if game.selected_button == self then ctx.blit(game.img[0, 0], self.x, self.y)
+       end
+
+       redef fun click(ev)
+       do
+               if game.selected_button != game.button_size then
+                       super
+               else
+                       game.selected_button = null
+                       game.board.dirty=true
+               end
+       end
+
+       redef fun click_board(evt, t)
+       do
+               var grid = t.grid
+               var w = t.x+1
+               var h = t.y+1
+               if w < 3 or h < 3 then
+                       game.snd_bing.replay
+                       game.statusbar.set_tmp("Too small!", "red")
+                       return
+               end
+               var aborts = false
+               for i in [0..grid.width[ do
+                       for j in [0..grid.height[ do
+                               if i>=w or j>=h then
+                                       var t2 = grid.grid[i][j]
+                                       if t2.kind > 0 then
+                                               aborts = true
+                                               t2.shocked = 5
+                                       end
+                               end
+                       end
+               end
+               if aborts then
+                       game.snd_duh.replay
+                       game.statusbar.set_tmp("Monsters on the way!", "red")
+                       return
+               end
+               game.snd_click.replay
+               grid.resize(w,h)
+       end
+end
+
+# Inactive area used to display the score
+class Score
+       super Entity
+       init(game: Game)
+       do
+               super(game,440,310,199,62)
+       end
+       redef fun draw(ctx)
+       do
+               ctx.textx("MONSTERS: {game.grid.number}",self.x,self.y+1,21,"cyan",null)
+               var level = game.level
+               if level == null then return
+               if level.get_state >= level.l_won then
+                       ctx.textx("BEST: {level.score}",self.x,self.y+22,21,"pink", null)
+               else
+                       ctx.textx("BEST: -",self.x,self.y+22,21,"pink", null)
+               end
+               if game.levels[9].get_state >= level.l_won then
+                       if level.is_challenge then
+                               ctx.textx("GOAL: {level.par}",self.x,self.y+44,21,"yellow",null)
+                       else
+                               ctx.textx("PAR: {level.par}",self.x,self.y+44,21,"yellow",null)
+                       end
+               end
+       end
+end
+
+# Status bar element.
+class StatusBar
+       super Entity
+       init(game: Game)
+       do
+               super(game,24, 440, 418-24, 30)
+       end
+
+       # Permanant text, if any
+       var main_txt: nullable String = null
+
+       # Text to display when the cursor if over an entity (`over_what`), if any
+       var over_txt: nullable String = null
+
+       # What is the entity for `over_txt`
+       var over_what: nullable Entity
+
+       # Text to temporally display, for some game event, if any
+       var tmp_txt: nullable String = null
+
+       # time-to-live for the `tmp_txt`
+       var tmp_txt_ttl = 0
+
+       # Color used to display `tmp_txt`
+       var tmp_txt_color: nullable String = null
+
+       # reset the status
+       fun clear do
+               self.main_txt = null
+               self.over_txt = null
+               self.tmp_txt = null
+               self.over = null
+       end
+
+       # set a temporary text
+       fun set_tmp(txt, color: String)
+       do
+               print "***STATUS** {txt}"
+               self.tmp_txt = txt
+               self.tmp_txt_ttl = 20
+               self.tmp_txt_color = color
+       end
+
+       redef fun draw(ctx)
+       do
+               var tmp_txt = self.tmp_txt
+               var over_txt = self.over_txt
+               var main_txt = self.main_txt
+               if tmp_txt != null and self.tmp_txt_ttl>0 then
+                       ctx.textx(tmp_txt,24,442,24,self.tmp_txt_color,null)
+               else if over_txt != null then
+                       ctx.textx(over_txt,24,442,24,"yellow",null)
+               else if main_txt != null then
+                       ctx.textx(main_txt,24,442,24,"white",null)
+               end
+       end
+
+       redef fun update
+       do
+               if self.tmp_txt_ttl>0 then
+                       self.tmp_txt_ttl-=1
+                       self.dirty=true
+               end
+       end
+end
+
+# ************************************************************************/
+
+# Simple audio asset
+class Audio
+       var path: String
+
+       # placebo
+       fun play do end
+
+       # placebo
+       fun pause do end
+
+       # Play a sound.
+       fun replay
+       do
+               sys.system("aplay assets/{path} &")
+       end
+end
+
+redef class Display
+       # Display a text
+       fun textx(str: String, x, y, height: Int, color, color2: nullable String)
+       do
+               #var w = measureText(str, height)
+               #rect(x,y,w,height)
+               text(str.to_upper, app.game.font, x, y)
+       end
+
+       # give the width for a giver text
+       fun measureText(str: String, height: Int): Int
+       do
+               var font = app.game.font
+               return str.length * (app.game.font.width + app.game.font.hspace)
+       end
+
+       # displays a debug rectangle
+       fun rect(x,y,w,h:Int)
+       do
+               var image = once app.load_image("hitbox.png")
+               blit_scaled(image, x, y, w, h)
+       end
+end
+
+# Simple basic class for event
+class Event
+       # Is a drag event?
+       var drag = false
+       # screen x
+       var offset_x: Int
+       # screen y
+       var offset_y: Int
+       # entity x
+       var game_x = 0
+       # entity y
+       var game_y = 0
+       # key pressed
+       var char_code: String
+end
+
+redef class Game
+       # width of a tile, used for most width reference in the game
+       var bw = 48
+       # height a tile, used for most width reference in the game
+       var bh = 48
+       # x-coordinate of the board (padding)
+       var xpad = 24
+       # y-coordinate of the board (padding)
+       var ypad = 24
+
+       # Load tiles
+
+       # Basic tileset
+       var img = new TileSet(app.load_image("tiles2.png"),48,48)
+
+       # Sub tileset (for marks or other)
+       var img2 = new TileSet(app.load_image("tiles2.png"),24,24)
+
+       # background image
+       var back: Image = app.load_image("background.png")
+
+       # Logo image
+       var logo: Image = app.load_image("logo.png")
+
+       # Font
+       var font = new TileSetFont(app.load_image("deltaforce_font.png"), 16, 17, "ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789.:;!?\"'() -,/")
+
+       var xxx = """
+       fun save_cookie(name, val:String) do
+       var days = 365
+       var date = new Date()
+       date.setTime(date.getTime()+(days*24*60*60*1000))
+       document.cookie = name+"="+val+"; expires="+date.toGMTString()+"; path=/"
+       end
+
+       fun read_cookie(name:String):String do
+       var key = name + "="
+       var ca = document.cookie.split(';')
+       for(var i=0; i<ca.length; i++) do
+       var c = ca[i]
+       while (c[0]==' ') c = c.substring(1, c.length)
+       if (c.indexOf(key) == 0) return c.substring(key.length)
+       end
+       return null
+       end
+       """
+
+       # DISPLAY *****************************************************************
+
+       # Is the game in editing mode
+       var editing = false
+
+       # The selected button, if any
+       var selected_button: nullable Button = null
+
+       # SOUND
+
+       # Is the music muted?
+       var music_muted: Bool = true #read_cookie("music_muted")
+
+       # Is the sound effects muted?
+       var sfx_muted: Bool = true #read_cookie("sfx_muted")
+
+       # The background music resource. */
+       var music = new Audio("music.ogg")
+
+       # Click sound
+       var snd_click = new Audio("click.wav")
+
+       # Wining soulf
+       var snd_win = new Audio("level.wav")
+
+       # Shocked sound
+       var snd_duh = new Audio("duh.wav")
+
+       # metal sound
+       var snd_bing = new Audio("bing.wav")
+
+       # transition sound
+       var snd_whip = new Audio("whip.wav")
+
+       # INPUT ******************************************************************
+
+       # Current grid edited (if any).
+       var grid_edit: nullable Grid = null
+
+       # Sequence of current entities
+       var entities = new Array[Entity]
+
+       # The current statusbar
+       var statusbar = new StatusBar(self)
+
+       # The grid board
+       var board = new Board(self)
+
+       # The current score board
+       var score = new Score(self)
+
+       # Monster button game elements.
+       var buttons = new Array[MonsterButton]
+
+       # MetalButton
+       var button_wall = new MetalButton(self)
+
+       # EraseButton
+       var button_erase = new EraseButton(self)
+
+       # ResizeButton
+       var button_size = new ResizeButton(self)
+
+       # fill `buttons`
+       fun init_buttons
+       do
+               for i in [0..9] do
+                       buttons[i] = new MonsterButton(self, i)
+               end
+       end
+
+       # Play a level in player mode.
+       fun play(l: Level)
+       do
+               level = l
+               grid.load(level.str)
+               init_play_menu(false)
+               if level.status != "" then
+                       statusbar.main_txt = level.status
+               else
+                       statusbar.main_txt = level.fullname
+               end
+               var t = new NextLevelButton(self)
+               entities.push(t)
+               run
+       end
+
+       # Play the next level.
+       fun play_next
+       do
+               play(levels[level.number+1])
+       end
+
+
+       # Helper function to initialize all states.
+       # Set up buttons for music and SFX.
+       fun init_game
+       do
+               editing = false
+               solver = null
+               entities.clear
+               entities.push(new MusicButton(self))
+               entities.push(new SFXButton(self))
+               entities.push(new MenuButton(self))
+               statusbar.clear
+               entities.push(statusbar)
+       end
+
+       # Helper function to initialize monster menu entries.
+       fun init_play_menu(full: Bool)
+       do
+               init_game
+               entities.push(board)
+               entities.push(new ResetButton(self))
+               entities.push(button_erase)
+               # Push monster buttons and determine the selected one
+               var sel: nullable Button = null
+               for i in [1..monsters] do
+                       if grid.monsters[i].number > 0 or full then
+                               if selected_button == buttons[i] or sel == null then
+                                       sel = buttons[i]
+                               end
+                               entities.push(buttons[i])
+                       end
+               end
+               selected_button = sel
+               entities.push(score)
+       end
+
+       # Play a arbitrary grid in try mode.
+       fun play_grid(g: Grid)
+       do
+               grid = g
+               init_play_menu(false)
+               statusbar.main_txt = "User level"
+               if grid_edit != null then
+                       entities.push(new EditButton(self))
+               end
+               entities.push(new WonButton(self))
+               run
+       end
+
+       # Launch the editor starting with a grid.
+       fun edit_grid(g: Grid)
+       do
+               grid = g
+               init_play_menu(true)
+               editing = true
+               statusbar.main_txt = "Level editor"
+               if level != null then statusbar.main_txt += ": level "+level.name
+               entities.push(button_wall)
+               entities.push(button_size)
+               entities.push(new TestButton(self))
+               entities.push(new SaveButton(self))
+               entities.push(new LoadButton(self))
+               run
+       end
+
+       # Launch the title screen
+       fun title
+       do
+               init_menu
+               entities.push(new Splash(self))
+               run
+       end
+
+       # Helper function to initialize the menu (and tile) screen
+       fun init_menu
+       do
+               init_game
+               level = null
+               var i = levels.first
+               for l in levels do
+                       if l.get_state == l.l_open then break
+                       i = l
+               end
+               entities.push(new StartButton(self, i))
+       end
+
+       # Launch the menu.
+       fun menu
+       do
+               init_menu
+               var t
+               t = new TextButton(self,"LEVEL SELECT", 120, ypad, "white", null, null)
+               entities.push(t)
+               for i in [0..levels.length[ do
+                       var b = new LevelButton(levels[i])
+                       entities.push(b)
+               end
+               t = new Achievement(self, 0, "Training")
+               entities.push(t)
+               t = new Achievement(self, 1, "Par")
+               entities.push(t)
+               t = new Achievement(self, 2, "Editor")
+               entities.push(t)
+               t = new Achievement(self, 3, "Challenge")
+               entities.push(t)
+               t = new Achievement(self, 4, "Congraturation")
+               entities.push(t)
+               t = new Achievement(self, 5, "Awesome")
+               entities.push(t)
+               run
+       end
+
+       # Last function called when the lauch state is ready
+       fun run do
+               dirty_all = true
+       end
+
+       init
+       do
+               load_levels
+               init_buttons
+               entities.clear
+               title
+       end
+
+       # Should all entity redrawn?
+       var dirty_all = true
+
+       # Draw all game entities.
+       fun draw(display: Display) do
+               dirty_all = true
+               if dirty_all then display.blit(back, 0, 0)
+               for g in entities do
+                       if g.dirty or dirty_all then
+                               g.dirty = false
+                               #if g.x2-g.x>0 and g.y2-g.y>0 then ctx.drawImage(back, g.x, g.y, g.x2-g.x, g.y2-g.y, g.x, g.y, g.x2-g.x, g.y2-g.y)
+                               g.draw(display)
+                               #ctx.rect(g.x, g.y, g.width, g.height)
+                       end
+               end
+               var ev = lastev
+               if ev isa Event then
+                       display.blit(img[4,0],ev.offset_x-42,ev.offset_y-6)
+               end
+               dirty_all = false
+       end
+
+       # Update all game entities.
+       fun step do
+               if solver != null and not solver_pause then
+                       for i in [0..solver_steps[ do
+                               if solver.step then
+                                       solver_pause = true
+                                       break
+                               end
+                       end
+                       solver.dump
+                       if solver.is_over then solver = null
+               end
+               for g in entities do
+                       g.update
+               end
+       end
+
+       # Return the game entity located at a mouse event.
+       fun get_game_element(ev: Event): nullable Entity
+       do
+               var x = ev.offset_x
+               var y = ev.offset_y
+               for g in entities do
+                       if x>=g.x and x<g.x2 and y>g.y and y<g.y2 then
+                               ev.game_x = x-g.x
+                               ev.game_y = y-g.y
+                               #print "get {g}"
+                               return g
+                       end
+               end
+               return null
+       end
+
+       # The game entlty the mouse went down on
+       var drag: nullable Entity = null
+
+       # Last mouse event. Used to dray the cursor
+       var lastev: nullable Event = null
+
+       # Callback when the mouse is pressed
+       fun onMouseDown(ev: Event) do
+               lastev = ev
+               var g = get_game_element(ev)
+               if g != null then
+                       g.click(ev)
+                       g.dirty = true
+               end
+               drag = g
+       end
+
+       # Callback when the mouse is releassed
+       fun onMouseUp(ev: Event) do
+               drag = null
+       end
+
+       # Callback when the mouse if moved while pressed
+       fun onMouseMove(ev: Event) do
+               lastev = ev
+               var g = get_game_element(ev)
+               if g == null then
+                       statusbar.dirty = true
+                       statusbar.over_txt = null
+                       statusbar.over_what = null
+                       return
+               end
+               if statusbar.over_what != g then
+                       statusbar.dirty = true
+                       var go = g.over
+                       statusbar.over_txt = go
+                       statusbar.over_what = g
+                       g.enter(ev)
+                       if go != null then print "***OVER*** {go}"
+               end
+               # We moved abode a element that accepts drag event
+               if drag == g and g.draggable then
+                       # print "DRAG {g}"
+                       ev.drag = true
+                       g.click(ev)
+                       g.dirty = true
+               end
+       end
+
+       # Current solver, if any
+       var solver: nullable Solver = null
+
+       # Is the solver paused?
+       var solver_pause = false
+
+       # Number of solver steps played in a single game `update`
+       var solver_steps = 20000
+
+       # Callback when a keyboard event is recieved
+       fun onKeyDown(ev: Event) do
+               var kc = ev.char_code
+               if kc == "e" then
+                       grid_edit = grid.copy(true)
+                       edit_grid(grid)
+               else if kc == "s" then
+                       if solver == null then
+                               solver = new Solver(grid)
+                               solver_pause = false
+                       else
+                               solver_pause = not solver_pause
+                       end
+                       #solver.step
+               else if kc == "d" then
+                       if solver == null then
+                               solver = new Solver(grid)
+                               solver_pause = true
+                       else
+                               solver.step
+                       end
+               else if kc == "+" then
+                       solver_steps += 100
+                       print solver_steps
+               else if kc == "-" then
+                       solver_steps -= 100
+                       print solver_steps
+               else for g in entities do
+                       if kc == g.shortcut then
+                               g.click(ev)
+                               g.dirty = true
+                       end
+               end
+       end
+end
+
+# The spash title image
+class Splash
+       super Entity
+       init(game: Game)
+       do
+               super(game,game.xpad,game.ypad,380,350)
+       end
+       redef fun draw(ctx)
+       do
+               ctx.blit(game.logo, game.xpad, game.ypad)
+       end
+       redef fun click(ev)
+       do
+               game.snd_whip.replay
+               game.menu
+       end
+end
+
+class NextLevelButton
+       super TextButton
+       init(game: Game)
+       do
+               super(game, "NEXT LEVEL", 440, 24, "cyan", "Play next level", null)
+               enabled = false
+       end
+
+       redef fun update
+       do
+               var w = game.level.check_won(game.grid)
+               if self.enabled != w then
+                       self.dirty = true
+                       self.enabled = w
+                       if w then
+                               game.snd_win.replay
+                               game.statusbar.set_tmp("Level solved!", "cyan")
+                       end
+               end
+       end
+
+       redef fun click(ev)
+       do
+               if not self.enabled then
+                       game.snd_duh.replay
+                       var grid = game.grid
+                       var monsters = grid.monsters
+                       var angry = new Array[Tile]
+                       var lonely = new Array[Tile]
+                       var edges = new Array[Tile]
+                       for i in [0..grid.width[ do
+                               for j in [0..grid.height[ do
+                                       var t = grid.grid[i][j]
+                                       if t.kind == 0 then continue
+                                       if t.nexts == 0 then lonely.push(t)
+                                       if t.nexts == 1 and not monsters[t.kind].chain then edges.push(t)
+                                       if t.nexts > 2 then angry.push(t)
+                               end
+                       end
+
+                       var l
+                       if angry.length>0 then
+                               l = angry
+                       else if lonely.length>0 then
+                               l = lonely
+                       else
+                               l = edges
+                       end
+                       for i in l do i.shocked=5
+
+                       if angry.length>0 then
+                               game.statusbar.set_tmp("Angry monsters!", "red")
+                       else if lonely.length>0 then
+                               game.statusbar.set_tmp("Lonely monsters!", "red")
+                       else if not grid.won then
+                               game.statusbar.set_tmp("Unconnected monsters!", "red")
+                       else
+                               game.statusbar.set_tmp("Not enough monsters!", "red")
+                       end
+                       return
+               end
+
+               game.snd_whip.replay
+               game.play_next
+       end
+end
+
+class MusicButton
+       super TextButton
+       init(game: Game)
+       do
+               super(game, "MUSIC", 470, 412, "purple", "Mute the music", "Unmute the music")
+       end
+       redef fun click2(ev)
+       do
+               game.music_muted = self.toggled
+               if game.music_muted then game.music.pause else game.music.play
+               #game.save_cookie("music_muted",music_muted?"true":"")
+       end
+end
+
+class SFXButton
+       super TextButton
+       init(game: Game)
+       do
+               super(game, "SOUND FX", 470, 382, "purple", "Mute the sound effects", "Unmute the sound effects")
+       end
+
+       redef fun click2(ev)
+       do
+               game.sfx_muted = self.toggled
+               if not game.sfx_muted then game.snd_whip.replay # Because the automatic one was muted
+               #save_cookie("sfx_muted",sfx_muted?"true":"")
+       end
+end
+
+class MenuButton
+       super TextButton
+       init(game: Game)
+       do
+               super(game, "MENU", 470, 442, "purple", "Exit to the main menu", null)
+               shortcut = "back"
+       end
+
+       redef fun click2(ev)
+       do
+               game.menu
+       end
+end
+
+class ResetButton
+       super TextButton
+       init(game: Game)
+       do
+               super(game,"RESET", 440, 61, "purple", "Clear the grid",null)
+       end
+
+       var count = 0
+
+       redef fun click2(ev)
+       do
+               self.count += 1
+               if self.count==1 then
+                       game.statusbar.set_tmp("Click again to reset","white")
+               else if self.count==2 then
+                       game.grid.reset(false)
+                       if game.editing then
+                               game.statusbar.set_tmp("Click again to clear all","white")
+                       end
+               else if game.editing then
+                       game.grid.reset(true)
+               end
+               game.dirty_all = true
+       end
+
+       redef fun enter2
+       do
+               self.count = 0
+       end
+end
+
+class EditButton
+       super TextButton
+       init(game: Game)
+       do
+               super(game,"EDIT", 540, 24, "purple", "Return to the editor",null)
+       end
+
+       redef fun click2(ev)
+       do
+               var ge = game.grid_edit
+               assert ge != null
+               game.edit_grid(ge)
+       end
+end
+
+class WonButton
+       super TextButton
+       init(game: Game)
+       do
+               super(game,"WON", 440, 24, "cyan", "", null)
+               enabled = false
+       end
+       redef fun click2(ev)
+       do
+               var ge = game.grid_edit
+               if not self.enabled then
+                       game.statusbar.set_tmp("Solve the level first!", "red")
+               else if ge != null then
+                       game.snd_whip.replay
+                       game.edit_grid(ge)
+               else
+                       game.snd_whip.replay
+                       game.menu
+               end
+       end
+
+       redef fun update
+       do
+               var w = game.grid.won
+               if self.enabled != w then
+                       self.dirty = true
+                       self.enabled = w
+                       if w then
+                               game.snd_win.replay
+                               game.statusbar.set_tmp("Level solved!", "cyan")
+                       end
+               end
+       end
+end
+
+class TestButton
+       super TextButton
+       init(game: Game)
+       do
+               super(game,"TEST", 440, 24, "cyan", "Try to play the level", null)
+       end
+
+       redef fun click2(ev)
+       do
+               game.grid_edit = game.grid
+               game.play_grid(game.grid.copy(false))
+       end
+end
+
+class SaveButton
+       super TextButton
+       init(game: Game)
+       do
+               super(game, "SAVE", 540, 24, "purple", "Save the level", null)
+       end
+
+       redef fun click2(ev)
+       do
+               var res = game.grid.save
+               print "SAVE: {res}"
+       end
+end
+
+class LoadButton
+       super TextButton
+       init(game: Game)
+       do
+               super(game,"LOAD", 540, 61, "purple", "Load an user level", null)
+       end
+
+       redef fun click2(ev)
+       do
+               var grid2 = new Grid(game.gw,game.gh,game.monsters)
+               if grid2.load("") then
+                       game.grid = grid2
+               end
+               game.dirty_all = true
+       end
+end
+
+class ContinueButton
+       super TextButton
+       init(game: Game)
+       do
+               super(game,"CONTINUE", 440, 24, "purple", "Continue playing", null)
+       end
+
+       redef fun click2(ev)
+       do
+               game.play_next
+       end
+end
+
+class StartButton
+       super TextButton
+       var level: Level
+       init(game: Game, level: Level)
+       do
+               self.level = level
+               if level.number == 0 then
+                       super(game,"START", 440, 24, "purple", "Play the first level", null)
+               else
+                       super(game,"CONTINUE", 440, 24, "purple", "Continue from level "+level.name, null)
+               end
+       end
+
+       redef fun click2(ev)
+       do
+               game.play(level)
+       end
+end
+
+#
+
+redef class App
+
+       # The game
+       var game: Game
+
+       # Wanted screen width
+       var screen_width = 640
+
+       # Wanted screen height
+       var screen_height = 480
+
+       redef fun window_created
+       do
+               super
+               game = new Game
+               game.font.hspace = -2
+               if args.length > 0 then
+                       game.play(game.levels[args.first.to_i])
+               end
+               # img loading?
+       end
+
+       # Maximum wanted frame per second
+       var max_fps = 30
+
+       # clock used to track FPS
+       private var clock = new Clock
+
+       redef fun frame_core(display)
+       do
+               game.step
+               game.draw(display)
+               var dt = clock.lapse
+               var target_dt = 1000000000 / max_fps
+               if dt.sec == 0 and dt.nanosec < target_dt then
+                       var sleep_t = target_dt - dt.nanosec
+                       sys.nanosleep(0, sleep_t)
+               end
+       end
+
+       redef fun input(input_event)
+       do
+               #print input_event
+               if input_event isa QuitEvent then # close window button
+                       quit = true # orders system to quit
+               else if input_event isa PointerEvent then
+                       var ev = new Event(input_event.x.to_i, input_event.y.to_i, "")
+                       if input_event.is_motion then
+                               game.onMouseMove(ev)
+                       else if input_event.pressed then
+                               game.onMouseDown(ev)
+                       else
+                               game.onMouseUp(ev)
+                       end
+                       return true
+               else if input_event isa KeyEvent and input_event.is_down then
+                       var ev = new Event(0, 0, input_event.key_name)
+                       game.onKeyDown(ev)
+                       return true
+               end
+
+               return false
+       end
+end
+
+redef class PointerEvent
+       fun is_motion: Bool do return false
+end
+
+redef class KeyEvent
+       fun key_name: String
+       do
+               var c = to_c
+               if c != null then return c.to_s
+               return "unknown"
+       end
+end
diff --git a/contrib/friendz/src/friendz_android.nit b/contrib/friendz/src/friendz_android.nit
new file mode 100644 (file)
index 0000000..94b9d2b
--- /dev/null
@@ -0,0 +1,27 @@
+# Monsterz - Chains of Friends
+#
+# 2010-2014 (c) Jean Privat <jean@pryen.org>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the Do What The Fuck You Want To
+# Public License, Version 2, as published by Sam Hocevar. See
+# http://sam.zoy.org/projects/COPYING.WTFPL for more details.
+
+# android version of the game
+module friendz_android
+
+import friendz
+import mnit_android
+
+redef class App
+       redef fun window_created
+       do
+               super
+               var w = screen_width
+               display.set_viewport(0,0,w,w*display.height/display.width)
+       end
+end
+
+redef class AndroidPointerEvent
+       redef fun is_motion do return not motion_event.just_went_down
+end
diff --git a/contrib/friendz/src/friendz_linux.nit b/contrib/friendz/src/friendz_linux.nit
new file mode 100644 (file)
index 0000000..7940843
--- /dev/null
@@ -0,0 +1,27 @@
+# Monsterz - Chains of Friends
+#
+# 2010-2014 (c) Jean Privat <jean@pryen.org>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the Do What The Fuck You Want To
+# Public License, Version 2, as published by Sam Hocevar. See
+# http://sam.zoy.org/projects/COPYING.WTFPL for more details.
+
+# Linux (SDL) version of the game
+module friendz_linux
+
+import friendz
+import mnit_linux
+
+redef class Display
+       redef fun wanted_width do return app.screen_width
+       redef fun wanted_height do return app.screen_height
+end
+
+redef class SDLDisplay
+       redef fun enable_mouse_motion_events do return true
+end
+
+redef class SDLMouseMotionEvent
+       redef fun is_motion do return true
+end
diff --git a/contrib/friendz/src/grid.nit b/contrib/friendz/src/grid.nit
new file mode 100644 (file)
index 0000000..3913154
--- /dev/null
@@ -0,0 +1,571 @@
+# Monsterz - Chains of Friends
+#
+# 2010-2014 (c) Jean Privat <jean@pryen.org>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the Do What The Fuck You Want To
+# Public License, Version 2, as published by Sam Hocevar. See
+# http://sam.zoy.org/projects/COPYING.WTFPL for more details.
+
+# Game login on the grid of monsters
+module grid
+
+# Grid of monsters.
+class Grid
+       # width of the current grid
+       var width: Int
+
+       # maximum width of the grid
+       var max_width: Int
+
+       # height of the current grid
+       var height: Int
+
+       # maximum height of the grid
+       var max_height: Int
+
+       # nm is the number of monster + 1 for the empty tile
+       var nb_monsters: Int
+
+       # the data grid
+       private var grid = new Array[Array[Tile]]
+
+       init(mw,mh,nm: Int)
+       do
+               self.max_width = mw
+               self.max_height = mh
+               self.nb_monsters = mh
+               clear
+       end
+
+       # Reinitialize the grid with new empty tiles and monsters info
+       fun clear
+       do
+               self.number = 0
+               self.error = 0
+               self.won = false
+               for i in [0..max_width[ do
+                       self.grid[i] = new Array[Tile]
+                       for j in [0..max_height[ do
+                               var t = new Tile(self, i, j)
+                               self.grid[i][j] = t
+                       end
+               end
+               self.monsters = new Array[MonsterInfo]
+               for i in [0..nb_monsters] do
+                       self.monsters[i] = new MonsterInfo
+               end
+               self.resize(max_width,max_height)
+       end
+
+       # Clear all monsters
+       # if `fixed` is false, fixed monsters remains
+       fun reset(fixed: Bool): Bool
+       do
+               var r = false
+               for i in [0..width[ do
+                       for j in [0..height[ do
+                               var t = self.grid[i][j]
+                               if fixed then t.fixed = false
+                               if not t.fixed and t.kind>0 then
+                                       t.update(0)
+                                       r = true
+                               end
+                       end
+               end
+               return r
+       end
+
+       # Total number of monsters in the grid
+       var number = 0
+
+       # Number of monsters alone or with >=3 next
+       var error = 0
+
+       # information about each kind of monsters
+       var monsters = new Array[MonsterInfo]
+
+       # Last result of win.
+       var won = false
+
+       # Check that the monster of `kind` form a complete chain.
+       # if not null, `t` is used as a starting tile to check the chain
+       fun check_chain(kind: Int, t: nullable Tile): Bool
+       do
+               var m = monsters[kind]
+               if m.angry > 0 or m.lonely > 0 or m.single > 2 then
+                       # easy case for no chain
+                       # print "easy {kind} chain: false"
+                       m.chain = false
+                       return false
+               else
+                       if t == null then
+                               # Search for a starting
+                               for x in [0..width[ do for y in [0..height[ do
+                                       var t2 = get(x,y)
+                                       if t2 == null or t2.kind != kind then continue
+                                       t = t2
+                                       break label found
+                               end label found
+                               if t == null then
+                                       assert m.number == 0
+                                       m.chain = true
+                                       return m.chain
+                               end
+                               # print "get neighbor {t}"
+                       end
+                       assert t.kind == kind
+                       var c = count_chain(t, 1000.rand)
+                       # print "old {kind} chain? {c} / {m.number}"
+                       m.chain = c == m.number
+                       return m.chain
+               end
+       end
+
+       # The total number of monsters connected to the chain of `t`
+       # `mark` is a uniq number used to mark tiles
+       fun count_chain(t: Tile, mark: Int): Int
+       do
+               t.chain_mark = mark
+               var res = 1
+               for i in [-1..1] do
+                       for j in [-1..1] do
+                               if (i==0 and j==0) or (i!=0 and j!=0) then continue
+                               var t2 = get(t.x+i,t.y+j)
+                               if t2 == null then continue
+                               if t2.chain_mark == mark or t2.kind != t.kind then continue
+                               res += count_chain(t2, mark)
+                       end
+               end
+               return res
+       end
+
+       # Resize the grid. Do not touch the content.
+       fun resize(w,h: Int)
+       do
+               self.width = w
+               self.height = h
+       end
+
+       # Try to get the tila at `x`,`y`.
+       # Returns null if the position is out of bound.
+       fun get(x,y: Int): nullable Tile
+       do
+               if x<0 or x>=self.width or y<0 or y>=self.height then return null
+               return self.grid[x][y]
+       end
+
+
+       var fixed_shaped = """[
+       [{x:1,y:0},{x:2,y:0},{x:1,y:1},{x:2,y:1}],
+       [{x:0,y:0},{x:0,y:1},{x:0,y:2}],
+       [{x:1,y:2},{x:2,y:2},{x:3,y:2}],
+       [{x:4,y:1},{x:4,y:2}],
+       [{x:3,y:0},{x:4,y:0}],
+       [{x:3,y:1}]
+       ]"""
+
+       # Set shapes for the fixed blocks.
+       fun metalize
+       do
+               for i in [0..width[ do
+                       for j in [0..height[ do
+                               var t = self.grid[i][j]
+                               if t.fixed then t.shape = null
+                       end
+               end
+               for shape in fixed_shaped.split(",") do
+                       for i in [0..width[ do
+                               for j in [0..height[ do
+                                       var ts = new Array[Tile]
+                                       for l in [0..shape.length[ do
+                                               #var t = self.get(i+shape[l].x-shape[0].x,j+shape[l].y-shape[0].y)
+                                               var t = self.get(i,j)
+                                               if t != null and t.fixed and t.shape == null then ts.push(t)
+                                       end
+                                       if ts.length == shape.length then
+                                               for l in [0..shape.length[ do
+                                                       ts[l].shape = shape[l]
+                                               end
+                                       end
+                               end
+                       end
+               end
+       end
+
+       # Return the serialization of the fixed tiles. */
+       fun save: String
+       do
+               var res = ""
+               var str = ".#ABCDEFGHI"
+               for y in [0..height[ do
+                       var rle = 0
+                       var last: nullable Int = null
+                       for x in [0..width[ do
+                               var t = self.grid[x][y]
+                               var tk = 0
+                               if t.fixed then tk = t.kind + 1
+                               if tk == last and rle<9 then
+                                       rle += 1
+                               else
+                                       if last != null then
+                                               if rle>1 then res += rle.to_s
+                                               res += str.chars[last].to_s
+                                       end
+                                       rle = 1
+                                       last = tk
+                               end
+                       end
+                       if last != null then
+                               if rle>1 then res += rle.to_s
+                               res += str.chars[last].to_s
+                       end
+                       res += "|"
+               end
+               return res
+       end
+
+       # Load a new grid from a seialization.
+       fun load(str: String): Bool
+       do
+               self.clear
+               var l = str.length
+               var x = 0
+               var y = 0
+               var mx = 1
+               var my = 1
+               var rle = 1
+               for i in [0..l[ do
+                       var z = rle
+                       while z > 0 do
+                               z -= 1
+                               rle = 1
+                               var c = str.chars[i]
+                               if c == '|' then
+                                       if x > mx then mx = x
+                                       x = 0
+                                       y += 1
+                               else if c == '.' then
+                                       x += 1
+                               else if c == '#' then
+                                       var t = self.get(x,y)
+                                       t.fixed = true
+                                       x += 1
+                               else if c >= 'A' and c <= 'I' then
+                                       var t = self.get(x,y)
+                                       assert t != null
+                                       t.update(c.ascii-'A'.ascii+1)
+                                       t.fixed = true
+                                       x += 1
+                               else if c >= '1' and c <= '9' then
+                                       rle = c.to_i
+                               else
+                                       abort
+                               end
+                       end
+               end
+               if x>0 then y += 1
+               if x > mx then mx = x
+               if y > my then my = y
+               if mx<3 or my<3 or mx>=max_width or my>=max_height then
+                       return false
+               end
+               self.resize(mx,my)
+               self.metalize
+               return true
+       end
+
+       # A ASCII version of the grid.
+       redef fun to_s: String
+       do
+               var ansicols = once ["37;1","31","36;1","32;1","35;1","33;1","33","34;1","31;1","37"]
+               var b = new FlatBuffer
+               b.append("{width}x{height}\n")
+               for j in [0..height[ do
+                       for i in [0..width[ do
+                               var t = grid[i][j]
+                               var k = t.kind
+                               var c = ' '
+                               if k == 0 then
+                                       if t.fixed then c = '#'
+                               else
+                                       b.add(0x1b.ascii)
+                                       b.add('[')
+                                       b.append ansicols[k]
+                                       c = (k + 'a'.ascii - 1).ascii
+                                       if t.fixed then c = c.to_upper
+                                       b.append("m")
+                               end
+                               b.add(c)
+                               if k != 0 then
+                                       b.add(0x1b.ascii)
+                                       b.append("[0m")
+
+                               end
+                       end
+                       b.append "|\n"
+               end
+               return b.to_s
+       end
+
+       # Return a copy of the current grid.
+       # if (!no_fixed) copy only the fixed tiles.
+       fun copy(no_fixed: Bool): Grid
+       do
+               var g = new Grid(self.max_width, self.max_height, self.nb_monsters)
+               g.resize(width, height)
+               for y in [0..height[ do
+                       for x in [0..width[ do
+                               var t = self.grid[x][y]
+                               if no_fixed or t.fixed then
+                                       var t2 = g.grid[x][y]
+                                       t2.update(t.kind)
+                                       t2.fixed = t.fixed
+                               end
+                       end
+               end
+               g.metalize
+               return g
+       end
+
+       # Internal check of the validity of tile and monster informations
+       fun check_grid
+       do
+               var m2 = new Array[MonsterInfo]
+               for m in [0..nb_monsters] do
+                       m2[m] = new MonsterInfo
+               end
+               for x in [0..width[ do
+                       for y in [0..height[ do
+                               var n = 0
+                               var f = 0
+                               var t = get(x,y)
+                               assert t != null
+                               assert t.x == x
+                               assert t.y == y
+                               var k = t.kind
+                               if k == 0 then continue
+
+                               for i in [-1..1] do
+                                       for j in [-1..1] do
+                                               if i == j or (i != 0 and j != 0) then continue
+                                               var t2 = get(x+i, y+j)
+                                               if t2 == null then continue
+                                               if t2.kind == k then
+                                                       n += 1
+                                               else if t2.kind == 0 and not t2.fixed then
+                                                       f += 1
+                                               end
+                                       end
+                               end
+                               assert n == t.nexts else
+                                       print self
+                                       print "{t} says {t.nexts} nexts, found {n}"
+                               end
+                               #assert f == t.frees else
+
+                               var m = m2[k]
+                               m.number += 1
+                               if n == 0 then
+                                       m.lonely += 1
+                               else if n == 1 then
+                                       m.single += 1
+                               else if n > 2 then
+                                       m.angry += 1
+                               end
+                       end
+               end
+               for m in [1..nb_monsters] do
+                       assert m2[m].number == monsters[m].number
+                       assert m2[m].lonely == monsters[m].lonely
+                       assert m2[m].single == monsters[m].single
+                       assert m2[m].angry == monsters[m].angry
+               end
+       end
+end
+
+# Information about each kind of monsters
+class MonsterInfo
+       # number of monsters of this kind on board
+       var number = 0
+       # number of monsters of this kind to place, -1 if no limit
+       var remains: Int = -1
+       # number of monsters that have exactly 1 next
+       var single = 0
+       # number of monsters that have exactly 0 next
+       var lonely = 0
+       # number of monsters that have 3 or more next
+       var angry = 0
+       # Are all monsters form a wining chain?
+       var chain = false
+end
+
+# A localized tile of a grid, can contain a monster and be fixed.
+class Tile
+       # The grid of the tile.
+       var grid: Grid
+
+       # The x coordinate in the grid (starting from 0).
+       var x: Int
+
+       # The y coordinate in the grid (starting from 0).
+       var y: Int
+
+       # The kind of monster in the grid. 0 means empty.
+       var kind = 0
+
+       # blink time to live (0 means no blinking).
+       var blink = 0
+
+       # shocked time to live (0 means not shocked)
+       var shocked = 0
+
+       # number of neighbors of the same kind.
+       var nexts = 0
+
+       # number of free non fixed next tiles
+       var frees = 0
+
+       # is the tile editable (metal block)
+       var fixed = false
+
+       redef fun to_s
+       do
+               var s
+               if fixed then
+                       s = "#ABCDEFGHI"
+               else
+                       s = ".abcdefghi"
+               end
+               return "\{{x},{y}:{s.chars[kind]}\}"
+       end
+
+       # Shape for metal block
+       var shape: nullable Object = null
+
+       # Flag for `count_chain` computation.
+       private var chain_mark = 0
+
+       # Set a new kind of monster on tile
+       # Return true is the move made the grid unsolvable (bad move)
+       fun update(nkind: Int): Bool
+       do
+               var t = self
+               var g = self.grid
+               var res = false
+               var okind = t.kind
+               if okind == nkind then return false
+
+
+               # First, remove it and update info.
+               if okind > 0 then
+                       var m = g.monsters[okind]
+                       var n = t.nexts
+                       if n > 2 then
+                               g.error -= 1
+                               m.angry -= 1
+                       else if n == 1 then
+                               m.single -= 1
+                       else if n == 0 then
+                               g.error -= 1
+                               m.lonely -= 1
+                       end
+                       m.number -= 1
+                       g.number -= 1
+               end
+               t.nexts = 0
+               t.blink = 5
+               t.frees = 0
+
+               var a_neigbor: nullable Tile = null
+               # update neighbors
+               for i in [-1..1] do
+                       for j in [-1..1] do
+                               if (i==0 and j==0) or (i!=0 and j!=0) then continue
+                               var t2 = g.get(t.x+i,t.y+j)
+                               if t2 == null then continue
+                               if t2.kind == 0 then
+                                       if not t2.fixed then t.frees += 1
+                                       continue
+                               end
+                               var m = g.monsters[t2.kind]
+
+                               if t2.kind == okind then
+                                       if a_neigbor == null then a_neigbor = t2
+                                       # same than old, thus dec neighbors
+                                       t2.nexts -=1
+                                       var n = t2.nexts
+                                       if n == 2 then
+                                               g.error -= 1
+                                               m.angry -= 1
+                                       else if n == 1 then
+                                               m.single += 1
+                                               g.error += 1
+                                       else if n == 0 then
+                                               m.single -= 1
+                                               m.lonely += 1
+                                       end
+                                       # print "+ {t} one less next: {t2} ; +({i}x{j})"
+                               end
+
+                               if t2.kind == nkind then
+                                       # Same than new, thus inc neighbors
+                                       t2.nexts += 1
+                                       t.nexts += 1
+                                       var n = t2.nexts
+                                       if n > 3 then
+                                               res = true
+                                       else if n == 3 then
+                                               g.error += 1
+                                               m.angry += 1
+                                               res = true
+                                       else if n == 2 then
+                                               m.single -= 1
+                                               g.error -= 1
+                                       else if n == 1 then
+                                               m.single += 1
+                                               m.lonely -= 1
+                                       end
+                                       # print "+ {t} one more next: {t2}"
+                               end
+                       end
+               end
+
+               # Add and update neighbors info
+               t.kind = nkind
+               if nkind > 0 then
+                       var m = g.monsters[nkind]
+                       var n = t.nexts
+                       if n > 2 then
+                               g.error += 1
+                               m.angry += 1
+                       else if n == 1 then
+                               m.single += 1
+                               g.error += 1
+                       else if n == 0 then
+                               g.error += 1
+                               m.lonely += 1
+                       end
+                       m.number+=1
+                       g.number+=1
+
+                       g.check_chain(nkind, t)
+               end
+
+               # check if the old kind broke, or create a chain
+               if okind > 0 then
+                       g.check_chain(okind, a_neigbor)
+               end
+
+               # update win status
+               g.won = true
+               for m in g.monsters do
+                       if m.number > 0 and not m.chain then g.won = false
+               end
+
+               #grid.check_grid
+
+               return res
+       end
+end
+
diff --git a/contrib/friendz/src/level.nit b/contrib/friendz/src/level.nit
new file mode 100644 (file)
index 0000000..d459875
--- /dev/null
@@ -0,0 +1,190 @@
+# Monsterz - Chains of Friends
+#
+# 2010-2014 (c) Jean Privat <jean@pryen.org>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the Do What The Fuck You Want To
+# Public License, Version 2, as published by Sam Hocevar. See
+# http://sam.zoy.org/projects/COPYING.WTFPL for more details.
+
+# Level managment
+module level
+
+import grid
+
+# A level in the game
+class Level
+       # The associated game
+       var game: Game
+
+       init(game: Game, i: Int, code: String)
+       do
+               self.game = game
+               var ls = code.split(";")
+               self.number = i
+               self.str = ls[0]
+               self.par = ls[1].to_i
+               if ls.length >= 3 then
+                       self.status = ls[2]
+               end
+               self.is_tutorial = i<=4
+               self.is_challenge = i>=25
+               if self.is_tutorial then
+                       self.name = "T{i+1}"
+                       self.fullname = "Tutorial level {i+1}"
+               else if not self.is_challenge then
+                       self.name = (i-4).to_s
+                       self.fullname = "Level {i-4}"
+               else
+                       self.name = "C{i-24}"
+                       self.fullname = "Challenge level {i-24}"
+               end
+       end
+
+       # number the level (0 for first)
+       var number: Int
+
+       # initial grid position
+       var str: String
+
+       # top score
+       var par: Int
+
+       # Help message if any
+       var status: String = ""
+
+       # Is the level a tutorial level?
+       var is_tutorial: Bool
+
+       # Is the level a challenge level?
+       var is_challenge: Bool
+
+       # The short name of the level (eg. "T1")
+       var name: String
+
+       # The long name of the level (eg. "Tutorial level 1")
+       var fullname: String
+
+       # The player best wining score.
+       # 0 it not yet won
+       var score = 0
+
+       var l_disabled = 1
+       var l_open = 2
+       var l_won = 3
+       var l_par = 4
+
+       fun get_state: Int
+       do
+               if self.score == 0 then
+                       if self.number == 0 or game.levels[self.number-1].score > 0 then return l_open
+                       if self.number == 25 and game.levels[19].score > 0 then return l_open else return l_disabled
+               else if self.score < self.par or not game.levels[9].score > 0 then
+                       return l_won
+               else return l_par
+       end
+
+       # Returns true if g is a wining condition for the level.
+       fun check_won(g: Grid): Bool
+       do
+               var w = g.won and (not self.is_challenge or g.number >= self.par)
+               if not w then return false
+               if g.number > self.score then
+                       self.score = g.number
+                       self.save
+               end
+               return true
+       end
+
+       fun save
+       do
+               #save_cookie("s"+self.hash, self.score>0?self.score:"")
+       end
+end
+
+# main game object
+class Game
+       # Game version
+       var version = "1.99"
+
+       # Names of kind of monsters @constant
+       var colors: Array[nullable String] = [null, "Red", "Cyan", "Green", "Purple", "Yellow", "Orange", "Blue", "Pink", "White"]
+
+       # max grid width
+       var gw = 16
+
+       # max grid height
+       var gh = 16
+
+       # Number of monster kinds (+1 for empty) @constant
+       var monsters = 9
+
+       # The grid to play on
+       var grid = new Grid(gw, gh, monsters)
+
+       # LEVELS ******************************************************************
+
+       # Raw level description
+       var levels_code: Array[String] = [
+       ".3#|A2.A|3#.|;6;Connect two monsters",
+       ".#.|A.#|#.A|;5;Diagonals do not count",
+       "2.A|A#.|2.A|;8;Connect all monsters",
+       "3A.|3.A|2.A.|;10;Only 1 or 2 neighbors",
+       "2.A.|.B.B|2.A.|;12;Build two chains",
+
+       "A.A#|4.|#.#.|A3.|;11",
+       "A2.#A|5.|2.A2.|4.#|4.A|;17",
+       "A2.B2.|.B2.A.|6.|.AB2.B|6.|6.|;36",
+       "A5.|B5.|#.A3.|6.|2.A.B.|.A4.|;26",
+       "8.|BA2.B.A.|2.C.C.B.|6.#.|;29",
+
+       ".B3.B|6.|.#3.#|.C3.B|.#4.|6.|.C3.C|;30",
+       ".A.#4.|.C3.A.E|.A6.|6.C.|8.|2.6#|6.E#|5.C2#|;54",
+       ".A5.A|.A.#3.A|2.#A.#.#|.#.#4.|2.A3.#.|8.|4.A2.A|2.A2.A.#|;37",
+       "AB3.C|2.AC2.|6.|.B.CB.|6.|;30",
+       "AC5#CA|2.#3.#2.|4.#4.|7.#.|5.#.#.|9.|2.G6.|6.2#.|9.|H#F#G#F#H|;72",
+
+       "AG3.C|2.AC2.|6.|.G2.G.|2.C3.|6.|;32",
+       "7.#2.|3.#.#4.|.#8.|7.D2.|2.#.3#3.|2.#3.2#FC|9..|2.2#3.FCD|9..|;71",
+       "2.#A4.|6.B.|.C6.|.A.A.AC.|#7.|5.B2.|8.|#A5.C|;56",
+       "9.3.|8.#3.|2.B.#3.BA2.|2.AC4.DC2.|2.EA8.|8.#3.|.2#3.#5.|7.#4.|2#3.#B5.|5.CD5.|.2#9.|3.E8.|;119",
+       "9.7.|.2#B8#E2#.|.E8.#3.#.|.#3.4#.#.#.#.|.#.#.#4.#.#.#.|.C.#3.2#.#.#.#.|.#.#.4#3.#.#.|.D8.3#.#.|.#.#.4#5.A.|.A5.#2.#.3#.|.3#.#.2#.#2.2#.|.D3.#4.2#2.#.|.#.3#.#.4#.#.|.#5.#6.#.|.3#C6#B3#.|9.7.|;130",
+
+       "8.|.#C2.C#.|.#E2.E#.|8.|8.|.B#2.#A.|.#C2.C#.|8.|8.|.BF2.FA.|.2#2.2#.|8.|8.|.2#2.2#.|.A#2.B#.|8.|;92",
+       ".A9..|D9.F.|7.CA3.|3.E8.|9.2.H|9.2.#|7.G4.|4.BG2.F3.|2.B6.E2.|9.G2.|.H8.C.|8.D3.|;140",
+       "2.F9..|2.C9..|9..C2.|8.AG3.|9.4.|4.2#5.F.|7.E4.#|3.A9.|.G.CF4.C.#.|9.2.E.|;125",
+       "9.HCB|.DH9.|.GC9.|9.3.|4.G7.|3.#8.|9.3.|3.B4.D3.|.G9..|8.#3.|2.C2.2#5.|9.3.|;135",
+       "9.6.A|6.C9.|2.A.B4.E3.#I.|9.I.#4.|9.7.|2.E3.C9.|7.B8.|5.G6.G3.|6.C6.G2.|9..H3.B.|.CH.#5.#2.C2.|4.H3.#3.H3.|.B3.F5.F4.|.D.D4.D5.D.|9.7.|6.C9.|;244",
+
+       "E6.|7.|.F.E.F.|.F.#.E.|.F.E.E.|7.|6.F|;48",
+       "3.2#3.|2.A2.A2.|.C4.C.|8.|3.2#3.|2.A2.A2.|.C4.C.|8.|;56",
+       "3.#3.A4.|5.#6.|3.#5.#2.|.#4.#4.#|4.#7.|#6.#4.|8.#3.|.#9..|.#3.#4.#.|3.2#3.#3.|;74",
+       "2.A7.|2.D7.|2.A4.A2.|2.G4.G2.|2.A4.D2.|2.D7.|2.A7.|2.G7.|2.D7.|2.G7.|;95",
+       "E9.6.|9.H4.#.|9.E2.#3.|9.F2.#2.#|9.3.H2.#|HEF9.3.#|9.7.|6.#9.|6.#2.H3.HEF|.6#2.E6.|9.F6.|5#9.2.|4.#9.#.|.F#3.7#.#.|9.5.#.|9.7.|;225"
+       ]
+
+       # The loaded levels
+       var levels = new Array[Level]
+
+       # Load levels
+       # used duting `init`
+       fun load_levels
+       do
+               # Transform level strings into level objects. */
+               for i in [0..levels_code.length[ do
+                       var l = new Level(self,i, levels_code[i])
+                       levels[i] = l
+                       #var v = read_cookie("s"+l.hash)
+                       #l = v
+               end
+       end
+
+       # The current played level (if any)
+       var level: nullable Level = null
+
+       init
+       do
+               load_levels
+       end
+end
+
diff --git a/contrib/friendz/src/solver.nit b/contrib/friendz/src/solver.nit
new file mode 100644 (file)
index 0000000..5b80fd4
--- /dev/null
@@ -0,0 +1,230 @@
+# Monsterz - Chains of Friends
+#
+# 2010-2014 (c) Jean Privat <jean@pryen.org>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the Do What The Fuck You Want To
+# Public License, Version 2, as published by Sam Hocevar. See
+# http://sam.zoy.org/projects/COPYING.WTFPL for more details.
+
+# Basic game solver
+module solver
+intrude import grid
+
+# A solver with a deep-first search algorithm
+# Instead of storig grids in memory, it plays by placing monsters and backtrack by removing placed monsters.
+class Solver
+       # Return true if grid accepts kind at tile t.
+       # Accepts means that
+       # * the tile is playble (free and not fixed)
+       # * the new monster will not be angry
+       # * the new monster will not make neighbor angry
+       fun accepts(t: Tile, kind: Int): Bool
+       do
+               var grid = t.grid
+               if t.kind != 0 or t.fixed then return false
+               #if t.x<0 or t.x>=grid.width or t.y<0 or t.y>=grid.height then return false
+               var cpt = 0
+               for i in [-1..1] do
+                       for j in [-1..1] do
+                               if i==0 and j==0 or i!=0 and j!=0 then continue
+                               var x2 = t.x+i
+                               var y2 = t.y+j
+                               if x2<0 or x2>=grid.width or y2<0 or y2>=grid.height then continue
+                               var t2 = grid.grid[x2][y2]
+                               if t2.kind == kind then
+                                       if t2.nexts >= 2 then return false
+                                       cpt += 1
+                                       if cpt>=3 then return false
+                               end
+                       end
+               end
+               return true
+       end
+
+       # Return the list of accepting neighbors of t
+       fun frees(t: Tile): Array[Tile]
+       do
+               var grid = t.grid
+               var res = new Array[Tile]
+               for i in [-1..1] do
+                       for j in [-1..1] do
+                               if i==0 and j==0 or i!=0 and j!=0 then continue
+                               var x2 = t.x+i
+                               var y2 = t.y+j
+                               if x2<0 or x2>=grid.width or y2<0 or y2>=grid.height then continue
+                               var t2 = grid.grid[x2][y2]
+                               if accepts(t2, t.kind) then res.push(t2)
+                       end
+               end
+               return res
+       end
+
+       # The grid played on
+       var grid: Grid
+
+       # Open moves to play
+       # If empty, it means the next move will be backtraking
+       var tries = new Array[Tile]
+
+       # The color played for `tries`
+       var kind = 1
+
+       # Search free tiles next to lonely monsters
+       # Used to initialize `tries`
+       fun look_start: Array[Tile]
+       do
+               var start = new Array[Tile]
+               var min = 1
+               kind = 0
+
+               var tile: nullable Tile = null
+
+               for x in [0..grid.width[ do
+                       for y in [0..grid.height[ do
+                               var t = grid.grid[x][y]
+                               var k = t.kind
+                               if k == 0 or grid.monsters[k].chain then continue
+                               var n = t.nexts
+
+                               if n > min then continue
+
+                               var fs = frees(t)
+                               var l = fs.length
+                               if l == 0 then continue
+
+                               tile = t
+                               if n == 0 then
+                                       if min == 1 or start.length > l then
+                                               start = fs
+                                               kind = k
+                                       end
+                               else if kind == 0 or kind == k then
+                                       start.add_all fs
+                                       kind = k
+                               end
+
+                               min = n
+                       end
+               end
+               #print "-------------"
+               #print grid
+               #dump
+               #print "START: {tile} -> {start.join(",")}"
+               #print "-------------"
+               return start
+       end
+
+       # compute and print some metrics about the problem
+       fun size_problem
+       do
+               var free = 0
+               for x in [0..grid.width[ do
+                       for y in [0..grid.height[ do
+                               var t = grid.grid[x][y]
+                               if t.kind == 0 and not t.fixed then free += 1
+                       end
+               end
+               print "FREE: {free}"
+               var ms = 0
+               for m in grid.monsters do
+                       if m.number > 0 then ms += 1
+               end
+               print "KINDS: {ms}"
+               print "SIZE: {(ms+1).to_f.pow(free.to_f)}"
+       end
+
+       init(grid: Grid)
+       do
+               self.grid = grid
+               size_problem
+               tries = look_start
+       end
+
+       # Zipper in the search tree as a FIFO
+       var history = new Array[Move]
+
+       # Perform a backtrack step
+       # Remove the placed monsters and go for the other tries
+       fun backtrack: Bool
+       do
+               if history.is_empty then
+                       is_over = true
+                       return true
+               end
+               var h = history.pop
+               tries = h.tries
+               opens -= tries.length
+               h.tile.update(0)
+               kind = h.kind
+               return false
+       end
+
+       # Number of player steps
+       var steps = 0
+
+       # Number of open in the
+       # real opens is `opens+tries.length`
+       var opens = 0
+
+       fun dump
+       do
+               print "STEPS: {steps}"
+               print "OPENS: {opens+tries.length}"
+               print "DEPTH: {history.length}"
+               print "NEXTS: {tries.join(", ")}"
+       end
+
+       # Is the last step exhausted all the possibilities?
+       var is_over = false
+
+       # Compute the next step.
+       # Return tru on a wining position (`grid.won`) or when the solver `is_over`
+       fun step: Bool
+       do
+               #print "=========="
+               #print grid
+               #dump
+               #print "=========="
+               steps += 1
+               if tries.is_empty then
+                       return backtrack
+               end
+               var t = tries.pop
+               var eval = 0
+               var fail = t.update(kind)
+               if not fail then
+                       #eval = evaluate_new_tile(t)
+                       #fail = (eval == -1)
+               end
+               if not fail then
+                       opens += tries.length
+                       var h = new Move(t, tries, kind)
+                       history.push(h)
+                       if grid.won then
+                               tries = new Array[Tile]
+                               return true
+                       end
+                       if t.nexts == 2 then
+                               tries = look_start
+                       else
+                               tries = frees(t)
+                       end
+               else
+                       t.update(0)
+               end
+               return false
+       end
+end
+
+# A stored move in the `Solver::history`
+class Move
+       # The tile played
+       var tile: Tile
+
+       # The remainig alternatives to try
+       var tries: Array[Tile]
+
+       # to color for the moves
+       var kind: Int
+end
index eff6c9d..67896c0 100644 (file)
@@ -7,3 +7,4 @@ shoot_linux
 websocket_server
 converter
 mnit_linux
+friendz_linux
diff --git a/tests/sav/friendz.res b/tests/sav/friendz.res
new file mode 100644 (file)
index 0000000..6f89576
--- /dev/null
@@ -0,0 +1 @@
+Runtime error: Abstract method `generate_input` called on `App` (../lib/mnit/mnit_app.nit:63)
diff --git a/tests/sav/friendz_android.res b/tests/sav/friendz_android.res
new file mode 100644 (file)
index 0000000..174d681
--- /dev/null
@@ -0,0 +1 @@
+Not executable (platform?)