summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.cvsignore1
-rw-r--r--.gitignore3
-rw-r--r--BubBob.icnsbin0 -> 42521 bytes
-rwxr-xr-xBubBob.py164
-rw-r--r--INSTALL.txt63
-rw-r--r--LICENSE.txt33
-rw-r--r--Makefile63
-rw-r--r--artistic.txt73
-rw-r--r--bubbob/.cvsignore4
-rw-r--r--bubbob/Makefile2
-rwxr-xr-xbubbob/bb.py345
-rw-r--r--bubbob/binboards.py122
-rw-r--r--bubbob/boarddef.py69
-rw-r--r--bubbob/boards.py1470
-rw-r--r--bubbob/bonuses.py2504
-rw-r--r--bubbob/bubbles.py1284
-rw-r--r--bubbob/command.py10
-rw-r--r--bubbob/doc/.cvsignore1
-rwxr-xr-xbubbob/doc/bonus-doc.py170
-rw-r--r--bubbob/doc/images/.cvsignore1
-rw-r--r--bubbob/ext1/.cvsignore2
-rw-r--r--bubbob/ext1/__init__.py403
-rw-r--r--bubbob/ext1/brick.wavbin0 -> 29150 bytes
-rw-r--r--bubbob/ext1/image1-0.ppmbin0 -> 7732 bytes
-rw-r--r--bubbob/ext1/music.wavbin0 -> 748826 bytes
-rw-r--r--bubbob/ext1/wall.wavbin0 -> 13274 bytes
-rw-r--r--bubbob/ext2/.cvsignore1
-rw-r--r--bubbob/ext2/__init__.py578
-rw-r--r--bubbob/ext2/image1.ppmbin0 -> 24635 bytes
-rw-r--r--bubbob/ext2/image2.ppmbin0 -> 3898 bytes
-rw-r--r--bubbob/ext2/music.wavbin0 -> 353000 bytes
-rw-r--r--bubbob/ext3/.cvsignore2
-rw-r--r--bubbob/ext3/__init__.py367
-rw-r--r--bubbob/ext3/image1-0.ppmbin0 -> 3124 bytes
-rw-r--r--bubbob/ext3/music.wavbin0 -> 284134 bytes
-rw-r--r--bubbob/ext3/shoot.wavbin0 -> 13274 bytes
-rw-r--r--bubbob/ext4/.cvsignore2
-rw-r--r--bubbob/ext4/__init__.py440
-rw-r--r--bubbob/ext4/image1-0.ppmbin0 -> 1588 bytes
-rw-r--r--bubbob/ext4/music.wavbin0 -> 566532 bytes
-rw-r--r--bubbob/ext5/.cvsignore1
-rw-r--r--bubbob/ext5/__init__.py289
-rw-r--r--bubbob/ext5/image1.ppm5
-rw-r--r--bubbob/ext5/image2.ppm5
-rw-r--r--bubbob/ext5/image3.ppm5
-rw-r--r--bubbob/ext5/image4.ppmbin0 -> 9274 bytes
-rw-r--r--bubbob/ext5/music.wavbin0 -> 346458 bytes
-rw-r--r--bubbob/ext5/ouch.wavbin0 -> 2631 bytes
-rw-r--r--bubbob/ext6/.cvsignore2
-rw-r--r--bubbob/ext6/__init__.py268
-rw-r--r--bubbob/ext6/crash.wavbin0 -> 13123 bytes
-rw-r--r--bubbob/ext6/image1-0.ppmbin0 -> 1971 bytes
-rw-r--r--bubbob/ext6/music.wavbin0 -> 431045 bytes
-rw-r--r--bubbob/ext7/.cvsignore2
-rw-r--r--bubbob/ext7/__init__.py399
-rw-r--r--bubbob/ext7/fire.wavbin0 -> 3352 bytes
-rw-r--r--bubbob/ext7/hit.wavbin0 -> 3352 bytes
-rw-r--r--bubbob/ext7/image1-0.ppmbin0 -> 93365 bytes
-rw-r--r--bubbob/ext7/music.wavbin0 -> 379900 bytes
-rw-r--r--bubbob/images.py542
-rw-r--r--bubbob/images/.cvsignore7
-rw-r--r--bubbob/images/10000_0.ppmbin0 -> 10852 bytes
-rw-r--r--bubbob/images/20000_0.ppmbin0 -> 13282 bytes
-rw-r--r--bubbob/images/30000_0.ppmbin0 -> 13282 bytes
-rw-r--r--bubbob/images/40000_0.ppmbin0 -> 13282 bytes
-rw-r--r--bubbob/images/50000_0.ppmbin0 -> 12877 bytes
-rw-r--r--bubbob/images/60000_0.ppmbin0 -> 13012 bytes
-rw-r--r--bubbob/images/70000_0.ppmbin0 -> 13012 bytes
-rw-r--r--bubbob/images/big_bubble.ppm4
-rw-r--r--bubbob/images/big_bubble_2.ppm4
-rw-r--r--bubbob/images/black.ppm5
-rw-r--r--bubbob/images/blitzy.ppmbin0 -> 33845 bytes
-rw-r--r--bubbob/images/blitzy_angry.ppmbin0 -> 12341 bytes
-rw-r--r--bubbob/images/blitzy_shot.ppmbin0 -> 1588 bytes
-rw-r--r--bubbob/images/bonus_0.ppmbin0 -> 17339 bytes
-rw-r--r--bubbob/images/bonus_1.ppmbin0 -> 24629 bytes
-rw-r--r--bubbob/images/bonus_10.ppmbin0 -> 24629 bytes
-rw-r--r--bubbob/images/bonus_11.ppmbin0 -> 24629 bytes
-rw-r--r--bubbob/images/bonus_12.ppmbin0 -> 12341 bytes
-rw-r--r--bubbob/images/bonus_2.ppmbin0 -> 24629 bytes
-rw-r--r--bubbob/images/bonus_3.ppmbin0 -> 24629 bytes
-rw-r--r--bubbob/images/bonus_4.ppmbin0 -> 24629 bytes
-rw-r--r--bubbob/images/bonus_5.ppmbin0 -> 24629 bytes
-rw-r--r--bubbob/images/bonus_6.ppmbin0 -> 24629 bytes
-rw-r--r--bubbob/images/bonus_7.ppmbin0 -> 24629 bytes
-rw-r--r--bubbob/images/bonus_8.ppmbin0 -> 24629 bytes
-rw-r--r--bubbob/images/bonus_9.ppmbin0 -> 24629 bytes
-rw-r--r--bubbob/images/bubble.ppm5
-rw-r--r--bubbob/images/buildcolors.py324
-rw-r--r--bubbob/images/butterfly.ppmbin0 -> 27701 bytes
-rw-r--r--bubbob/images/cream_pie_big.ppmbin0 -> 24352 bytes
-rw-r--r--bubbob/images/diamond_big_blue.ppmbin0 -> 24352 bytes
-rw-r--r--bubbob/images/diamond_big_purple.ppm5
-rw-r--r--bubbob/images/diamond_big_red.ppmbin0 -> 24352 bytes
-rw-r--r--bubbob/images/diamond_big_yellow.ppmbin0 -> 24352 bytes
-rw-r--r--bubbob/images/digits_0.ppmbin0 -> 7193 bytes
-rw-r--r--bubbob/images/door.ppmbin0 -> 3130 bytes
-rw-r--r--bubbob/images/dragon_0.ppmbin0 -> 61493 bytes
-rw-r--r--bubbob/images/dragon_bubble_0.ppmbin0 -> 49205 bytes
-rw-r--r--bubbob/images/extend.ppmbin0 -> 55355 bytes
-rw-r--r--bubbob/images/extra1.ppmbin0 -> 24629 bytes
-rw-r--r--bubbob/images/extra2.ppmbin0 -> 6203 bytes
-rw-r--r--bubbob/images/extra3.ppmbin0 -> 21518 bytes
-rw-r--r--bubbob/images/extra4.ppmbin0 -> 9229 bytes
-rw-r--r--bubbob/images/extra5.ppm5
-rw-r--r--bubbob/images/extra6.ppmbin0 -> 11374 bytes
-rw-r--r--bubbob/images/extra7.ppm13
-rw-r--r--bubbob/images/extra8.ppmbin0 -> 34259 bytes
-rw-r--r--bubbob/images/fire_drop.ppmbin0 -> 444 bytes
-rw-r--r--bubbob/images/fire_surface.ppmbin0 -> 3085 bytes
-rw-r--r--bubbob/images/fish_0.ppmbin0 -> 21557 bytes
-rw-r--r--bubbob/images/flappy.ppmbin0 -> 46133 bytes
-rw-r--r--bubbob/images/flapy_angry.ppmbin0 -> 24629 bytes
-rw-r--r--bubbob/images/game_over_0.ppmbin0 -> 6196 bytes
-rw-r--r--bubbob/images/ghost.ppm6
-rw-r--r--bubbob/images/ghosty.ppmbin0 -> 46133 bytes
-rw-r--r--bubbob/images/ghosty_angry.ppmbin0 -> 24629 bytes
-rw-r--r--bubbob/images/glue.ppm4
-rw-r--r--bubbob/images/gramy.ppmbin0 -> 58421 bytes
-rw-r--r--bubbob/images/gramy_angry.ppmbin0 -> 24629 bytes
-rw-r--r--bubbob/images/hat1.ppm5
-rw-r--r--bubbob/images/hat2.ppm5
-rw-r--r--bubbob/images/hat5.ppm5
-rw-r--r--bubbob/images/ice_cyan_big.ppmbin0 -> 24358 bytes
-rw-r--r--bubbob/images/ice_violet_big.ppmbin0 -> 24358 bytes
-rw-r--r--bubbob/images/keys.ppmbin0 -> 49167 bytes
-rw-r--r--bubbob/images/level_digits.ppmbin0 -> 25253 bytes
-rw-r--r--bubbob/images/lightning_large.ppmbin0 -> 17833 bytes
-rw-r--r--bubbob/images/lightning_small.ppmbin0 -> 1741 bytes
-rw-r--r--bubbob/images/monky.ppmbin0 -> 61493 bytes
-rw-r--r--bubbob/images/monky_angry.ppmbin0 -> 24629 bytes
-rw-r--r--bubbob/images/nasty.ppmbin0 -> 46133 bytes
-rw-r--r--bubbob/images/nasty_angry.ppmbin0 -> 24629 bytes
-rw-r--r--bubbob/images/orcy.ppmbin0 -> 70709 bytes
-rw-r--r--bubbob/images/orcy_angry.ppmbin0 -> 24629 bytes
-rw-r--r--bubbob/images/palettes.datbin0 -> 28800 bytes
-rw-r--r--bubbob/images/pastec_big.ppm44
-rw-r--r--bubbob/images/peach_big.ppmbin0 -> 24352 bytes
-rw-r--r--bubbob/images/point_0.ppmbin0 -> 86453 bytes
-rw-r--r--bubbob/images/red_Hurry_up.ppmbin0 -> 9851 bytes
-rw-r--r--bubbob/images/sheep.ppmbin0 -> 27707 bytes
-rw-r--r--bubbob/images/shot.ppmbin0 -> 24629 bytes
-rw-r--r--bubbob/images/spinning_drop.ppmbin0 -> 4660 bytes
-rw-r--r--bubbob/images/springy.ppmbin0 -> 58421 bytes
-rw-r--r--bubbob/images/springy_angry.ppmbin0 -> 36917 bytes
-rw-r--r--bubbob/images/star_large.ppmbin0 -> 36923 bytes
-rw-r--r--bubbob/images/sugar_pie_big.ppmbin0 -> 24358 bytes
-rw-r--r--bubbob/images/water_flow.ppm4
-rw-r--r--bubbob/images/water_still.ppm4
-rw-r--r--bubbob/images/water_surface.ppm4
-rw-r--r--bubbob/images/yellow_Hurry_up.ppmbin0 -> 9851 bytes
-rw-r--r--bubbob/levels/Arena.binbin0 -> 164736 bytes
-rw-r--r--bubbob/levels/CompactLevels.py1902
-rw-r--r--bubbob/levels/HouseOfFun.binbin0 -> 196096 bytes
-rw-r--r--bubbob/levels/Levels.binbin0 -> 178432 bytes
-rw-r--r--bubbob/levels/LostLevels.binbin0 -> 178432 bytes
-rw-r--r--bubbob/levels/README.txt23
-rw-r--r--bubbob/levels/RandomLevels.py362
-rw-r--r--bubbob/levels/rnglevel1276
-rw-r--r--bubbob/levels/scratch.py2301
-rw-r--r--bubbob/macbinary.py260
-rw-r--r--bubbob/mnstrmap.py397
-rw-r--r--bubbob/monsters.py937
-rw-r--r--bubbob/music/Snd1-8.wavbin0 -> 532268 bytes
-rw-r--r--bubbob/music/Snd2-8.wavbin0 -> 10778156 bytes
-rw-r--r--bubbob/music/Snd3-8.wavbin0 -> 1890476 bytes
-rw-r--r--bubbob/music/Snd4-8.wavbin0 -> 339884 bytes
-rw-r--r--bubbob/music/Snd5-8.wavbin0 -> 503468 bytes
-rw-r--r--bubbob/music/Snd6-8.wavbin0 -> 9895724 bytes
-rw-r--r--bubbob/patmap.py177
-rw-r--r--bubbob/player.py1213
-rw-r--r--bubbob/ranking.py391
-rw-r--r--bubbob/save_rnglevel.py125
-rwxr-xr-xbubbob/setup.py22
-rw-r--r--bubbob/sounds/die.wavbin0 -> 29758 bytes
-rw-r--r--bubbob/sounds/extra.wavbin0 -> 3084 bytes
-rw-r--r--bubbob/sounds/extralife.wavbin0 -> 20624 bytes
-rw-r--r--bubbob/sounds/fruit.wavbin0 -> 3300 bytes
-rw-r--r--bubbob/sounds/hell.wavbin0 -> 41036 bytes
-rw-r--r--bubbob/sounds/hurry.wavbin0 -> 19536 bytes
-rw-r--r--bubbob/sounds/jump.wavbin0 -> 5516 bytes
-rw-r--r--bubbob/sounds/letsgo.wavbin0 -> 19007 bytes
-rw-r--r--bubbob/sounds/pop.wavbin0 -> 2837 bytes
-rw-r--r--bubbob/sounds/shh.wavbin0 -> 18272 bytes
-rw-r--r--bubbob/sounds/yippee.wavbin0 -> 6305 bytes
-rw-r--r--bubbob/sprmap.py533
-rw-r--r--bubbob/statesaver.c610
-rw-r--r--bubbob/statesaver.py135
-rwxr-xr-xbubbob/statesaver.sobin0 -> 62672 bytes
-rw-r--r--bubbob/test_rnglevel.py64
-rw-r--r--bubbob/test_statesaver.py127
-rw-r--r--bubbob/tmp/pat00.ppmbin0 -> 17294 bytes
-rw-r--r--bubbob/tmp/pat01.ppmbin0 -> 17294 bytes
-rw-r--r--bubbob/tmp/pat02.ppmbin0 -> 17294 bytes
-rw-r--r--bubbob/tmp/pat03.ppmbin0 -> 17294 bytes
-rw-r--r--bubbob/tmp/pat04.ppmbin0 -> 17294 bytes
-rw-r--r--bubbob/tmp/pat05.ppmbin0 -> 17294 bytes
-rw-r--r--bubbob/tmp/pat06.ppmbin0 -> 17294 bytes
-rw-r--r--bubbob/tmp/pat07.ppmbin0 -> 17294 bytes
-rw-r--r--bubbob/tmp/pat08.ppmbin0 -> 17294 bytes
-rw-r--r--bubbob/tmp/pat09.ppmbin0 -> 17294 bytes
-rw-r--r--bubbob/tmp/pat10.ppmbin0 -> 6925 bytes
-rw-r--r--bubbob/tmp/pat11.ppmbin0 -> 30734 bytes
-rw-r--r--bubbob/tmp/pat12.ppmbin0 -> 30734 bytes
-rw-r--r--bubbob/tmp/pat13.ppmbin0 -> 30734 bytes
-rw-r--r--bubbob/tmp/pat14.ppmbin0 -> 30734 bytes
-rw-r--r--bubbob/tmp/pat15.ppmbin0 -> 30734 bytes
-rw-r--r--bubbob/tmp/pat16.ppm4
-rw-r--r--bubbob/tmp/pat17.ppmbin0 -> 30734 bytes
-rw-r--r--bubbob/tmp/pat18.ppmbin0 -> 30734 bytes
-rw-r--r--bubbob/tmp/pat19.ppmbin0 -> 30734 bytes
-rw-r--r--bubbob/tmp/pat20.ppmbin0 -> 7693 bytes
-rw-r--r--common/.cvsignore1
-rw-r--r--common/__init__.py1
-rw-r--r--common/gamesrv.py1378
-rw-r--r--common/hostchooser.py192
-rw-r--r--common/httpserver.py192
-rw-r--r--common/javaserver.py157
-rw-r--r--common/msgstruct.py75
-rw-r--r--common/pixmap.py163
-rw-r--r--common/stdlog.py106
-rw-r--r--common/udpovertcp.py46
-rw-r--r--display/.cvsignore4
-rw-r--r--display/Client.py170
-rw-r--r--display/Makefile2
-rw-r--r--display/__init__.py1
-rw-r--r--display/caching.py261
-rw-r--r--display/dpy_gtk.py204
-rw-r--r--display/dpy_pygame.py245
-rwxr-xr-xdisplay/dpy_windows.py23
-rw-r--r--display/dpy_x.py40
-rw-r--r--display/modes.py196
-rw-r--r--display/music1.py33
-rw-r--r--display/pclient.py860
-rw-r--r--display/playback.py203
-rw-r--r--display/puremixer.py123
-rw-r--r--display/pythonxlibintf.py202
-rwxr-xr-xdisplay/setup.py16
-rw-r--r--display/snd_linux.py148
-rw-r--r--display/snd_off.py5
-rw-r--r--display/snd_pygame.py82
-rw-r--r--display/snd_windows.py93
-rw-r--r--display/windows/.cvsignore3
-rwxr-xr-xdisplay/windows/wingame.c867
-rwxr-xr-xdisplay/windows/wingame.def2
-rwxr-xr-xdisplay/windows/wingame.dsp111
-rwxr-xr-xdisplay/windows/wingame.dsw29
-rw-r--r--display/xshm.c1202
-rwxr-xr-xdisplay/xshm.sobin0 -> 112912 bytes
-rw-r--r--doc/BubBob.py.132
-rw-r--r--doc/Client.py.1200
-rw-r--r--doc/Makefile30
-rw-r--r--doc/bb.py.1129
-rw-r--r--http2/.cvsignore2
-rw-r--r--http2/config.txt1
-rw-r--r--http2/data/bab.pngbin0 -> 1902 bytes
-rw-r--r--http2/data/baub.pngbin0 -> 1736 bytes
-rw-r--r--http2/data/beab.pngbin0 -> 1677 bytes
-rw-r--r--http2/data/beb.pngbin0 -> 1587 bytes
-rw-r--r--http2/data/biab.pngbin0 -> 1679 bytes
-rw-r--r--http2/data/bib.pngbin0 -> 1655 bytes
-rw-r--r--http2/data/biob.pngbin0 -> 1531 bytes
-rw-r--r--http2/data/bob.pngbin0 -> 1682 bytes
-rw-r--r--http2/data/boob.pngbin0 -> 1617 bytes
-rw-r--r--http2/data/bub.pngbin0 -> 1490 bytes
-rw-r--r--http2/data/byb.pngbin0 -> 2592 bytes
-rw-r--r--http2/data/checked.pngbin0 -> 1032 bytes
-rw-r--r--http2/data/close.pngbin0 -> 478 bytes
-rw-r--r--http2/data/confirm.html34
-rw-r--r--http2/data/disabled.pngbin0 -> 814 bytes
-rw-r--r--http2/data/hat1.pngbin0 -> 202 bytes
-rw-r--r--http2/data/hat2.pngbin0 -> 310 bytes
-rw-r--r--http2/data/header.pngbin0 -> 11619 bytes
-rw-r--r--http2/data/index.html292
-rw-r--r--http2/data/lbab.pngbin0 -> 2592 bytes
-rw-r--r--http2/data/lbeb.pngbin0 -> 1583 bytes
-rw-r--r--http2/data/lbib.pngbin0 -> 1663 bytes
-rw-r--r--http2/data/lbiob.pngbin0 -> 1520 bytes
-rw-r--r--http2/data/name.html189
-rw-r--r--http2/data/new.html271
-rw-r--r--http2/data/options.html285
-rw-r--r--http2/data/sfbob.pngbin0 -> 1785 bytes
-rw-r--r--http2/data/sfbub.pngbin0 -> 1664 bytes
-rw-r--r--http2/data/stop.html101
-rw-r--r--http2/data/unchecked.pngbin0 -> 327 bytes
-rw-r--r--http2/data/wave1.pngbin0 -> 307 bytes
-rw-r--r--http2/data/wave2.pngbin0 -> 327 bytes
-rw-r--r--http2/data/wave3.pngbin0 -> 313 bytes
-rw-r--r--http2/header.pngbin0 -> 11619 bytes
-rw-r--r--http2/httppages.py705
-rwxr-xr-xhttp2/sf/bb12.py274
-rw-r--r--http2/sf/sfbub.pngbin0 -> 1664 bytes
-rw-r--r--http2/sf/started.html44
-rw-r--r--java/.cvsignore1
-rw-r--r--java/Makefile2
-rw-r--r--java/pclient.java1196
-rw-r--r--metaserver/.cvsignore4
-rw-r--r--metaserver/__init__.py1
-rw-r--r--metaserver/home.pngbin0 -> 1283 bytes
-rw-r--r--metaserver/index.html51
-rw-r--r--metaserver/mbub.pngbin0 -> 1138 bytes
-rw-r--r--metaserver/metaclient.py596
-rw-r--r--metaserver/metaserver.py365
-rw-r--r--metaserver/metastruct.py75
-rw-r--r--metaserver/pipelayer.py337
-rw-r--r--metaserver/socketoverudp.py174
306 files changed, 33834 insertions, 0 deletions
diff --git a/.cvsignore b/.cvsignore
new file mode 100644
index 0000000..ccabf62
--- /dev/null
+++ b/.cvsignore
@@ -0,0 +1 @@
+BubBob.log
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8cbc609
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+CVS/
+*~
+*.py[co]
diff --git a/BubBob.icns b/BubBob.icns
new file mode 100644
index 0000000..4f34b4b
--- /dev/null
+++ b/BubBob.icns
Binary files differ
diff --git a/BubBob.py b/BubBob.py
new file mode 100755
index 0000000..52820c3
--- /dev/null
+++ b/BubBob.py
@@ -0,0 +1,164 @@
+#! /usr/bin/env python
+
+#
+# This script is used to start the server.
+# For command-line usage please run
+#
+# python bubbob/bb.py --help
+#
+
+# __________
+import os, sys
+print 'Running on Python', sys.version
+if __name__ == '__main__':
+ LOCALDIR = sys.argv[0]
+else:
+ LOCALDIR = __file__
+try:
+ LOCALDIR = os.readlink(LOCALDIR)
+except:
+ pass
+sys.argv[0] = os.path.abspath(LOCALDIR)
+LOCALDIR = os.path.dirname(sys.argv[0])
+# ----------
+
+import socket, tempfile
+
+sys.path.insert(0, LOCALDIR)
+os.chdir(LOCALDIR)
+
+try:
+ username = '-'+os.getlogin()
+except:
+ try:
+ import pwd
+ username = '-'+pwd.getpwuid(os.getuid())[0]
+ except:
+ username = ''
+TAGFILENAME = 'BubBob-%s%s.url' % (socket.gethostname(), username)
+TAGFILENAME = os.path.join(tempfile.gettempdir(), TAGFILENAME)
+
+
+def load_url_file():
+ try:
+ url = open(TAGFILENAME, 'r').readline().strip()
+ except (OSError, IOError):
+ return None, None
+ if not url.startswith('http://127.0.0.1:'):
+ return None, None
+ url1 = url[len('http://127.0.0.1:'):]
+ try:
+ port = int(url1[:url1.index('/')])
+ except ValueError:
+ return None, None
+ return url, port
+
+def look_for_local_server():
+ # Look for a running local web server
+ url, port = load_url_file()
+ if port is None:
+ return None
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ try:
+ s.connect(('127.0.0.1', port))
+ except socket.error, e:
+ return None
+ try:
+ s.shutdown(2)
+ s.close()
+ except Exception, e:
+ pass
+ url2, port2 = load_url_file()
+ if port2 != port:
+ return None
+ return url2
+
+def start_local_server():
+ MAINSCRIPT = os.path.join('bubbob', 'bb.py')
+ has_server = os.path.exists(MAINSCRIPT)
+ if hasattr(os, 'fork') and hasattr(os, 'dup2'):
+ if os.fork() == 0:
+ # in the child process
+ if has_server:
+ sys.path.insert(0, os.path.join(LOCALDIR, 'bubbob'))
+ import bb
+ bb.BubBobGame.Quiet = 1
+ else:
+ sys.path.insert(0, os.path.join(LOCALDIR, 'http2'))
+ import httppages
+ import gamesrv, stdlog
+ logfile = stdlog.LogFile()
+ if has_server:
+ bb.start_metaserver(TAGFILENAME, 0)
+ else:
+ httppages.main(None, TAGFILENAME, 0)
+ if logfile:
+ print >> logfile
+ if logfile:
+ print "Logging to", logfile.filename
+ fd = logfile.f.fileno()
+ try:
+ # detach from parent
+ os.dup2(fd, 1)
+ os.dup2(fd, 2)
+ os.dup2(fd, 0)
+ except OSError:
+ pass
+ logfile.close()
+ gamesrv.mainloop()
+ sys.exit(0)
+ else:
+ if not has_server:
+ MAINSCRIPT = os.path.join('http2', 'httppages.py')
+ args = [sys.executable, MAINSCRIPT, '--quiet',
+ '--saveurlto=%s' % TAGFILENAME]
+ # (quoting sucks on Windows) ** 42
+ if sys.platform == 'win32':
+ args[0] = '"%s"' % (args[0],)
+ os.spawnv(os.P_NOWAITO, sys.executable, args)
+
+
+# main
+url = look_for_local_server()
+if not url:
+ start_local_server()
+ # wait for up to 5 seconds for the server to start
+ for i in range(10):
+ import time
+ time.sleep(0.5)
+ url = look_for_local_server()
+ if url:
+ break
+ else:
+ print >> sys.stderr, 'The local server is not starting, giving up.'
+ sys.exit(1)
+
+try:
+ import webbrowser
+ browser = webbrowser.get()
+ name = getattr(browser, 'name', browser.__class__.__name__)
+ print "Trying to open '%s' with '%s'..." % (url, name)
+ browser.open(url)
+except:
+ exc, val, tb = sys.exc_info()
+ print '-'*60
+ print >> sys.stderr, "Failed to launch the web browser:"
+ print >> sys.stderr, " %s: %s" % (exc.__name__, val)
+ print
+ print "Sorry, I guess you have to go to the following URL manually:"
+else:
+ print "Done running '%s'." % name
+ if look_for_local_server() != url:
+ # assume that browser.open() waited for the browser to finish
+ # and that the server has been closed from the browser.
+ raise SystemExit
+ print
+ print '-'*60
+ print "If the browser fails to open the page automatically,"
+ print "you will have to manually go to the following URL:"
+print ' ', url
+print '-'*60
+print "Note that the server runs in the background. You have to use"
+print "the 'Stop this program' link to cleanly stop it."
+print "Normally, however, running this script multiple times should"
+print "not create multiple servers in the background."
diff --git a/INSTALL.txt b/INSTALL.txt
new file mode 100644
index 0000000..2ffd320
--- /dev/null
+++ b/INSTALL.txt
@@ -0,0 +1,63 @@
+ =======================================================
+==== The Bub's Brothers ====
+ =======================================================
+
+
+Home page:
+ http://bub-n-bros.sourceforge.net/
+
+Installation instructions:
+ http://bub-n-bros.sourceforge.net/download.html
+
+Basic instructions on how to play:
+ http://bub-n-bros.sourceforge.net/howto.html
+
+If you unpacked this from an official release, you will find a copy of
+some pages in the doc/ subdirectory.
+
+Installation on *NIX
+--------------------
+
+NOTE!! THIS IS STILL EXPERIMENTAL. The game does not have to be
+ installed to play. The currently directory layout was not
+ designed with Unix-style installation in mind. For a
+ QUICK START see in doc/Introduction.html.
+
+The game (and the man pages) can be installed on *NIX systems into a
+standard location like this:
+
+1) Check locations and permissions in the top-level Makefile. The game
+ is installed into /usr/local by default.
+
+2) Optionally run "make" to build extensions that enable cool powerups
+ and xshm operation. Recommended
+
+3) Run "make install" as root or other user with sufficient rights.
+
+NOTE!! The game logic is still quite directory-layout dependant ATM so
+ installing is implemented by copying the whole directory into
+ /usr/local/lib/ (the location can be changed in the Makefile)
+ and making symlinks to appropriate directories. This procedure
+ will probably be cleaned up in future releases.
+
+Copyright
+---------
+
+Almost all sprite images, sounds, background musics and levels are
+directly taken from the MacOS version of Bub & Bob 1 by McSebi, and
+redistributed with his gracious permission. Most graphics have been
+improved or remade by David Gowers.
+
+ http://www.mcsebi.de
+
+See LICENSE.txt for more information.
+
+
+Authors
+-------
+
+Programming: Armin Rigo
+Art: David Gowers, based on graphics from McSebi
+Levels: Gio & Armin
+Special thanks: Odie & Brachamutanda
+Beta-testers: IMA Connection
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..60378c1
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,33 @@
+License
+=======
+
+The source code is copyrighted by Armin Rigo <arigo@tunes.org>, and
+distributed under the MIT License, a copy of which is included below.
+
+In the current version most graphics have been made by David
+Gowers, based on graphics from McSebi.
+
+Almost all sounds, background musics and levels are directly
+taken from the MacOS version of Bub & Bob 1 by McSebi. Thanks
+to him for great artwork and his permission to redistribute
+it! He released them under the following terms:
+
+ """
+ I release the graphics and sound data of my
+ program Bub & Bob 1 under the revised Artistic License.
+ Please read artistic.txt for details.
+ Sebastian Wegner <sebi@mcsebi.com>
+ Copyright (C) 2005 Sebastian Wegner
+ """
+
+
+MIT License
+===========
+
+Source Code Copyright (c) 2003-2005 Armin Rigo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..39ed683
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,63 @@
+OWNER=root
+GROUP=games
+LIBDIR=/usr/local/lib
+BINDIR=/usr/local/games
+
+MANOWNER=root
+MANGROUP=root
+MANDIR=/usr/local/man
+
+INSTALL=install
+PYTHON=python
+
+export # we export all variales to sub-makes
+
+all:
+ if [ -e bubbob ]; then make -C bubbob; fi
+ make -C display
+ @echo -------------------------------------------------------------
+ @echo \'make\' successful.
+ @echo ' '
+ @echo ' Start the game interactively with: python BubBob.py'
+ @if [ -e bubbob ]; then echo ' Server only (pure command-line): python bubbob/bb.py --help'; else echo ' Only the client is installed here.'; fi
+ @echo ' '
+ @echo -------------------------------------------------------------
+
+clean:
+ -rm -f `find -name "*~"`
+ -rm -f `find -name "*.py[co]"`
+ -rm -fr `find -name "build"`
+ make -C doc clean
+ cd bubbob/images && python buildcolors.py -c
+ rm -fr cache
+
+sync: magma-sync codespeak-sync
+
+magma-sync:
+ rsync --delete -avz -e ssh ~/games/* magma:games/x/
+
+codespeak-sync:
+ rsync --delete -avz -e ssh ${HOME}/games/metaserver ${HOME}/games/common codespeak.net:games/
+
+meta:
+ ssh codespeak.net python games/metaserver/metaserver.py -f
+
+docs:
+ make -C doc
+
+install-docs:
+ make -C doc install
+
+# crude install
+install: install-docs
+# install fanciness not yet implemented :)
+# make -C bubbob install
+# make -C display install
+ $(INSTALL) -d $(LIBDIR)/bub-n-bros
+ cp -R . $(LIBDIR)/bub-n-bros
+ chown -R $(OWNER):$(GROUP) $(LIBDIR)/bub-n-bros
+ ln -s $(LIBDIR)/bub-n-bros/display/Client.py $(BINDIR)/bubnbros
+ ln -s $(LIBDIR)/bub-n-bros/bubbob/bb.py $(BINDIR)/bubnbros-server
+ chmod +x $(BINDIR)/bubnbros
+ chmod +x $(BINDIR)/bubnbros-server
+ $(PYTHON) $(LIBDIR)/bub-n-bros/bubbob/images/buildcolors.py
diff --git a/artistic.txt b/artistic.txt
new file mode 100644
index 0000000..afcdcea
--- /dev/null
+++ b/artistic.txt
@@ -0,0 +1,73 @@
+Artistic License 2.0
+Copyright (c) 2000-2006, The Perl Foundation.
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+Preamble
+This license establishes the terms under which a given free software Package may be copied, modified, distributed, and/or redistributed. The intent is that the Copyright Holder maintains some artistic control over the development of that Package while still keeping the Package available as open source and free software.
+
+You are always permitted to make arrangements wholly outside of this license directly with the Copyright Holder of a given Package. If the terms of this license do not permit the full use that you propose to make of the Package, you should contact the Copyright Holder and seek a different licensing arrangement.
+
+Definitions
+"Copyright Holder" means the individual(s) or organization(s) named in the copyright notice for the entire Package.
+
+"Contributor" means any party that has contributed code or other material to the Package, in accordance with the Copyright Holder's procedures.
+
+"You" and "your" means any person who would like to copy, distribute, or modify the Package.
+
+"Package" means the collection of files distributed by the Copyright Holder, and derivatives of that collection and/or of those files. A given Package may consist of either the Standard Version, or a Modified Version.
+
+"Distribute" means providing a copy of the Package or making it accessible to anyone else, or in the case of a company or organization, to others outside of your company or organization.
+
+"Distributor Fee" means any fee that you charge for Distributing this Package or providing support for this Package to another party. It does not mean licensing fees.
+
+"Standard Version" refers to the Package if it has not been modified, or has been modified only in ways explicitly requested by the Copyright Holder.
+
+"Modified Version" means the Package, if it has been changed, and such changes were not explicitly requested by the Copyright Holder.
+
+"Original License" means this Artistic License as Distributed with the Standard Version of the Package, in its current version or as it may be modified by The Perl Foundation in the future.
+
+"Source" form means the source code, documentation source, and configuration files for the Package.
+
+"Compiled" form means the compiled bytecode, object code, binary, or any other form resulting from mechanical transformation or translation of the Source form.
+
+Permission for Use and Modification Without Distribution
+(1) You are permitted to use the Standard Version and create and use Modified Versions for any purpose without restriction, provided that you do not Distribute the Modified Version.
+
+Permissions for Redistribution of the Standard Version
+(2) You may Distribute verbatim copies of the Source form of the Standard Version of this Package in any medium without restriction, either gratis or for a Distributor Fee, provided that you duplicate all of the original copyright notices and associated disclaimers. At your discretion, such verbatim copies may or may not include a Compiled form of the Package.
+
+(3) You may apply any bug fixes, portability changes, and other modifications made available from the Copyright Holder. The resulting Package will still be considered the Standard Version, and as such will be subject to the Original License.
+
+Distribution of Modified Versions of the Package as Source
+(4) You may Distribute your Modified Version as Source (either gratis or for a Distributor Fee, and with or without a Compiled form of the Modified Version) provided that you clearly document how it differs from the Standard Version, including, but not limited to, documenting any non-standard features, executables, or modules, and provided that you do at least ONE of the following:
+
+(a) make the Modified Version available to the Copyright Holder of the Standard Version, under the Original License, so that the Copyright Holder may include your modifications in the Standard Version.
+(b) ensure that installation of your Modified Version does not prevent the user installing or running the Standard Version. In addition, the Modified Version must bear a name that is different from the name of the Standard Version.
+(c) allow anyone who receives a copy of the Modified Version to make the Source form of the Modified Version available to others under
+(i) the Original License or
+(ii) a license that permits the licensee to freely copy, modify and redistribute the Modified Version using the same licensing terms that apply to the copy that the licensee received, and requires that the Source form of the Modified Version, and of any works derived from it, be made freely available in that license fees are prohibited but Distributor Fees are allowed.
+Distribution of Compiled Forms of the Standard Version or Modified Versions without the Source
+(5) You may Distribute Compiled forms of the Standard Version without the Source, provided that you include complete instructions on how to get the Source of the Standard Version. Such instructions must be valid at the time of your distribution. If these instructions, at any time while you are carrying out such distribution, become invalid, you must provide new instructions on demand or cease further distribution. If you provide valid instructions or cease distribution within thirty days after you become aware that the instructions are invalid, then you do not forfeit any of your rights under this license.
+
+(6) You may Distribute a Modified Version in Compiled form without the Source, provided that you comply with Section 4 with respect to the Source of the Modified Version.
+
+Aggregating or Linking the Package
+(7) You may aggregate the Package (either the Standard Version or Modified Version) with other packages and Distribute the resulting aggregation provided that you do not charge a licensing fee for the Package. Distributor Fees are permitted, and licensing fees for other components in the aggregation are permitted. The terms of this license apply to the use and Distribution of the Standard or Modified Versions as included in the aggregation.
+
+(8) You are permitted to link Modified and Standard Versions with other works, to embed the Package in a larger work of your own, or to build stand-alone binary or bytecode versions of applications that include the Package, and Distribute the result without restriction, provided the result does not expose a direct interface to the Package.
+
+Items That are Not Considered Part of a Modified Version
+(9) Works (including, but not limited to, modules and scripts) that merely extend or make use of the Package, do not, by themselves, cause the Package to be a Modified Version. In addition, such works are not considered parts of the Package itself, and are not subject to the terms of this license.
+
+General Provisions
+(10) Any use, modification, and distribution of the Standard or Modified Versions is governed by this Artistic License. By using, modifying or distributing the Package, you accept this license. Do not use, modify, or distribute the Package, if you do not accept this license.
+
+(11) If your Modified Version has been derived from a Modified Version made by someone other than you, you are nevertheless required to ensure that your Modified Version complies with the requirements of this license.
+
+(12) This license does not grant you the right to use any trademark, service mark, tradename, or logo of the Copyright Holder.
+
+(13) This license includes the non-exclusive, worldwide, free-of-charge patent license to make, have made, use, offer to sell, sell, import and otherwise transfer the Package with respect to any patent claims licensable by the Copyright Holder that are necessarily infringed by the Package. If you institute patent litigation (including a cross-claim or counterclaim) against any party alleging that the Package constitutes direct or contributory patent infringement, then this Artistic License to you shall terminate on the date that such litigation is filed.
+
+(14) Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/bubbob/.cvsignore b/bubbob/.cvsignore
new file mode 100644
index 0000000..687980a
--- /dev/null
+++ b/bubbob/.cvsignore
@@ -0,0 +1,4 @@
+*.py[co]
+build
+*.so
+*.pyd
diff --git a/bubbob/Makefile b/bubbob/Makefile
new file mode 100644
index 0000000..ab56103
--- /dev/null
+++ b/bubbob/Makefile
@@ -0,0 +1,2 @@
+statesaver.so: statesaver.c
+ python setup.py build_ext -i
diff --git a/bubbob/bb.py b/bubbob/bb.py
new file mode 100755
index 0000000..c0ab351
--- /dev/null
+++ b/bubbob/bb.py
@@ -0,0 +1,345 @@
+#! /usr/bin/env python
+
+from __future__ import generators
+
+# __________
+import os, sys
+if __name__ == '__main__':
+ LOCALDIR = sys.argv[0]
+else:
+ LOCALDIR = __file__
+try:
+ LOCALDIR = os.readlink(LOCALDIR)
+except:
+ pass
+LOCALDIR = os.path.dirname(os.path.abspath(LOCALDIR))
+# ----------
+
+import random, time
+
+sys.path.insert(0, os.path.join(os.path.dirname(LOCALDIR), 'common'))
+sys.path.insert(0, LOCALDIR)
+import gamesrv
+
+PROFILE = 0
+
+
+class BubBobGame(gamesrv.Game):
+
+ FnDesc = "Bub & Bob"
+ FnBasePath = "bubbob"
+ Quiet = 0
+ End = 0
+
+ def __init__(self, levelfile,
+ beginboard = 1,
+ stepboard = 1,
+ finalboard = 100,
+ limitlives = None,
+ extralife = 50000,
+ lifegainlimit = None,
+ autoreset = 0,
+ metaserver = 0,
+ monsters = 0):
+ gamesrv.Game.__init__(self)
+ self.game_reset_gen = None
+ self.levelfile = levelfile
+ self.beginboard = beginboard
+ self.finalboard = finalboard
+ self.stepboard = stepboard
+ self.limitlives = limitlives
+ self.lifegainlimit = lifegainlimit
+ self.extralife = extralife
+ self.autoreset = autoreset
+ self.metaserver = metaserver
+ self.f_monsters = monsters
+ self.updatemetaserver()
+ levelsname, ext = os.path.splitext(os.path.basename(levelfile))
+ self.FnDesc = BubBobGame.FnDesc + ' ' + levelsname
+ self.reset()
+ self.openserver()
+
+ def openboard(self, num=None):
+ self.End = 0
+ if num is None:
+ num = self.beginboard-1
+ import boards
+ boards.loadmodules(force=1)
+ import boards # possibly re-import
+ self.width = boards.bwidth + 9*boards.CELL
+ self.height = boards.bheight
+ boards.curboard = None
+ boards.BoardGen = [boards.next_board(num)]
+ self.updatemetaserver()
+
+ def reset(self):
+ import player
+ self.openboard()
+ for p in player.BubPlayer.PlayerList:
+ p.reset()
+
+ def FnPlayers(self):
+ from player import BubPlayer
+ result = {}
+ for p in BubPlayer.PlayerList:
+ result[p.pn] = p
+ return result
+
+ def FnFrame(self):
+ if self.metaregister:
+ self.do_updatemetaserver()
+ frametime = 0.0
+ for i in xrange(500):
+ import boards
+ for gen in boards.BoardGen[:]:
+ try:
+ frametime += gen.next()
+ except StopIteration:
+ try:
+ boards.BoardGen.remove(gen)
+ except ValueError:
+ pass
+ if frametime >= 1.1:
+ break
+ else:
+ # should normally never occur
+ boards.BoardGen[:] = [boards.next_board()]
+ frametime = 1.0
+ if self.game_reset_gen is None:
+ if self.End and self.autoreset:
+ self.game_reset_gen = boards.game_reset()
+ else:
+ try:
+ self.game_reset_gen.next()
+ except StopIteration:
+ self.game_reset_gen = None
+ return frametime * boards.FRAME_TIME
+
+ def FnServerInfo(self, msg):
+ try:
+ from images import writestr
+ writestr(50, 50, msg)
+ self.sendudpdata()
+ except:
+ pass
+
+ def FnExcHandler(self, kbd):
+ if kbd:
+ self.FnServerInfo("Server was Ctrl-C'ed!")
+ else:
+ self.FnServerInfo('Ooops -- server crash!')
+ from player import BubPlayer
+ if kbd and not [p for p in BubPlayer.PlayerList if p.isplaying()]:
+ return 0
+ import traceback
+ print "-"*60
+ traceback.print_exc()
+ print "-"*60
+ if not kbd:
+ try:
+ if self.metaserver:
+ try:
+ import metaclient
+ except ImportError:
+ pass
+ else:
+ if metaclient.metaclisrv:
+ metaclient.metaclisrv.send_traceback()
+ except Exception, e:
+ print '! %s: %s' % (e.__class__.__name__, e)
+ import boards
+ num = getattr(boards.curboard, 'num', None)
+ if self.Quiet:
+ print "Crash recovery! Automatically restarting board %s" % num
+ import time; time.sleep(2)
+ else:
+ print "Correct the problem and leave pdb to restart board %s..."%num
+ import pdb; pdb.post_mortem(sys.exc_info()[2])
+ self.openboard(num)
+ return 1
+
+ def FnListBoards():
+ import boards
+ result = []
+ for fn in os.listdir('levels'):
+ base, ext = os.path.splitext(fn)
+ if ext in ('.py', '.bin'):
+ result.append((base, os.path.join('levels', fn)))
+ return result
+ FnListBoards = staticmethod(FnListBoards)
+
+ def FnExtraDesc(self):
+ import boards
+ s = gamesrv.Game.FnExtraDesc(self)
+ if boards.curboard and self.End != 'gameover':
+ s = '%s, board %d' % (s, boards.curboard.num+1)
+ return s
+
+ def do_updatemetaserver(self):
+ self.metaregister -= 1
+ if self.metaregister > 0:
+ return
+ if self.metaserver and (self.autoreset or self.End != 'gameover'):
+ setuppath('metaserver')
+ import metaclient
+ metaclient.meta_register(self)
+ print '.'
+ else:
+ try:
+ import metaclient
+ except ImportError:
+ pass
+ else:
+ metaclient.meta_unregister(self)
+
+ def updatemetaserver(self):
+ self.metaregister = 2
+
+ updateboard = updateplayers = updatemetaserver
+
+
+def setuppath(dirname):
+ dir = os.path.abspath(os.path.join(LOCALDIR, os.pardir, dirname))
+ if not os.path.isdir(dir):
+ print >> sys.stderr, (
+ '../%s: directory not found ("cvs update -d" ?)' % dirname)
+ sys.exit(1)
+ if dir not in sys.path:
+ sys.path.insert(0, dir)
+
+def parse_cmdline(argv):
+ # parse command-line
+ def usage():
+ print >> sys.stderr, 'usage:'
+ print >> sys.stderr, ' python bb.py'
+## print >> sys.stderr, ' python bb.py [-w/--webbrowser=no]'
+## print >> sys.stderr, 'where:'
+## print >> sys.stderr, ' -w --webbrowser=no don''t automatically start web browser'
+ print >> sys.stderr, 'or:'
+ print >> sys.stderr, ' python bb.py [level-file.bin] [-m] [-b#] [-s#] [-l#] [-M#]'
+ print >> sys.stderr, 'with options:'
+ print >> sys.stderr, ' -m --metaserver register the server on the Metaserver so anyone can join'
+ print >> sys.stderr, ' -b# --begin # start at board number # (default 1)'
+ print >> sys.stderr, ' --start # synonym for --begin'
+ print >> sys.stderr, ' --final # end at board number # (default 100)'
+ print >> sys.stderr, ' -s# --step # advance board number by steps of # (default 1)'
+ print >> sys.stderr, ' -l# --lives # limit the number of lives to #'
+ print >> sys.stderr, ' --extralife # gain extra life every # points'
+ print >> sys.stderr, ' --limitlives # max # of lives player can gain in one board'
+ print >> sys.stderr, ' -M# --monsters # multiply the number of monsters by #'
+ print >> sys.stderr, ' (default between 1.0 and 2.0 depending on # of players)'
+ print >> sys.stderr, ' -i --infinite restart the server at the end of the game'
+ print >> sys.stderr, ' --port LISTEN=# set fixed tcp port for game server'
+ print >> sys.stderr, ' --port HTTP=# set fixed tcp port for http server'
+ print >> sys.stderr, ' -h --help display this text'
+ #print >> sys.stderr, ' -rxxx record the game in file xxx'
+ sys.exit(1)
+
+ try:
+ from getopt import gnu_getopt as getopt
+ except ImportError:
+ from getopt import getopt
+ from getopt import error
+ try:
+ opts, args = getopt(argv, 'mb:s:l:M:ih',
+ ['metaserver', 'start=', 'step=',
+ 'lives=', 'monsters=', 'infinite', 'help',
+ 'extralife=', 'limitlives=', 'final=',
+ 'saveurlto=', 'quiet', 'port=', 'makeimages'])
+ except error, e:
+ print >> sys.stderr, 'bb.py: %s' % str(e)
+ print >> sys.stderr
+ usage()
+
+ options = {}
+ #webbrowser = 1
+ save_url_to = None
+ quiet = 0
+ for key, value in opts:
+ if key in ('-m', '--metaserver'):
+ options['metaserver'] = 1
+ elif key in ('-b', '--start', '--begin'):
+ options['beginboard'] = int(value)
+ elif key in ('-s', '--step'):
+ options['stepboard'] = int(value)
+ elif key in ('-l', '--lives'):
+ options['limitlives'] = int(value)
+ elif key in ('--limitlives'):
+ options['lifegainlimit'] = int(value)
+ elif key in ('--final'):
+ options['finalboard'] = int(value)
+ if options['finalboard'] < options['beginboard']:
+ print >> sys.stderr, 'bb.py: final board value must be larger than begin board.'
+ sys.exit(1)
+ elif key in ('--extralife'):
+ options['extralife'] = int(value)
+ elif key in ('-M', '--monsters'):
+ options['monsters'] = float(value)
+ elif key in ('-i', '--infinite'):
+ options['autoreset'] = 1
+ elif key in ('-h', '--help'):
+ usage()
+ elif key == '--saveurlto':
+ save_url_to = value
+ elif key == '--quiet':
+ quiet = 1
+ elif key == '--port':
+ portname, portvalue = value.split('=')
+ portvalue = int(portvalue)
+ import msgstruct
+ msgstruct.PORTS[portname] = portvalue
+ elif key == '--makeimages':
+ import images
+ sys.exit(0)
+ #elif key in ('-w', '--webbrowser'):
+ # webbrowser = value.startswith('y')
+ if args:
+ if len(args) > 1:
+ print >> sys.stderr, 'bb.py: multiple level files specified'
+ sys.exit(1)
+ levelfile = os.path.abspath(args[0])
+ os.chdir(LOCALDIR)
+ BubBobGame(levelfile, **options)
+ else:
+ if options:
+ print >> sys.stderr, 'bb.py: command-line options ignored'
+ start_metaserver(save_url_to, quiet)
+
+def start_metaserver(save_url_to, quiet):
+ os.chdir(LOCALDIR)
+ setuppath('http2')
+ import httppages
+ httppages.main(BubBobGame, save_url_to, quiet)
+
+
+def setup():
+ keybmp = gamesrv.getbitmap(os.path.join('images', 'keys.ppm'))
+ def keybmplist(x):
+ return [keybmp.geticon(x, y, 32, 32) for y in range(0, 128, 32)]
+ BubBobGame.FnKeys = [
+ ("right", keybmplist(0), "kRight"),
+ ("left", keybmplist(32), "kLeft"),
+ ("jump", keybmplist(64), "kJump"),
+ ("fire", keybmplist(96), "kFire"),
+ ("-right", [], "kmRight"),
+ ("-left", [], "kmLeft"),
+ ("-jump", [], "kmJump"),
+ ("-fire", [], "kmFire"),
+ ]
+
+setup()
+
+def main():
+ parse_cmdline(sys.argv[1:])
+ if not PROFILE:
+ gamesrv.mainloop()
+ else:
+ import profile
+ prof = profile.Profile()
+ try:
+ prof = prof.run('gamesrv.mainloop()')
+ finally:
+ prof.dump_stats('profbb')
+
+if __name__ == '__main__':
+ main()
diff --git a/bubbob/binboards.py b/bubbob/binboards.py
new file mode 100644
index 0000000..af878ff
--- /dev/null
+++ b/bubbob/binboards.py
@@ -0,0 +1,122 @@
+import macbinary
+import boards
+import gamesrv
+from mnstrmap import Nasty, Monky, Ghosty, Flappy
+from mnstrmap import Springy, Orcy, Gramy, Blitzy
+from images import KEYCOL
+
+keycol = (KEYCOL & 0xFF,
+ (KEYCOL>>8) & 0xFF,
+ (KEYCOL>>16) & 0xFF)
+
+
+def meancolor(img):
+ r1 = g1 = b1 = 0
+ count = float(len(img) * len(img[0]))
+ for line in img:
+ for r, g, b in line:
+ r1 += r
+ g1 += g
+ b1 += b
+ return r1/count, g1/count, b1/count
+
+def addshadow(img, (r1, g1, b1), depth=8):
+ w = len(img[0])
+ h = len(img)
+ pad = depth * [keycol]
+ result = [line + pad for line in img] + [
+ (w+depth) * [keycol] for d in range(depth)]
+ for d in range(depth):
+ f = 1.0 - float(d)/depth
+ color = (r1 * f, g1 * f, b1 * f)
+ for i in range(w):
+ result[h+d][1+d+i] = color
+ for i in range(h):
+ result[1+d+i][w+d] = color
+ return result
+
+def addrshadow(img, (r1, g1, b1), depth=8):
+ w = len(img[0])
+ h = len(img)
+ pad = depth * [keycol]
+ result = [line + pad for line in img]
+ for d in range(depth):
+ f = 1.0 - float(d)/depth
+ color = (r1 * f, g1 * f, b1 * f)
+ for i in range(h):
+ result[i][w+d] = color
+ return result
+
+
+def load(filename):
+ print "Loading %s..." % filename
+ Bin = macbinary.MacBinary(filename)
+ levels = {}
+ mnstrlist = [Nasty, Monky, Ghosty, Flappy,
+ Springy, Orcy, Gramy, Blitzy]
+
+ for key, lvl in Bin['LEVL'].items():
+ d = lvl.getlevel(mnstrlist)
+ class BinBoard(boards.Board):
+ pass
+ for key1, value1 in d.items():
+ setattr(BinBoard, key1, value1)
+ levels[key] = BinBoard
+
+ def loader(code, rsrc=Bin['ppat'], cache={}):
+ try:
+ return cache[code]
+ except KeyError:
+ pass
+ keycol1 = None
+ bid = code[0]
+ result = None
+ if code[1] == 'l':
+ # left border wall
+ img = rsrc[bid + 228].getimage()
+ color = meancolor(img)
+ img = [line[:32] for line in img]
+ result = addrshadow(img, color)
+ elif code[1] == 'r':
+ # right border wall
+ img = rsrc[bid + 228].getimage()
+ w = len(img[0])
+ assert w in (32, 64), bid
+ if w == 64:
+ color = meancolor(img)
+ img = [line[32:64] for line in img]
+ result = addrshadow(img, color)
+ else:
+ # normal wall
+ dx, dy = code[1:]
+ img = rsrc[bid + 128].getimage()
+ w = len(img[0])
+ h = len(img)
+ assert w & 15 == h & 15 == 0, bid
+ dx *= 16
+ dy *= 16
+ if dx < w and dy < h:
+ color = meancolor(img)
+ img = [line[dx:dx+16] for line in img[dy:dy+16]]
+ result = addshadow(img, color)
+ keycol1 = KEYCOL
+ if result is not None:
+ w, h, data = macbinary.image2rgb(result)
+ ppmdata = "P6\n%d %d\n255\n%s" % (w, h, data)
+ result = gamesrv.newbitmap(ppmdata, keycol1), (0, 0, w, h)
+ cache[code] = result
+ return result
+
+ def bin_haspat(code, loader=loader):
+ try:
+ return loader(code) is not None
+ except KeyError:
+ return 0
+ def bin_loadpattern(code, keycol=None, loader=loader):
+ result = loader(code)
+ assert result is not None, code
+ return result
+
+ boards.haspat = bin_haspat
+ boards.loadpattern = bin_loadpattern
+ return levels
diff --git a/bubbob/boarddef.py b/bubbob/boarddef.py
new file mode 100644
index 0000000..168a3e1
--- /dev/null
+++ b/bubbob/boarddef.py
@@ -0,0 +1,69 @@
+import boards, mnstrmap
+
+class left:
+ dir = 1
+ def __init__(self, cls):
+ self.cls = cls
+ def build(self, x, y):
+ return self.cls(x=x, y=y-1, dir=self.dir)
+
+class right(left):
+ dir = 0
+
+LNasty = left(mnstrmap.Nasty)
+RNasty = right(mnstrmap.Nasty)
+
+LMonky = left(mnstrmap.Monky)
+RMonky = right(mnstrmap.Monky)
+
+LGhosty = left(mnstrmap.Ghosty)
+RGhosty = right(mnstrmap.Ghosty)
+
+LFlappy = left(mnstrmap.Flappy)
+RFlappy = right(mnstrmap.Flappy)
+
+LSpringy = left(mnstrmap.Springy)
+RSpringy = right(mnstrmap.Springy)
+
+LOrcy = left(mnstrmap.Orcy)
+ROrcy = right(mnstrmap.Orcy)
+
+LGramy = left(mnstrmap.Gramy)
+RGramy = right(mnstrmap.Gramy)
+
+LBlitzy = left(mnstrmap.Blitzy)
+RBlitzy = right(mnstrmap.Blitzy)
+
+
+# Sugar around the Board class
+class Level(boards.Board):
+
+ WIND_DELTA = boards.CELL
+ winds = None
+ monsters = []
+
+ def __init__(self, num):
+ walls = [line for line in self.walls.split('\n') if line]
+ self.monsters = list(self.monsters)
+ for y in range(len(walls)):
+ line = walls[y]
+ for x in range(len(line)):
+ c = line[x]
+ if c != ' ' and c != '#':
+ deflist = getattr(self, c)
+ if isinstance(deflist, left):
+ deflist = (deflist,)
+ for builder in deflist:
+ self.monsters.append(builder.build(x,y))
+ self.walls = self.walls.replace(c, ' ')
+ if self.winds is None:
+ width = len(walls[0])
+ height = len(walls)
+ spaces = " " * (width-6)
+ lbar = '>'*(width/2-2)
+ rbar = '<'*(width/2-2)
+ winds = ['>> ' + spaces + ' <<',
+ lbar + 'x'*(width-len(lbar)-len(rbar)) + rbar]
+ winds += ['>>^' + spaces + '^<<'] * (height-2)
+ self.winds = '\n'.join(winds)
+ boards.Board.__init__(self, num)
diff --git a/bubbob/boards.py b/bubbob/boards.py
new file mode 100644
index 0000000..dced6f5
--- /dev/null
+++ b/bubbob/boards.py
@@ -0,0 +1,1470 @@
+from __future__ import generators
+import random, os, sys, math
+import gamesrv
+import images
+
+CELL = 16 # this constant is inlined at some places, don't change
+HALFCELL = CELL//2
+FRAME_TIME = 0.025
+#DEFAULT_LEVEL_FILE = 'levels/scratch.py'
+
+BOARD_BKGND = 1 # 0 = black, 1 = darker larger wall tiles
+
+
+class Copyable:
+ pass # see bonuses.py, class Clock
+
+
+class Board(Copyable):
+ letter = 0
+ fire = 0
+ lightning = 0
+ water = 0
+ top = 0
+
+ WIND_DELTA = HALFCELL
+
+ def __init__(self, num):
+ # the subclasses should define 'walls', 'winds', 'monsters'
+ self.walls = walls = [line for line in self.walls.split('\n') if line]
+ self.winds = winds = [line for line in self.winds.split('\n') if line]
+ self.num = num
+ self.width = len(walls[0])
+ self.height = len(walls)
+ for line in walls:
+ assert len(line) == self.width, "some wall lines are longer than others"
+ for line in winds:
+ assert len(line) == self.width, "some wind lines are longer than others"
+ #assert walls[0] == walls[-1], "first and last lines must be identical"
+ assert len(winds) == self.height, "wall and wind heights differ"
+ self.walls_by_pos = {}
+ self.sprites = {}
+ if self.top:
+ testline = self.walls[0]
+ else:
+ testline = self.walls[-1]
+ self.holes = testline.find(' ') >= 0
+ self.playingboard = 0
+ self.bonuslevel = not self.monsters or (gamesrv.game.finalboard is not None and self.num >= gamesrv.game.finalboard)
+ self.cleaning_gen_state = 0
+
+ def set_musics(self, prefix=[]):
+ if (self.num+1) % 20 < 10:
+ gamesrv.set_musics(prefix + [images.music_intro], [images.music_game],
+ reset=0)
+ else:
+ gamesrv.set_musics(prefix + [], [images.music_game2], reset=0)
+
+ def writesprites(self, name, xyicolist):
+ sprlist = self.sprites.setdefault(name, [])
+ xyicolist = xyicolist[:]
+ xyicolist.reverse()
+ for s in sprlist[:]:
+ if xyicolist:
+ s.move(*xyicolist.pop())
+ else:
+ s.kill()
+ sprlist.remove(s)
+ while xyicolist:
+ x, y, ico = xyicolist.pop()
+ sprlist.append(gamesrv.Sprite(ico, x, y))
+
+ def enter(self, complete=1, inplace=0, fastreenter=False):
+ global curboard
+ if inplace:
+ print "Re -",
+ print "Entering board", self.num+1
+ self.set_musics()
+ # add board walls
+ l = self.sprites.setdefault('walls', [])
+ bl = self.sprites.setdefault('borderwalls', [])
+ if inplace:
+ deltay = 0
+ else:
+ deltay = bheight
+ wnx = wny = 1
+ while haspat((self.num, wnx, 0)):
+ wnx += 1
+ while haspat((self.num, 0, wny)):
+ wny += 1
+ self.wnx = wnx
+ self.wny = wny
+
+ if haspat((self.num, 'l')):
+ lefticon = patget((self.num, 'l'))
+ if haspat((self.num, 'r')):
+ righticon = patget((self.num, 'r'))
+ else:
+ righticon = lefticon
+ xrange = range(2, self.width-2)
+ else:
+ xrange = range(self.width)
+ lefticon = righticon = None
+
+ if BOARD_BKGND == 1:
+ gl = self.sprites.setdefault('background', [])
+ xmax = (self.width-2)*CELL
+ ymax = self.height*CELL
+ y = -HALFCELL
+ ystep = 0
+ firstextra = 1
+ while y < ymax:
+ x = 2*CELL+HALFCELL
+ xstep = 0
+ while x < xmax:
+ bitmap, rect = loadpattern((self.num, xstep, ystep),
+ images.KEYCOL)
+ bitmap, rect = images.makebkgndpattern(bitmap, rect)
+ if firstextra:
+ # special position where a bit of black might show up
+ x -= rect[2]
+ xstep = (xstep-1) % wnx
+ firstextra = 0
+ continue
+ bkgndicon = bitmap.geticon(*rect)
+ w = gamesrv.Sprite(bkgndicon, x, y + deltay)
+ gl.append(w)
+ x += rect[2]
+ xstep = (xstep+1) % wnx
+ y += rect[3]
+ ystep = (ystep+1) % wny
+ else:
+ gl = []
+
+ if lefticon is not None:
+ for y in range(0, self.height, lefticon.h // CELL):
+ bl.append(gamesrv.Sprite(lefticon, 0, y*CELL + deltay))
+
+ for y in range(self.height):
+ for x in xrange:
+ c = self.walls[y][x]
+ if c == '#':
+ wallicon = patget((self.num, x%wnx, y%wny), images.KEYCOL)
+ w = gamesrv.Sprite(wallicon, x*CELL, y*CELL + deltay)
+ l.append(w)
+ self.walls_by_pos[y,x] = w
+
+ if not l:
+ # self.sprites['walls'] must not be empty, for putwall
+ wallicon = patget((self.num, 0, 0), images.KEYCOL)
+ w = gamesrv.Sprite(wallicon, 0, -wallicon.h)
+ l.append(w)
+
+ if righticon is not None:
+ for y in range(0, self.height, lefticon.h // CELL):
+ bl.append(gamesrv.Sprite(righticon, (self.width-2)*CELL, y*CELL + deltay))
+
+ while deltay:
+ dy = -min(deltay, 8)
+ for w in gl:
+ w.step(0, dy)
+ for w in l:
+ w.step(0, dy)
+ for w in bl:
+ w.step(0, dy)
+ deltay += dy
+ yield 1
+
+ if inplace:
+ for w in images.ActiveSprites:
+ w.to_front()
+
+ curboard = self
+ if gamesrv.game:
+ gamesrv.game.updateboard()
+ if not complete:
+ return
+ # add players
+ from player import BubPlayer, scoreboard
+ if not inplace:
+ random.shuffle(BubPlayer.PlayerList)
+ scoreboard(1, inplace=inplace)
+ if not fastreenter:
+ random.shuffle(BubPlayer.PlayerList)
+ playing = []
+ for p in BubPlayer.PlayerList:
+ if p.isplaying():
+ p.enterboard(playing)
+ p.zarkon()
+ playing.append(p)
+ for d in BubPlayer.DragonList:
+ d.enter_new_board()
+ else:
+ # kill stuff left over from leave(inplace=1) (Big Clock bonus only)
+ import bonuses
+ keepme = bonuses.Points
+ dragons = {}
+ playing = []
+ for p in BubPlayer.PlayerList:
+ if p.isplaying():
+ for d in p.dragons:
+ if hasattr(d, 'dcap'):
+ d.dcap['shield'] = 90
+ dragons[d] = True
+ playing.append(p)
+ for s in images.ActiveSprites[:]:
+ if isinstance(s, keepme) or s in dragons:
+ pass
+ else:
+ s.kill()
+ # add monsters
+ if not self.bonuslevel:
+ import monsters
+ f_monsters = gamesrv.game.f_monsters
+ if f_monsters < 0.1:
+ f_monsters = max(1.0, min(2.0, (len(playing)-2)/2.2+1.0))
+ for mdef in self.monsters:
+ if not fastreenter:
+ yield 2
+ cls = getattr(monsters, mdef.__class__.__name__)
+ dir = mdef.dir
+ i = random.random()
+ while i < f_monsters:
+ cls(mdef, dir=dir)
+ dir = -dir
+ i += 1.0
+ self.playingboard = 1
+
+ def putwall(self, x, y, w=None):
+ wallicon = patget((self.num, x%self.wnx, y%self.wny), images.KEYCOL)
+ if w is None:
+ w = gamesrv.Sprite(wallicon, 0, bheight)
+ l = self.sprites['walls']
+ w.to_back(l[-1])
+ l.append(w)
+ self.walls_by_pos[y,x] = w
+ if y >= 0:
+ line = self.walls[y]
+ self.walls[y] = line[:x] + '#' + line[x+1:]
+
+ def killwall(self, x, y, kill=1):
+ w = self.walls_by_pos[y,x]
+ if kill:
+ l = self.sprites['walls']
+ if len(l) > 1:
+ l.remove(w)
+ w.kill()
+ else:
+ # self.sprites['walls'] must never be empty
+ # or putwall will crash!
+ w.move(0, -bheight)
+ del self.walls_by_pos[y,x]
+ line = self.walls[y]
+ self.walls[y] = line[:x] + ' ' + line[x+1:]
+ return w
+
+ def reorder_walls(self):
+ walls_by_pos = self.walls_by_pos
+ items = [(yx, w1.ico) for yx, w1 in walls_by_pos.items()]
+ if not items:
+ return # otherwise self.sprites['walls'] would be emptied
+ items.sort()
+ l = self.sprites['walls']
+ while len(l) > len(items):
+ l.pop().kill()
+ assert len(items) == len(l)
+ for ((y,x), ico), w2 in zip(items, l):
+ w2.move(x*CELL, y*CELL, ico)
+ walls_by_pos[y,x] = w2
+
+ def leave(self, inplace=0):
+ global curboard
+ if not gamesrv.has_loop_music():
+ gamesrv.fadeout(1.5)
+ from player import BubPlayer
+ for p in BubPlayer.PlayerList:
+ if p.isplaying():
+ p.savecaps()
+ if BubPlayer.LeaveBonus:
+ for t in BubPlayer.LeaveBonus:
+ yield t
+ BubPlayer.LeaveBonus = None
+ curboard = None
+ if inplace:
+ i = -1
+ else:
+ while images.ActiveSprites:
+ s = random.choice(images.ActiveSprites)
+ s.kill()
+ yield 0.9
+ i = 0
+ sprites = []
+ for l in self.sprites.values():
+ sprites += l
+ self.sprites.clear()
+ self.walls_by_pos.clear()
+ random.shuffle(sprites)
+ for s in sprites:
+ s.kill()
+ if i:
+ i -= 1
+ else:
+ yield 0.32
+ i = 3
+ if not inplace:
+ for p in BubPlayer.PlayerList:
+ if p.isplaying():
+ p.zarkoff()
+ yield 4
+
+ def clean_gen_state(self):
+ self.cleaning_gen_state = 1
+ while len(BoardGen) > 1:
+ #yield force_singlegen()
+ #if 'flood' in self.sprites:
+ # for s in self.sprites['flood']:
+ # s.kill()
+ # del self.sprites['flood']
+ yield normal_frame()
+ self.cleaning_gen_state = 0
+
+def bget(x, y):
+ if 0 <= x < curboard.width:
+ if y < 0 or y >= curboard.height:
+ y = 0
+ return curboard.walls[y][x]
+ else:
+ return '#'
+
+def wget(x, y):
+ delta = curboard.WIND_DELTA
+ x = (x + delta) // 16
+ y = (y + delta) // 16
+ if 0 <= x < curboard.width:
+ if y < 0:
+ y = 0
+ elif y >= curboard.height:
+ y = -1
+ return curboard.winds[y][x]
+ elif x < 0:
+ return '>'
+ else:
+ return '<'
+
+def onground(x, y):
+ if y & 15:
+ return 0
+ x0 = (x+5) // 16
+ x1 = (x+16) // 16
+ x2 = (x+27) // 16
+ y0 = y // 16 + 2
+
+ if x0 < 0 or x2 >= curboard.width:
+ return 0
+ y1 = y0 - 1
+ if not (0 < y0 < curboard.height):
+ if y0 != curboard.height:
+ y1 = 0
+ y0 = 0
+ y0 = curboard.walls[y0]
+ y1 = curboard.walls[y1]
+ return (' ' == y1[x0] == y1[x1] == y1[x2] and
+ not (' ' == y0[x0] == y0[x1] == y0[x2]))
+ #return (' ' == bget(x0,y0-1) == bget(x1,y0-1) == bget(x2,y0-1) and
+ # not (' ' == bget(x0,y0) == bget(x1,y0) == bget(x2,y0)))
+ #return (bget(x1,y0-1)==' ' and
+ # ((bget(x1,y0)=='#') or
+ # (bget(x0,y0)=='#' and bget(x0,y0-1)==' ') or
+ # (bget(x2,y0)=='#' and bget(x2,y0-1)==' ')))
+
+def onground_nobottom(x, y):
+ return onground(x, y) and y+32 < bheight
+
+def underground(x, y):
+ if y % CELL:
+ return 0
+ x0 = (x+5) // CELL
+ x1 = (x+CELL) // CELL
+ x2 = (x+2*CELL-5) // CELL
+ y0 = y // CELL
+
+ if x0 < 0 or x2 >= curboard.width:
+ return 0
+ y1 = y0 - 1
+ if not (0 < y0 < curboard.height):
+ if y0 != curboard.height:
+ y1 = 0
+ y0 = 0
+ y0 = curboard.walls[y0]
+ y1 = curboard.walls[y1]
+ return (' ' == y0[x0] == y0[x1] == y0[x2] and
+ not (' ' == y1[x0] == y1[x1] == y1[x2]))
+
+def x2bounds(x):
+ if x < 32:
+ return 32
+ elif x > bwidth - 64:
+ return bwidth - 64
+ else:
+ return x
+
+def vertical_warp(nx, ny):
+ if ny >= bheight:
+ ny -= bheightmod
+ elif ny < -32:
+ ny += bheightmod
+ return nx, ny
+
+def vertical_warp_sprite(spr):
+ if spr.y >= bheight:
+ spr.step(0, -bheightmod)
+ elif spr.y < -32:
+ spr.step(0, bheightmod)
+
+##def vertical_warp(nx, ny):
+## if ny >= bheight:
+## ny -= bheightmod
+## elif ny < -32:
+## ny += bheightmod
+## else:
+## return (nx, ny), 0
+## from player import BubPlayer
+## if BubPlayer.Moebius:
+## nx = bwidth - 2*CELL - nx
+## return (nx, ny), 1
+## else:
+## return (nx, ny), 0
+
+
+MODULES = ['boards', 'bonuses', 'bubbles', 'images',
+ 'mnstrmap', 'monsters', 'player', 'ranking',
+ 'binboards', 'macbinary', 'boarddef',
+ 'ext1', 'ext2', 'ext3', 'ext4', 'ext5', 'ext6', 'ext7']
+
+def loadmodules(force=0):
+ levelfilename = gamesrv.game.levelfile
+ modulefiles = {None: levelfilename}
+ for m in MODULES:
+ if os.path.isfile(m+'.py'):
+ modulefiles[m] = m+'.py'
+ elif os.path.isfile(os.path.join(m, '__init__.py')):
+ modulefiles[m] = os.path.join(m, '__init__.py')
+ mtimes = {}
+ for m, mfile in modulefiles.items():
+ mtimes[m] = os.stat(mfile).st_mtime
+ reload = force or (mtimes != getattr(sys, 'ST_MTIMES', None))
+ import player
+ playerlist = player.BubPlayer.PlayerList
+ if reload:
+ delete = hasattr(sys, 'ST_MTIMES')
+ sys.ST_MTIMES = mtimes
+ if delete:
+ print "Reloading modules."
+ for m, mfile in modulefiles.items():
+ if m is not None and m in sys.modules:
+ del sys.modules[m]
+
+ # Clear
+ clearallsprites()
+
+ import player
+ for p in playerlist:
+ player.upgrade(p)
+ for n in range(len(playerlist), images.MAX):
+ playerlist.append(player.BubPlayer(n))
+ player.BubPlayer.PlayerList = playerlist
+ if reload:
+ import boards
+ from images import haspat, loadpattern
+ boards.haspat = haspat
+ boards.loadpattern = loadpattern
+ del boards.BoardList[:]
+ if levelfilename.lower().endswith('.py'):
+ levels = {}
+ print 'Source level file:', levelfilename
+ execfile(levelfilename, levels)
+ if 'GenerateLevels' in levels:
+ levels = levels['GenerateLevels']()
+ if isinstance(levels, list):
+ levels = dict(zip(range(len(levels)), levels))
+ else:
+ import binboards
+ levels = binboards.load(levelfilename)
+ boards.register(levels)
+ return reload
+
+def clearallsprites():
+ gamesrv.clearsprites()
+ import images
+ del images.ActiveSprites[:]
+ images.SpritesByLoc.clear()
+
+def wait_for_one_player():
+ from player import BubPlayer
+ clearallsprites()
+ nimages = None
+ while not [p for p in BubPlayer.PlayerList if p.isplaying()]:
+ yield 3
+ if not nimages:
+ desc = getattr(gamesrv.game, 'FnDesc', '?')
+ host, port = getattr(gamesrv.game, 'address', ('?', '?'))
+ images.writestrlines([
+ "Welcome to",
+ desc.upper(),
+ "at %s:%s" % (host.lower(), port),
+ None,
+ "Click on your Favorite Color's Dragon",
+ "Choose four keys: Right, Left, Jump, Shoot",
+ "and Let's Go!",
+ None,
+ "Click again for more than one player",
+ "on the same machine.",
+ ])
+
+ from mnstrmap import PlayerBubbles
+ nimages = [PlayerBubbles.bubble[0],
+ PlayerBubbles.bubble[1],
+ PlayerBubbles.bubble[1],
+ PlayerBubbles.bubble[0],
+ PlayerBubbles.bubble[2],
+ PlayerBubbles.bubble[2]]
+ screenwidth = bwidth + 9*CELL
+ screenheight = bheight
+
+ def welcomebubbling(self):
+ fx = self.x
+ dx = (random.random() - 0.5) * 1.9
+ for y in range(self.y-3, -self.ico.h, -3):
+ fx += dx
+ self.move(int(fx), y)
+ yield None
+ if y == self.ypop:
+ from mnstrmap import PlayerBubbles
+ self.setimages(None)
+ self.gen.append(self.die(PlayerBubbles.explosion))
+ self.kill()
+
+ yield 10
+ gamesrv.set_musics([], [images.music_game2], reset=0)
+
+ if ((not images.ActiveSprites or random.random() < 0.05678) and
+ gamesrv.clients):
+ # make sure the extension's images are loaded too
+ # NB. this is also needed for import auto-detection
+ import ext1; import ext2; import ext3
+ import ext4; import ext5; import ext6
+ import ext7
+
+ ico = images.sprget(nimages[0])
+ s = images.ActiveSprite(ico,
+ random.randrange(0, screenwidth-ico.w),
+ screenheight)
+ s.ypop = random.randrange(-ico.h, screenheight)
+ s.gen = [welcomebubbling(s)]
+ s.setimages(s.cyclic(nimages, speed=1))
+ if random.random() > 0.4321:
+ try:
+ key, (filename, (x, y, w, h)) = random.choice(
+ images.sprmap.items())
+ except:
+ w = h = 0
+ if w == h == 32:
+ s2 = images.ActiveSprite(images.sprget(key), -32, 0)
+ s2.gen = [s2.following(s, (s.ico.w-32)//2, (s.ico.h-32)//2)]
+ s.ypop = None
+ images.action(images.ActiveSprites[:])
+
+def patget(n, keycol=None):
+ bitmap, rect = loadpattern(n, keycol)
+ return bitmap.geticon(*rect)
+
+def get_lives():
+ return gamesrv.game.limitlives
+
+def do_nothing():
+ while True:
+ yield 5
+
+BoardList = []
+curboard = None
+BoardGen = [do_nothing()]
+
+def next_board(num=0, complete=1, fastreenter=False):
+ yield force_singlegen()
+ set_frametime(1.0)
+ brd = curboard
+ inplace = 0
+ if brd:
+ inplace = brd.bonuslevel or fastreenter
+ num = brd.num
+ if not inplace:
+ num += gamesrv.game.stepboard
+ if num >= len(BoardList):
+ num = len(BoardList)-1
+ if (gamesrv.game.finalboard is not None and num > gamesrv.game.finalboard):
+ num = gamesrv.game.finalboard
+ for t in brd.leave(inplace=inplace):
+ yield t
+
+ # reset global board state
+ from player import BubPlayer, reset_global_board_state
+ reset_global_board_state()
+ if not inplace:
+ del BubPlayer.MonsterList[:]
+ # wait for at least one player
+ for t in wait_for_one_player():
+ yield t
+ # reload modules if changed
+ if loadmodules():
+ import boards
+ boards.BoardGen = [boards.next_board(num)]
+ return
+
+ if num < 0:
+ num = 0
+ elif num >= len(BoardList):
+ num = len(BoardList)-1
+ brd = BoardList[num](num)
+ for t in brd.enter(complete, inplace=inplace, fastreenter=fastreenter):
+ yield t
+
+ if brd.bonuslevel:
+ gen = bonus_play
+ else:
+ gen = normal_play
+ BoardGen[0] = gen()
+
+def set_frametime(ft, privtime=100):
+ from player import BubPlayer
+ BubPlayer.BaseFrametime = ft
+ BubPlayer.PlayersPrivateTime = privtime
+ images.loadsounds(1.0 / ft)
+
+def extra_boardgen(gen, at_end=0):
+ if curboard.playingboard:
+ if at_end or not BoardGen:
+ BoardGen.append(gen)
+ else:
+ BoardGen.insert(1, gen)
+
+def replace_boardgen(gen, force=0):
+ if curboard.playingboard or force:
+ curboard.playingboard = 0
+ BoardGen[0] = gen
+
+def force_singlegen():
+ del BoardGen[1:]
+ return 0
+
+def has_singlegen():
+ return len(BoardGen) <= 1
+
+def display_hat(p, d):
+ if p.team == -1 or getattr(d,'isdying',0) or hasattr(d,'no_hat'):
+ return
+ try:
+ bottom_up = d.bottom_up()
+ except AttributeError:
+ bottom_up = 0
+ try:
+ image = ('hat', p.team, d.dir, d.hatangle)
+ except AttributeError:
+ image = ('hat', p.team)
+ if bottom_up:
+ image = 'vflip', image
+ y = d.y
+ else:
+ y = d.y - 16
+ ico = images.sprget(image)
+ if (getattr(d,'hatsprite',None) is None or
+ not d.hatsprite.alive):
+ d.hatsprite = images.ActiveSprite(ico, d.x, y)
+ else:
+ d.hatsprite.to_front()
+ d.hatsprite.move(d.x, y, ico)
+ d.hatsprite.gen = [d.hatsprite.die([None])]
+
+def normal_frame():
+ from player import BubPlayer
+ BubPlayer.FrameCounter += 1
+
+ # main generator dispatch loop
+ images.action(images.ActiveSprites[:])
+
+ frametime = 10
+ for p in BubPlayer.PlayerList:
+ if p.isplaying():
+ frametime = BubPlayer.BaseFrametime
+ p.zarkon()
+ for d in p.dragons:
+ d.to_front()
+ display_hat(p, d)
+ d.prefix(p.pn)
+ if not (BubPlayer.FrameCounter & 31):
+ gamesrv.compactsprites()
+ reset = getattr(BubPlayer, 'MultiplyerReset', 0)
+ if reset and BubPlayer.FrameCounter >= reset:
+ BubPlayer.MultiplyerReset = 0
+ set_frametime(1.0)
+ return frametime
+
+def normal_play():
+ from player import BubPlayer
+ import bonuses
+ framecounter = 0
+ bonus_callback = bonuses.start_normal_play()
+ while BubPlayer.MonsterList:
+ bonus_callback()
+ yield normal_frame()
+ if not BubPlayer.DragonList:
+ continue
+ framecounter += 1
+ BASE = 500
+ if not (framecounter % BASE):
+ if framecounter == 4*BASE:
+ from monsters import Monster
+ from mnstrmap import BigImages
+ ico = images.sprget(BigImages.hurryup[1])
+ s = images.ActiveSprite(ico, (bwidth-ico.w)//2, (bheight-ico.h)//2)
+ s.setimages(s.die(BigImages.hurryup * 12, 2))
+ images.Snd.Hurry.play()
+ mlist = [s for s in images.ActiveSprites
+ if (isinstance(s, Monster) and s.regular() and
+ not s.angry)]
+ if mlist:
+ s = random.choice(mlist)
+ s.angry = [s.genangry()]
+ s.resetimages()
+ if framecounter >= 6*BASE:
+ mlist = [s for s in images.ActiveSprites
+ if isinstance(s, Monster) and s.regular() and s.angry]
+ if mlist:
+ images.Snd.Hell.play()
+ gamesrv.set_musics([], [])
+ s = random.choice(mlist)
+ s.become_ghost()
+ framecounter = -200
+ else:
+ framecounter = 2*BASE
+ if framecounter == 0:
+ curboard.set_musics()
+ replace_boardgen(last_monster_killed(), 1)
+
+##def normal_play():
+## # TESTING!!
+## from player import BubPlayer
+## for p in BubPlayer.PlayerList:
+## if not p.icons:
+## p.loadicons(p.icons, images.sprget)
+## results = {BubPlayer.PlayerList[0]: 100,
+## BubPlayer.PlayerList[1]: 200,
+## BubPlayer.PlayerList[2]: 300,
+## BubPlayer.PlayerList[3]: 400,
+## BubPlayer.PlayerList[4]: 100,
+## BubPlayer.PlayerList[5]: 200,
+## BubPlayer.PlayerList[6]: 300,
+## BubPlayer.PlayerList[7]: 400,
+## BubPlayer.PlayerList[8]:1000,
+## BubPlayer.PlayerList[9]:1000,
+## }
+## maximum = None
+## for t in result_ranking(results, maximum):
+## yield t
+
+def last_monster_killed(end_delay=390, music=None):
+ from player import BubPlayer
+ for t in exit_board(music=music):
+ yield t
+ if curboard.bonuslevel:
+ curboard.playingboard = 1
+ for t in bonus_play():
+ yield t
+ end_delay -= 1
+ if end_delay <= 0:
+ replace_boardgen(next_board(), 1)
+ break
+ else:
+ for i in range(end_delay):
+ yield normal_frame()
+ replace_boardgen(next_board(), 1)
+
+##def bonus_play():
+## from player import BubPlayer
+## import bubbles
+## while BubPlayer.LimitScoreColor is None:
+## yield normal_frame()
+## players = [(p.points, p.pn) for p in BubPlayer.PlayerList
+## if p.isplaying()]
+## if players:
+## players.sort()
+## points, BubPlayer.LimitScoreColor = players[-1]
+## BubPlayer.LimitScore = ((points + limit) // 100000) * 100000
+## for p in BubPlayer.PlayerList:
+## if p.isplaying():
+## p.givepoints(0) # check LimitScore and update scoreboard()
+## while not (BubPlayer.BubblesBecome or BubPlayer.MegaBonus):
+## if random.random() < 0.06:
+## bubbles.newbonusbubble()
+## yield normal_frame()
+## # special board end
+## import monsters
+## monsters.argh_em_all()
+## replace_boardgen(last_monster_killed(), 1)
+
+class TimeCounter(Copyable):
+ def __init__(self, limittime, blink=0):
+ from player import BubPlayer
+ self.saved_time = BubPlayer.LimitTime
+ self.time = limittime / FRAME_TIME
+ self.prev = None
+ self.blink = blink
+ def update(self, t):
+ from player import BubPlayer, scoreboard
+ self.time -= t
+ if self.time < 0.0:
+ self.time = 0.0
+ BubPlayer.LimitTime = self.time * FRAME_TIME
+ next = int(BubPlayer.LimitTime)
+ if self.blink and BubPlayer.LimitTime - next >= 0.5:
+ BubPlayer.LimitTime = next = None
+ if self.prev != next:
+ scoreboard(compresslimittime=1)
+ self.prev = next
+ def restore(self):
+ from player import BubPlayer
+ BubPlayer.LimitTime = self.saved_time
+
+def bonus_play():
+ from player import BubPlayer
+ import bubbles
+ BubPlayer.MegaBonus = None
+ BubPlayer.BubblesBecome = None
+ Time0 = 5.0 / FRAME_TIME # when to slow down time
+ tc = TimeCounter(BubPlayer.LimitTime or 180.9) # 3:00
+ prev = None
+ while not (BubPlayer.BubblesBecome or BubPlayer.MegaBonus):
+ if random.random() < 0.099:
+ bubbles.newbonusbubble()
+ t = normal_frame()
+ tc.update(t)
+ if tc.time < Time0:
+ if tc.time <= 0.5:
+ tc.time = 0.5
+ BubPlayer.LimitTime = 0.0
+ t *= math.sqrt(Time0 / tc.time)
+ yield t
+ if tc.time == 0.5:
+ gamesrv.game.End = 'gameover'
+ gamesrv.game.updateboard()
+ replace_boardgen(game_over(), 1)
+ return
+ # special board end
+ import monsters
+ monsters.argh_em_all()
+ replace_boardgen(last_monster_killed(), 1)
+
+def game_over():
+ yield force_singlegen()
+ from player import scoreboard
+ import ranking
+ images.Snd.Extralife.play()
+ gamesrv.set_musics([], [images.music_potion])
+ scoreboard()
+ for t in ranking.game_over():
+ yield t
+
+def game_reset():
+ import time
+ from player import BubPlayer
+ t1 = time.time()
+ while 1:
+ yield 0
+ if BubPlayer.LimitTime and BubPlayer.LimitTime >= 1.0:
+ # someone else ticking the clock, try again later
+ return
+ if abs(time.time() - t1) > 2.0:
+ break
+ # anyone playing ?
+ if not gamesrv.game.End:
+ return # yes -> cancel game_reset()
+ # let's tick the clock !
+ tc = TimeCounter(60.9, blink=1) # 1:00
+ t1 = time.time()
+ while tc.time:
+ yield 0
+ # anyone playing now ?
+ if not gamesrv.game.End:
+ tc.restore()
+ return # yes -> cancel game_reset()
+ t = time.time() # use real time
+ deltat = (t-t1)/FRAME_TIME
+ if deltat < 1.0:
+ deltat = 1.0
+ elif deltat > 100.0:
+ deltat = 100.0
+ tc.update(deltat)
+ t1 = t
+ gamesrv.game.reset()
+
+##def wasting_play():
+## from player import BubPlayer, scoreboard
+## import bubbles
+## curboard.wastingplay = {}
+## for p in BubPlayer.PlayerList:
+## if p.isplaying():
+## p.letters = {}
+## p.bonbons = p.points // 50000
+## scoreboard()
+
+## while len(BubPlayer.DragonList) > 1:
+## if random.random() < 0.03:
+## bubbles.newbubble(1)
+## yield normal_frame()
+## for d in BubPlayer.DragonList:
+## curboard.wastingplay[d.bubber] = len(curboard.wastingplay)
+## for i in range(50):
+## yield normal_frame()
+
+## total = len(curboard.wastingplay)
+## results = [(total-n, p) for p, n in curboard.wastingplay.items()]
+## results.sort()
+## results = [(p, str(n)) for n, p in results]
+## for t in display_ranking(results):
+## yield t
+## # never ending
+
+def skiplevels(blink, skip):
+ # (not used any more)
+ saved = BoardGen[:]
+ while skip:
+ skip -= 1
+ BoardGen[:] = saved
+ for i in range(10): # frozen pause
+ yield 3
+ if blink:
+ blink.step(-bwidth, 0)
+ yield 3.33
+ blink.step(bwidth, 0)
+ blink = None
+ for t in next_board(complete=(skip==0)):
+ yield t
+
+def exit_board(delay=8, music=None, repeatmusic=[]):
+ from bubbles import Bubble
+ from bonuses import RandomBonus, end_normal_play
+ from player import BubPlayer
+ from monsters import Monster
+ end_normal_play()
+ curboard.playingboard = 0
+ actives = images.ActiveSprites[:]
+ for s in actives:
+ if ((isinstance(s, Monster) and s.still_playing())
+ or isinstance(s, RandomBonus)):
+ s.kill()
+ music = music or []
+ if BubPlayer.MegaBonus:
+ music[:1] = [images.music_modern]
+ if music or repeatmusic:
+ gamesrv.set_musics(music, repeatmusic)
+ for i in range(delay):
+ yield normal_frame()
+ bubble_outcome = BubPlayer.BubblesBecome or Bubble.pop
+ for s in actives:
+ if isinstance(s, Bubble):
+ bubble_outcome(s)
+ yield normal_frame()
+ if BubPlayer.MegaBonus:
+ BubPlayer.MegaBonus()
+
+def potion_fill(blist, big=0):
+ from player import BubPlayer
+ from bonuses import Bonus
+ #timeleft = 1680.0
+ for t in exit_board(0, music=[images.music_potion]):
+ #timeleft -= t
+ yield t
+ notes = all_notes = []
+ y = 1
+ while y < 11 or (y < height-2 and (len(all_notes) < 10 or big)):
+ for x in range(2, width-3, 2):
+ if ' ' == bget(x,y) == bget(x+1,y) == bget(x,y+1) == bget(x+1,y+1):
+ b = Bonus(x*CELL, y*CELL, falling=0, *blist[((x+y)//2)%len(blist)])
+ b.timeout = (444,666)[big]
+ all_notes.append(b)
+ for i in range(2):
+ t = normal_frame()
+ #timeleft -= t
+ yield t
+ y += 2
+ while notes: #and timeleft > 0.0:
+ notes = [b for b in notes if b.alive]
+ t = normal_frame()
+ #timeleft -= t
+ yield t
+ for i in range(10):
+ t = normal_frame()
+ #timeleft -= t
+ yield t
+ results = {}
+ for b in all_notes:
+ for d in b.taken_by:
+ bubber = d.bubber
+ results[bubber] = results.get(bubber, 0) + 1
+ for t in result_ranking(results, len(all_notes)):
+ yield t
+ #fadeouttime = 3.33
+ #fullsoundframes = bonusframes - 10 - int(fadeouttime / FRAME_TIME)
+ #for i in range(fullsoundframes):
+ # yield normal_frame()
+ #gamesrv.fadeout(fadeouttime)
+ #for i in range(fullsoundframes, 490):
+ # yield normal_frame()
+
+def result_ranking(results, maximum=None, timeleft=200):
+ import ranking
+ results = ranking.ranking_picture(results, maximum, timeleft is not None)
+ if curboard.bonuslevel and timeleft is not None:
+ play_again = bonus_play()
+ else:
+ play_again = None
+ for t in ranking.display(results, timeleft, play_again):
+ yield t
+ if gamesrv.game.End != 'gameover':
+ gamesrv.set_musics([], [])
+ replace_boardgen(next_board(), 1)
+
+def extra_water_flood():
+ from mnstrmap import Flood
+ from monsters import Monster
+ waves_icons = [images.sprget(n) for n in Flood.waves]
+ fill_icon = images.sprget(Flood.fill)
+ bspr = []
+ if 'flood' in curboard.sprites:
+ return # only one flooding at a time
+ curboard.sprites['flood'] = bspr
+ waves_sprites = [gamesrv.Sprite(waves_icons[0], x, bheight-CELL)
+ for x in range(0, bwidth, CELL)]
+ bspr += waves_sprites
+ fill_by_line = []
+ poplist = [None]
+ while waves_sprites[0].y > 0:
+ yield 0
+ waves_icons.insert(0, waves_icons.pop())
+ for s in waves_sprites:
+ s.seticon(waves_icons[0])
+ yield 0
+ sprites = [gamesrv.Sprite(fill_icon, s.x, s.y) for s in waves_sprites]
+ bspr += sprites
+ fill_by_line.append(sprites)
+ for s in waves_sprites:
+ s.step(0, -16)
+ for s in images.touching(0, waves_sprites[0].y, bwidth, bheight):
+ if isinstance(s, Monster):
+ s.argh(poplist)
+ while 1:
+ for i in range(2):
+ yield 0
+ waves_icons.insert(0, waves_icons.pop())
+ for s in waves_sprites:
+ s.seticon(waves_icons[0])
+ if not fill_by_line:
+ break
+ for s in fill_by_line.pop():
+ s.kill()
+ for s in waves_sprites:
+ s.step(0, 16)
+ for s in waves_sprites:
+ s.kill()
+ del curboard.sprites['flood']
+
+def extra_aquarium():
+ from mnstrmap import Flood
+ from player import BubPlayer
+ for i in range(200):
+ if 'flood' not in curboard.sprites: # only one flooding at a time
+ break
+ yield 0
+ if curboard.cleaning_gen_state:
+ return
+ else:
+ return
+ curboard.sprites['flood'] = []
+ gl = curboard.sprites.setdefault('background', [])
+ curboard.holes = True # so that random PlainBubbles show up anyway
+ walls = curboard.sprites['walls']
+ seen = {}
+
+ def newsprite(ico, x, y):
+ s = gamesrv.Sprite(ico, x, y)
+ s.to_back(walls[0])
+ gl.append(s)
+ return s
+
+ def fishplayers(ymin):
+ for d in BubPlayer.DragonList:
+ if d not in seen and d.y >= ymin:
+ seen[d] = True
+ d.become_fish()
+ d.bubber.emotic(d, 4)
+
+ waves_icons = [images.sprget(n) for n in Flood.waves]
+ fill_icon = images.sprget(Flood.fill)
+ waves_sprites = [newsprite(waves_icons[0], x, bheight-CELL)
+ for x in range(2*CELL + HALFCELL, bwidth - 2*CELL, CELL)]
+ while waves_sprites[0].y > -fill_icon.h:
+ fishplayers(waves_sprites[0].y)
+ yield 0
+ waves_icons.append(waves_icons.pop(0))
+ for s in waves_sprites:
+ s.seticon(waves_icons[0])
+ fishplayers(waves_sprites[0].y)
+ yield 0
+ for s in waves_sprites:
+ newsprite(fill_icon, s.x, s.y)
+ for s in waves_sprites:
+ s.step(0, -16)
+ for s in waves_sprites:
+ s.kill()
+ BubPlayer.SuperFish = True
+ fishplayers(-sys.maxint)
+
+def extra_walls_falling():
+ walls_by_pos = curboard.walls_by_pos
+ moves = 1
+ while moves and not curboard.cleaning_gen_state:
+ moves = 0
+ for y in range(height-3, -1, -1):
+ for x in range(2, width-2):
+ if ((y,x) in walls_by_pos and
+ (y+1,x) not in walls_by_pos and
+ (y+2,x) not in walls_by_pos):
+ y0 = y
+ while (y0-1,x) in walls_by_pos:
+ y0 -= 1
+ w = curboard.killwall(x, y0, 0)
+ curboard.putwall(x, y+1, w)
+ moves = 1
+ curboard.reorder_walls()
+ for y in range(6):
+ yield 0
+
+def single_blocks_falling(xylist):
+ walls_by_pos = curboard.walls_by_pos
+ while xylist:
+ newlist = []
+ for x, y in xylist:
+ if ((y,x) in walls_by_pos and (y+1,x) not in walls_by_pos and
+ y < curboard.height-1):
+ newlist.append((x, y+1))
+ for x, y in newlist:
+ w = curboard.killwall(x, y-1, 0)
+ curboard.putwall(x, y, w)
+ xylist = newlist
+ curboard.reorder_walls()
+ for i in range(7):
+ yield 0
+
+def extra_display_repulse(cx, cy, dlimit=5000, dfactor=1000):
+ offsets = {}
+ for s in gamesrv.sprites_by_n.values():
+ x, y = s.getdisplaypos()
+ if x is not None:
+ dx = x - cx
+ dy = y - cy
+ d = dx*dx + dy*dy + 100
+ if d <= dlimit:
+ dx = (dx*dfactor)//d
+ dy = (dy*dfactor)//d
+ offsets[s] = dx, dy
+ s.setdisplaypos(int(x+dx), int(y+dy))
+ yield 0
+ yield 0
+ while offsets:
+ prevoffsets = offsets
+ offsets = {}
+ for s, (dx, dy) in prevoffsets.items():
+ if s.alive:
+ if dx < 0:
+ dx += max(1, (-dx)//5)
+ elif dx:
+ dx -= max(1, dx//5)
+ if dy < 0:
+ dy += max(1, (-dy)//5)
+ elif dy:
+ dy -= max(1, dy//5)
+ if dx or dy:
+ offsets[s] = dx, dy
+ s.setdisplaypos(int(s.x+dx), int(s.y+dy))
+ yield 0
+
+def extra_bkgnd_black(cx, cy):
+ gl = curboard.sprites.get('background')
+ dist = 0
+ while gl:
+ dist += 17
+ dist2 = dist * dist
+ gl2 = []
+ for s in gl:
+ if (s.x-cx)*(s.x-cx) + (s.y-cy)*(s.y-cy) < dist2:
+ s.kill()
+ else:
+ gl2.append(s)
+ gl[:] = gl2
+ yield 0
+
+def extra_light_off(timeout, icocache={}):
+ for i in range(timeout):
+ if curboard.cleaning_gen_state:
+ break
+ dragons = {}
+ import player
+ playerlist = player.BubPlayer.PlayerList
+ for bubber in playerlist:
+ for dragon in bubber.dragons:
+ dragons[dragon] = True
+ for s in gamesrv.sprites_by_n.values():
+ try:
+ ico = icocache[s.ico, s in dragons]
+ except KeyError:
+ ico = images.make_darker(s.ico, s in dragons)
+ icocache[s.ico, s in dragons] = ico
+ s.setdisplayicon(ico)
+ yield 0
+ for s in gamesrv.sprites_by_n.values():
+ s.setdisplayicon(s.ico)
+
+def extra_swap_up_down(N=27):
+ # unregister all walls
+ walls = curboard.walls_by_pos.items()
+ walls.sort()
+ if not walls:
+ return
+ curboard.walls_by_pos.clear()
+ emptyline = '##' + ' '*(width-4) + '##'
+ curboard.walls = [emptyline] * height
+ l = curboard.sprites['walls']
+ wallicon = l[0].ico
+ wallpool = l[:]
+ l[:] = [gamesrv.Sprite(wallicon, 0, -wallicon.h)]
+
+ # force the top half of the walls on front
+ #for (y,x), w in walls:
+ # if y*2 < height:
+
+ # show the walls swapping up/down
+ ycenter = ((height-1)*CELL) // 2
+ for i in range(N):
+ alpha = math.cos((math.pi*(i+1))/N)
+ ymap = {}
+ for y in range(height):
+ ymap[y] = int(alpha*(y*CELL-ycenter)) + ycenter
+ for (y,x), w in walls:
+ if y in ymap:
+ w.move(x*CELL, ymap[y])
+ yield 0
+ if i == (N+1)//2:
+ # reorder the wall sprites in the middle of the swap
+ walls = [((-y,x), w) for (y,x), w in walls]
+ walls.sort()
+ for i in range(len(walls)):
+ (y,x), w = walls[i]
+ walls[i] = (y,x), wallpool[i]
+ walls = [((-y,x), w) for (y,x), w in walls]
+ walls.sort()
+ # reverse all dragons!
+ from player import BubPlayer
+ for dragon in BubPlayer.DragonList:
+ dragon.dcap['gravity'] *= -1.0
+
+ # freeze the walls in their new position
+ i = 0
+ for (y,x), w in walls:
+ y = height-1 - y
+ if 0 <= y < height and (y,x) not in curboard.walls_by_pos:
+ w = wallpool[i]
+ i += 1
+ curboard.putwall(x, y, w)
+ l[:0] = wallpool[:i]
+ for w in wallpool[i:]:
+ w.kill()
+ curboard.reorder_walls()
+
+def extra_catch_all_monsters(dragons=[], everything=False):
+ from monsters import Monster
+ from bubbles import BigBubbleCatcher
+ from bonuses import Bonus, BonusMaker
+ if not dragons:
+ from player import BubPlayer
+ dragons = BubPlayer.DragonList
+ i = 0
+ give_up = 33
+ for s in images.ActiveSprites[:]:
+ if curboard.cleaning_gen_state:
+ break
+ while not dragons:
+ give_up -= 1
+ if give_up == 0:
+ return
+ yield 0
+ if not s.alive or not s.touchable:
+ continue
+ if isinstance(s, Bonus):
+ ok = s.bubblable
+ elif isinstance(s, BonusMaker):
+ ok = everything
+ else:
+ ok = isinstance(s, Monster)
+ if ok:
+ dragon = dragons[i%len(dragons)]
+ BigBubbleCatcher(dragon, s, 542 + 17*i)
+ i += 1
+ yield 0
+ yield 0
+
+def extra_make_random_level(cx=None, cy=None, repeat_delay=200):
+ from bonuses import DustStar
+ # generate any random level
+ localdir = os.path.dirname(__file__)
+ filename = os.path.join(localdir, 'levels', 'RandomLevels.py')
+ d = {}
+ execfile(filename, d)
+ Level = d['GenerateSingleLevel'](curboard.width, curboard.height)
+ lvl = Level(curboard.num)
+ walllist = []
+ if cx is None: cx = bwidth // 2
+ if cy is None: cy = bheight // 2
+ for y in range(curboard.height):
+ dy = cy - (y*16+8)
+ dy2 = dy*dy*0.75
+ for x in range(2, curboard.width-2):
+ dx = cx - (x*16+8)
+ d2 = dx*dx + dy2
+ walllist.append((d2, x, y))
+ walllist.sort()
+
+ # dynamically replace the current level's walls with the new level's
+ dist = 0
+ speedf = 15.0
+ added = 0
+ for d2, x, y in walllist:
+ while d2 > dist*dist:
+ if added:
+ curboard.reorder_walls()
+ added = 0
+ yield 0
+ dist += 4.1
+ speedf *= 0.99
+ if curboard.walls[y][x] == ' ':
+ if lvl.walls[y][x] == ' ':
+ continue
+ else:
+ curboard.putwall(x, y)
+ added = 1
+ big = 1
+ else:
+ if lvl.walls[y][x] == ' ':
+ curboard.killwall(x, y)
+ big = 1
+ else:
+ big = 0
+ sx = x*16
+ sy = y*16
+ DustStar(sx - 8*big,
+ sy - 8*big,
+ speedf * (sx-cx) / dist,
+ speedf * (sy-cy) / dist,
+ big=big)
+ # patch the winds too
+ curboard.winds = lvl.winds
+ curboard.reorder_walls()
+ yield 0
+ # wait a bit and restart
+ if repeat_delay < 1000:
+ for i in range(repeat_delay):
+ yield 0
+ if curboard.cleaning_gen_state:
+ return
+ extra_boardgen(extra_make_random_level(
+ repeat_delay = repeat_delay * 3 // 2))
+
+def extra_bubbles(timeout):
+ from bubbles import newforcedbubble
+ falloff = 0.25
+ L = math.log(0.965) # same speed falloff rate as in throwing_bubble()
+ cx = (bwidth - CELL) // 2
+ cy = (bheight - CELL) // 2
+ for i in range(timeout):
+ if curboard.cleaning_gen_state:
+ return
+ if random.random() < falloff:
+ bubble = newforcedbubble()
+ if bubble:
+ tx = random.randrange(CELL, bwidth - 2*CELL) - cx
+ ty = random.randrange(CELL, bheight - 2*CELL) - cy
+ if ty == 0:
+ ty = 1
+ dist = math.sqrt(tx * tx + ty * ty)
+ acos = tx / dist
+ asin = ty / dist
+ hspeed = 4 - dist * L
+ bubble.thrown_bubble(cx, cy, hspeed, (acos, asin))
+ falloff *= 0.998
+ yield 0
+
+
+def initsubgame(music, displaypoints):
+ from player import BubPlayer, scoreboard
+ for t in exit_board(0, repeatmusic=[music]):
+ yield t
+ BubPlayer.DisplayPoints = displaypoints
+ scoreboard()
+ for t in curboard.clean_gen_state():
+ yield t
+
+def register(dict):
+ global width, height, bwidth, bheight, bheightmod
+ items = dict.items()
+ items.sort()
+ for name, board in items:
+ try:
+ if not issubclass(board, Board) or board is Board:
+ continue
+ except TypeError:
+ continue
+ BoardList.append(board)
+ # check sizes
+ assert BoardList, "board file does not define any board"
+ B = BoardList[0]
+ try:
+ test = B(-1)
+ width = test.width
+ height = test.height
+ for B in BoardList[1:]:
+ test = B(-1)
+ assert test.width == width, "some boards have a different width"
+ assert test.height == height, "some boards have a different height"
+ except Exception, e:
+ print 'Caught "%s" in level "%s":' % (e, B.__name__)
+ raise e
+ bwidth = width*CELL
+ bheight = height*CELL
+ bheightmod = (height+2)*CELL
+
+##def define_boards(filename):
+## global curboard, boards, width, height, bwidth, bheight, bheightmod
+## curboard = None
+## boards = []
+## def board((wallfile, wallrect), shape):
+## lines = shape.strip().split('\n')
+## bmp = gamesrv.getbitmap(wallfile)
+## wallicon = bmp.geticon(*wallrect)
+## boards.append(Board(lines, wallicon))
+## d = {'board': board}
+## execfile(filename, d)
+## assert boards, "board file does not define any board"
+## width = boards[0].width
+## height = boards[0].height
+## for b in boards[1:]:
+## assert b.width == width, "some boards have a different width"
+## assert b.height == height, "some boards have a different height"
+## bwidth = width*CELL
+## bheight = height*CELL
+## bheightmod = len(boards[0].lines)*CELL
+
+
+#try:
+# import psyco
+#except ImportError:
+# pass
+#else:
+# psyco.bind(normal_frame)
diff --git a/bubbob/bonuses.py b/bubbob/bonuses.py
new file mode 100644
index 0000000..6873188
--- /dev/null
+++ b/bubbob/bonuses.py
@@ -0,0 +1,2504 @@
+from __future__ import generators
+import random, os, math
+import random as random_module
+import gamesrv
+import images
+import boards
+from boards import *
+from images import ActiveSprite
+from mnstrmap import GreenAndBlue, Bonuses, Diamonds, Stars, BigImages
+from mnstrmap import PotionBonuses, Fire
+from player import BubPlayer
+
+
+questionmarklist = ['questionmark3',
+ 'questionmark4',
+ 'questionmark5',
+ 'questionmark4',
+ 'questionmark3',
+ 'questionmark2',
+ 'questionmark1',
+ 'questionmark2']
+
+class Bonus(ActiveSprite):
+ bubblable = 1
+ touchable = 1
+ points = 750
+ timeout = 250
+ sound = 'Fruit'
+ endaction = None
+ multiply = 1
+ killgens = 1
+
+ def __init__(self, x, y, nimage=None, points=None, falling=1):
+ if nimage is not None:
+ self.nimage = nimage
+ if points is not None:
+ self.points = points
+ ActiveSprite.__init__(self, images.sprget(self.nimage), x, y)
+ self.taken_by = []
+ self.gen.append(self.timeouter())
+ if falling:
+ self.gen.append(self.faller())
+
+ def buildoutcome(self):
+ return (self.__class__,)
+
+ def faller(self):
+ while self.y < boards.bheight:
+ if onground_nobottom(self.x, self.y):
+ yield None
+ yield None
+ else:
+ self.move(self.x, (self.y+4) & ~3)
+ yield None
+ self.kill()
+
+ def timeouter(self):
+ for i in range(self.timeout):
+ yield None
+ if self.timeout:
+ self.kill()
+
+ def touched(self, dragon):
+ dx, dy, dw, dh = dragon.x, dragon.y, dragon.ico.w, dragon.ico.h
+ if (dx + dw > self.x + 10 and
+ dy + dh > self.y + 8 and
+ self.x + self.ico.w > dx + 10 and
+ self.y + self.ico.h > dy + 10):
+ self.reallytouched(dragon)
+
+ def reallytouched(self, dragon):
+ if not self.taken_by:
+ if self.killgens:
+ self.gen = []
+ self.gen.append(self.taking())
+ sound = self.sound
+ if sound:
+ if isinstance(sound, str):
+ sound = getattr(images.Snd, sound)
+ self.play(sound)
+ if dragon not in self.taken_by:
+ self.taken_by.append(dragon)
+ if isinstance(self, (RandomBonus, MonsterBonus)):
+ s_bonus = dragon.bubber.stats.setdefault('bonus', {})
+ s_bonus[self.nimage] = s_bonus.get(self.nimage, 0) + 1
+
+ def taking(self, follow_dragons=0, delay=1):
+ from player import Dragon
+ for t in range(delay):
+ yield None # time to be taken by several dragons
+ if self.points:
+ for p in self.taken_by:
+ if follow_dragons and p.alive:
+ s = p
+ else:
+ s = self
+ points(s.x + s.ico.w//2, s.y + s.ico.h//2 - CELL, p, self.points)
+ dragons = [d for d in self.taken_by if isinstance(d, Dragon)]
+ if self.taken1(dragons) != -1:
+ self.kill()
+
+ def taken1(self, dragons):
+ for d in dragons * self.multiply:
+ if d.alive:
+ self.taken(d)
+
+ def taken(self, dragon):
+ pass
+
+ def in_bubble(self, bubble):
+ self.untouchable()
+ bubble.move(self.x, self.y)
+ bubble.to_front()
+ self.to_front()
+ self.gen = [self.bubbling(bubble, self.ico)]
+ self.move(bubble.x+8, bubble.y+8, images.sprget('questionmark3'))
+ self.setimages(self.cyclic(questionmarklist, 2))
+
+ def bubbling(self, bubble, ico):
+ while not hasattr(bubble, 'poplist'):
+ self.move(bubble.x+8, bubble.y+8)
+ yield None
+ if bubble.poplist is not None:
+ dragon = bubble.poplist[0]
+ if dragon is not None:
+ self.play(images.Snd.Yippee)
+ if dragon not in self.taken_by:
+ self.taken_by.append(dragon)
+ if self.points > 10:
+ dragon.bubber.givepoints(self.points - 10)
+ pn = dragon.bubber.pn
+ if self.points in GreenAndBlue.points[pn]:
+ Points(bubble.x + bubble.ico.w//2, bubble.y, pn, self.points)
+ self.taken1(BubPlayer.DragonList)
+ p = Parabolic(ico, bubble.x, bubble.y)
+ p.gen.append(p.moving(-1.0))
+ self.kill()
+
+ def is_on_ground(self):
+ return onground(self.x, self.y)
+
+
+def points(x, y, dragon, points):
+ dragon.bubber.givepoints(abs(points))
+ pn = dragon.bubber.pn
+ if points in GreenAndBlue.points[pn]:
+ Points(x, y, pn, points)
+
+class Points(ActiveSprite):
+
+ def __init__(self, x, y, pn, points):
+ ico = images.sprget(GreenAndBlue.points[pn][points])
+ ActiveSprite.__init__(self, ico, x - ico.w//2, max(8, y))
+ self.nooverlap = 1
+ self.gen.append(self.raiser())
+
+ def raiser(self):
+ wait = 0
+ for s in images.ActiveSprites:
+ if s is self:
+ break
+ if (isinstance(s, Points) and s.nooverlap and
+ abs(self.x-s.x)<self.ico.w*2//3 and
+ abs(self.y-s.y)<self.ico.h):
+ wait += 5
+ for t in range(wait):
+ yield None
+ for i in range(25):
+ if i == 7:
+ self.nooverlap = 0
+ self.step(0, -2)
+ yield None
+ if self.y <= 0:
+ break
+ for i in range(20):
+ yield None
+ self.kill()
+
+
+class Parabolic(ActiveSprite):
+ fallstraight = 0
+ fallspeed = 4
+
+ def moving(self, y_amplitude = -8.0):
+ bottom_up = self.fallspeed < 0
+ dxy = [(random.random()-0.5) * 15.0,
+ (random.random()+0.5) * y_amplitude * (1,-1)[bottom_up]]
+ if bottom_up:
+ kw = {'gravity': -0.3}
+ else:
+ kw = {}
+ for n in self.parabolic(dxy, self.fallstraight, **kw):
+ progress = self.parabole_progress = dxy[1] * (1,-1)[bottom_up]
+ yield n
+ if progress >= 4.0 and self.fallstraight:
+ del self.parabole_progress
+ self.gen.append(self.falling())
+ return
+ self.kill()
+
+ def falling(self):
+ nx, ny = vertical_warp(self.x, self.y & ~3)
+ if self.fallspeed < 0:
+ groundtest = underground
+ else:
+ groundtest = onground
+ while not groundtest(nx, ny):
+ ny += self.fallspeed
+ nx, ny1 = vertical_warp(nx, ny)
+ if ny1 != ny:
+ ny = ny1
+ self.wrapped_around()
+ self.move(nx, ny)
+ yield None
+ self.move(nx, ny)
+ self.build()
+ self.kill()
+
+ def killmonsters(self, poplist):
+ from monsters import Monster
+ while 1:
+ for s in self.touching(0):
+ if isinstance(s, Monster):
+ s.argh(poplist)
+ yield None
+
+ def build(self):
+ pass
+
+ def wrapped_around(self):
+ pass
+
+
+class Parabolic2(Parabolic):
+ points = 0
+
+ def __init__(self, x, y, imglist, imgspeed=3, onplace=0, y_amplitude=-8.0):
+ Parabolic.__init__(self, images.sprget(imglist[0]), x, y)
+ if onplace:
+ self.gen.append(self.falling())
+ else:
+ self.gen.append(self.moving(y_amplitude))
+ if len(imglist) > 1:
+ self.setimages(self.cyclic(imglist, imgspeed))
+
+ def touched(self, dragon, rect=None):
+ if self.points:
+ points(self.x + self.ico.w/2, self.y + self.ico.h/2 - CELL,
+ dragon, self.points)
+ self.kill()
+
+
+class BonusMaker(Parabolic2):
+ fallstraight = 1
+ touchable = 1
+
+ def __init__(self, x, y, imglist, imgspeed=3, onplace=0, outcome=None):
+ assert outcome
+ self.outcome = outcome
+ if outcome == (Flower2,):
+ self.fallspeed = -self.fallspeed
+ Parabolic2.__init__(self, x, y, imglist, imgspeed, onplace)
+
+ def falling(self):
+ cls = self.outcome[0]
+ if issubclass(cls, Megabonus):
+ self.build()
+ return self.die([])
+ else:
+ return Parabolic2.falling(self)
+
+ def wrapped_around(self):
+ cls = self.outcome[0]
+ if issubclass(cls, RandomBonus) and not boards.curboard.playingboard:
+ self.kill()
+
+ def build(self):
+ cls = self.outcome[0]
+ args = self.outcome[1:]
+ if issubclass(cls, RandomBonus) and not boards.curboard.playingboard:
+ return None
+ else:
+ return cls(self.x, self.y, *args)
+
+ def touched(self, dragon, rect=None):
+ pass
+
+ def in_bubble(self, bubble):
+ bonus = self.build()
+ self.kill()
+ if bonus:
+ bonus.in_bubble(bubble)
+ return bonus
+
+class BonusMakerExtraStar(ActiveSprite):
+
+ def __init__(self, x, y, sx, sy, colorname):
+ self.sx = sx
+ self.sy = sy
+ imglist = [('smstar', colorname, k) for k in range(2)]
+ ActiveSprite.__init__(self, images.sprget(imglist[-1]),
+ x + HALFCELL, y + HALFCELL)
+ self.setimages(self.cyclic(imglist, speed=2))
+
+ def follow_bonusmaker(self, bm):
+ for t in range(4):
+ yield None
+ if hasattr(bm, 'parabole_progress'):
+ break
+ else:
+ self.kill()
+ return
+ start = bm.parabole_progress
+ if start < 3.9:
+ while bm.alive and hasattr(bm, 'parabole_progress'):
+ f = (bm.parabole_progress-start) / (4.0-start)
+ self.move(bm.x + HALFCELL + int(f*self.sx),
+ bm.y + HALFCELL + int(f*self.sy))
+ yield None
+ self.kill()
+
+
+class MonsterBonus(Bonus):
+
+ def __init__(self, x, y, multiple, forceimg=0):
+ self.level = multiple
+ if multiple >= len(Bonuses.monster_bonuses):
+ multiple = len(Bonuses.monster_bonuses) - 1
+ img, pts = Bonuses.monster_bonuses[multiple]
+ Bonus.__init__(self, x, y, forceimg or img, pts)
+
+ def buildoutcome(self):
+ return (self.__class__, self.level)
+
+ def taken(self, dragon):
+ dragon.carrybonus(self, 543)
+
+class IceMonsterBonus(MonsterBonus):
+
+ def __init__(self, x, y, multiple):
+ self.level = multiple
+ if multiple >= 1:
+ img, pts = Bonuses.violet_ice, 750
+ else:
+ img, pts = Bonuses.cyan_ice, 700
+ Bonus.__init__(self, x, y, img, pts)
+
+
+
+class DustStar(ActiveSprite):
+ localrandom = random.Random()
+
+ def __init__(self, x, y, basedx, basedy, big=1, clock=0):
+ self.colorname = self.localrandom.choice(Stars.COLORS)
+ self.imgspeed = self.localrandom.randrange(3, 6)
+ self.rotation_reversed = self.localrandom.random() < 0.5
+ ico, imggen = self.select_ico(getattr(Stars, self.colorname))
+ ActiveSprite.__init__(self, ico, x, y)
+ self.setimages(imggen)
+ self.gen.append(self.fly(basedx, basedy, big))
+ if not big:
+ self.make_small()
+ elif clock:
+ self.setimages(None)
+ self.seticon(images.sprget(Bonuses.clock))
+
+ def select_ico(self, imglist):
+ if self.rotation_reversed:
+ imglist = list(imglist)
+ imglist.reverse()
+ return (images.sprget(imglist[-1]),
+ self.cyclic(imglist, self.imgspeed))
+
+ def make_small(self):
+ images = [('smstar', self.colorname, k) for k in range(2)]
+ ico, imggen = self.select_ico(images)
+ self.seticon(ico)
+ self.setimages(imggen)
+
+ def fly(self, dx, dy, big):
+ random = self.localrandom
+ dx += (random.random() - 0.5) * 2.8
+ dy += (random.random() - 0.5) * 2.8
+ fx = self.x
+ fy = self.y
+ if big:
+ j = 0
+ else:
+ j = 2
+ while j < 3:
+ ttl = random.expovariate(1.0 / 12)
+ if ttl > 35:
+ ttl = 35
+ for i in range(int(ttl)+4):
+ fx += dx
+ fy += dy
+ self.move(int(fx), int(fy))
+ yield None
+ if j == 0:
+ self.make_small()
+ fx += 8
+ fy += 8
+ j += 1
+ self.kill()
+
+
+class RandomBonus(Bonus):
+ timeout = 500
+
+class TemporaryBonus(RandomBonus):
+ captime = 0
+ bonusleveldivider = 2
+ def taken(self, dragon):
+ dragon.dcap[self.capname] += 1
+ self.carried(dragon)
+ def carried(self, dragon):
+ captime = self.captime
+ if boards.curboard.bonuslevel:
+ captime = (captime or 999) // self.bonusleveldivider
+ if captime:
+ dragon.carrybonus(self, captime)
+ else:
+ dragon.carrybonus(self)
+ self.endaction = None
+ def endaction(self, dragon):
+ if dragon.dcap[self.capname] >= 1:
+ dragon.dcap[self.capname] -= 1
+
+
+class ShoeSpeed(RandomBonus):
+ "Fast Runner. Cumulative increase of horizontal speed."
+ nimage = Bonuses.shoe
+ bigbonus = {'multiply': 3}
+ bigdoc = "Run Really Fast."
+ def taken(self, dragon):
+ dragon.dcap['hspeed'] += 1
+ dragon.carrybonus(self)
+
+class CoffeeSpeed(RandomBonus):
+ "Caffeine. Cumulative increase of the horizontal speed and fire rate."
+ nimage = Bonuses.coffee
+ big = 0
+ bigbonus = {'big': 1, 'multiply': 3}
+ bigdoc = "Super-Excited! Break through walls!"
+ def taken(self, dragon):
+ dragon.dcap['hspeed'] += 0.5
+ dragon.dcap['firerate'] += 1
+ if self.big:
+ dragon.dcap['breakwalls'] = 1
+ dragon.carrybonus(self)
+
+class Butterfly(TemporaryBonus):
+ "Lunar Gravity. Allows you to jump twice as high as before."
+ nimage = Bonuses.butterfly
+ big = 0
+ bigbonus = {'big': 1}
+ bigdoc = "Butterflies all around."
+ def taken1(self, dragons):
+ if self.big:
+ import mnstrmap, monsters
+ for i in range(17):
+ monsters.Butterfly(mnstrmap.Butterfly,
+ self.x + random.randrange(-40, 41),
+ self.y + random.randrange(-30, 31),
+ random.choice([-1, 1]))
+ else:
+ TemporaryBonus.taken1(self, dragons)
+ def taken(self, dragon):
+ dragon.dcap['gravity'] *= 0.5
+ self.carried(dragon)
+ def endaction(self, dragon):
+ dragon.dcap['gravity'] *= 2.0
+
+class Cocktail(TemporaryBonus):
+ "Short Lived Bubbles. Makes your bubbles explode more quickly."
+ nimage = Bonuses.cocktail
+ points = 2000
+ capname = 'bubbledelay'
+ bigbonus = {'multiply': 3}
+ bigdoc = "Makes your bubbles explode at once. Dangerous!"
+
+class Extend(RandomBonus):
+ "E X T E N D. Gives you your missing letters and clear the level. "
+ nimage = Bonuses.extend
+ points = 0
+ big = 0
+ bigbonus = {'big': 1}
+ bigdoc = "A lot of letter bubbles! Run! Run!"
+
+ def taken1(self, dragons):
+ if self.big:
+ self.letterexplosion()
+ else:
+ RandomBonus.taken1(self, dragons)
+
+ def taken(self, dragon):
+ from bubbles import extend_name
+ names = [extend_name(l) for l in range(6)]
+ missing = [name for name in names if name not in dragon.bubber.letters]
+ x = dragon.x + dragon.ico.w//2
+ y = dragon.y
+ points(x, y, dragon, 10000*len(missing))
+ for l in range(6):
+ if extend_name(l) in missing:
+ dragon.bubber.giveletter(l, promize=0)
+
+ def letterexplosion(self):
+ from bubbles import LetterBubble
+ playercount = len([p for p in BubPlayer.PlayerList if p.isplaying()])
+ N = 3 + (playercount > 3)
+ angles = [i*(2.0*math.pi/N) for i in range(N)]
+ for l, dx, dy in [(0, 5, 9), (1, 16, 10), (2, 26, 8),
+ (3, 7, 23), (4, 15, 24), (5, 25, 24)]:
+ delta = 2.0*math.pi * random.random()
+ angles = [angle-delta for angle in angles]
+ x = self.x + self.ico.w//2 + 3*(dx-16)
+ y = self.y + self.ico.h//2 + 3*(dy-16)
+ for angle in angles:
+ bubble = LetterBubble(None, l)
+ bubble.thrown_bubble(x, y, 7.0 + 4.0 * random.random(),
+ (math.cos(angle), math.sin(angle)))
+
+class HeartPoison(RandomBonus):
+ "Heart Poison. Freeze all free monsters."
+ nimage = Bonuses.heart_poison
+ big = 0
+ bigbonus = {'big': 1}
+ bigdoc = "Freeze all other players too!"
+ def taken1(self, dragons):
+ import monsters
+ monsters.freeze_em_all()
+ if self.big:
+ def heart_pause(dragon, gen):
+ for i in range(222):
+ yield None
+ dragon.gen = gen
+ for d in BubPlayer.DragonList:
+ if d not in dragons:
+ d.gen = [heart_pause(d, d.gen)]
+
+class VioletNecklace(RandomBonus):
+ "Monster Duplicator. Double the number of free monsters."
+ points = 650
+ nimage = Bonuses.violet_necklace
+ big = 0
+ bigbonus = {'big': 1}
+ bigdoc = "This level's boring, let's bring even more monsters..."
+ def taken1(self, dragons):
+ if self.big:
+ import monsters, mnstrmap
+ mlist = 2*['Nasty', 'Monky', 'Springy', 'Orcy', 'Gramy', 'Blitzy']
+ wrange = (boards.bwidth - 8*CELL) // 2
+ for dir in [1, -1]:
+ for i in range(len(mlist)):
+ name = mlist[i]
+ mdef = getattr(mnstrmap, name)
+ cls = getattr(monsters, name)
+ x = wrange * i // len(mlist)
+ if dir == 1:
+ x = 2*CELL + HALFCELL + x
+ else:
+ x = boards.bwidth - 4*CELL - HALFCELL - x
+ y = -2*CELL - i * 2*CELL
+ cls(mdef, x, y, dir)
+ else:
+ for s in BubPlayer.MonsterList[:]:
+ if s.regular():
+ for i in range(self.multiply):
+ s.__class__(s.mdef, s.x, s.y, -s.dir * (-1)**i)
+
+class WandBonus(RandomBonus):
+ "Wand/Chest. Turn the bubble into bonuses at the end of the level."
+ nimages = [Bonuses.brown_wand, Bonuses.yellow_wand, Bonuses.green_wand,
+ Bonuses.violet_wand, Bonuses.blue_wand, Bonuses.red_wand,
+ Bonuses.violet_chest, Bonuses.blue_chest, Bonuses.red_chest,
+ Bonuses.yellow_chest,
+ ]
+ Modes = [
+ (Bonuses.brown_wand, 750, Bonuses.cyan_ice, 700, BigImages.cyan_ice, 20000),
+ (Bonuses.yellow_wand, 750, Bonuses.violet_ice, 750, BigImages.violet_ice, 20000),
+ (Bonuses.green_wand, 750, Bonuses.peach2, 800, BigImages.peach2, 30000),
+ (Bonuses.violet_wand, 750, Bonuses.pastec2, 850, BigImages.pastec2, 30000),
+ (Bonuses.blue_wand, 750, Bonuses.cream_pie, 900, BigImages.cream_pie, 40000),
+ (Bonuses.red_wand, 750, Bonuses.sugar_pie, 950, BigImages.sugar_pie, 40000),
+ (Bonuses.violet_chest, 2000, Diamonds.violet, 6000, BigImages.violet, 60000),
+ (Bonuses.blue_chest, 2000, Diamonds.blue, 7000, BigImages.blue, 60000),
+ (Bonuses.red_chest, 2000, Diamonds.red, 8000, BigImages.red, 70000),
+ (Bonuses.yellow_chest, 2000, Diamonds.yellow, 9000, BigImages.yellow, 70000),
+ ]
+ def __init__(self, x, y):
+ self.mode = random.choice(WandBonus.Modes)
+ RandomBonus.__init__(self, x, y, *self.mode[:2])
+ def taken1(self, dragons):
+ BubPlayer.BubblesBecome = self.bubble_outcome
+ BubPlayer.MegaBonus = self.mega_bonus
+ def bubble_outcome(self, bubble):
+ if bubble.pop():
+ x = bubble.x
+ if x < 2*CELL:
+ x = 2*CELL
+ elif x > boards.bwidth - 4*CELL:
+ x = boards.bwidth - 4*CELL
+ Bonus(x, bubble.y, *self.mode[2:4])
+ def mega_bonus(self):
+ nico, npoints = self.mode[4:6]
+ ico = images.sprget(nico)
+ x = random.randrange(0, boards.bwidth-ico.w)
+ mb = Megabonus(x, -ico.h, nico, npoints)
+ mb.outcome = (Bonus,) + self.mode[2:4]
+ mb.outcome_image = self.mode[2]
+WandBonus1 = WandBonus # increase probability
+
+class Megabonus(Bonus):
+ touchable = 0
+ vspeed = 6
+ sound = 'Extra'
+ coverwithbonus = 99
+ fallerdelay = 71
+
+ def faller(self):
+ self.fullpoints = self.points
+ self.bubbles = {}
+ for t in range(self.fallerdelay):
+ yield None
+ self.ready_to_go()
+ self.bubbles_pos = list(self.bubbles_position())
+ self.gen.append(self.animate_bubbles())
+ y0 = self.y - HALFCELL
+ ymax = boards.bheight - CELL - self.ico.h
+ self.touchable = 1
+ ny = self.y
+ while self.y >= y0:
+ if self.vspeed:
+ ny += self.vspeed
+ if ny > ymax:
+ ny = ymax
+ self.vspeed = 0
+ self.move(self.x, int(ny))
+ yield None
+ self.kill()
+
+ def ready_to_go(self):
+ pass
+
+ def is_on_ground(self):
+ return self.y == boards.bheight - CELL - self.ico.h
+
+ def kill(self):
+ for bubble in self.bubbles.values():
+ bubble.pop()
+ Bonus.kill(self)
+
+ def taken(self, dragon):
+ poplist = [dragon]
+ for bubble in self.bubbles.values():
+ bubble.pop(poplist)
+
+ def bubbles_position(self):
+ import time; start=time.time()
+ cx = self.ico.w//2 - CELL
+ cy = self.ico.h//2 - CELL
+ positions = []
+ pi2 = math.pi * 2
+ dist = 10.0
+ for i in range(31):
+ while 1:
+ angle = random.random() * pi2
+ nx = cx + int(dist*math.sin(angle))
+ ny = cy + int(dist*math.cos(angle))
+ for ox, oy in positions:
+ if (nx-ox)*(nx-ox) + (ny-oy)*(ny-oy) < 220:
+ dist += 0.3
+ break
+ else:
+ break
+ positions.append((nx, ny))
+ #print time.time()-start
+ return positions
+## nx = 5
+## ny = 6
+## xmargin = 2
+## ymargin = 7
+## xstep = (self.ico.w+2*xmargin-2*CELL) / float(nx-1)
+## ystep = (self.ico.h+2*ymargin-2*CELL) / float(ny-1)
+## for dx in range(nx):
+## corner = dx in [0, nx-1]
+## for dy in range(corner, ny-corner):
+## dx1 = int(dx*xstep)-xmargin
+## dy1 = int(dy*ystep)-ymargin
+## yield (dx1 + random.randrange(-2,3),
+## dy1 + random.randrange(-2,3))
+
+ def nearest_free_point(self, x0, y0):
+ distlst = [((x0-x)*(x0-x)+(y0-y)*(y0-y)+random.random(), x, y)
+ for x, y in self.bubbles_pos if (x, y) not in self.bubbles]
+ if distlst:
+ ignored, dx, dy = min(distlst)
+ return dx, dy
+ else:
+ return None, None
+
+ def in_bubble(self, bubble):
+ if not self.touchable:
+ return # bubbling a BonusMaker about to make a big bonus
+ dx, dy = self.nearest_free_point(bubble.x-self.x, bubble.y-self.y)
+ if dx is not None:
+ self.cover_bubble(dx, dy, bubble.d.bubber)
+ self.gen.append(self.cover_bubbles(bubble.d.bubber))
+ bubble.kill()
+
+ def cover_bubbles(self, bubber):
+ while 1:
+ for t in range(2):
+ yield None
+ bubbles = [dxy for dxy, b in self.bubbles.items()
+ if b.bubber is bubber]
+ if not bubbles:
+ break
+ dx, dy = self.nearest_free_point(*random.choice(bubbles))
+ if dx is None:
+ break
+ self.cover_bubble(dx, dy, bubber)
+ self.untouchable()
+
+ def cover_bubble(self, dx, dy, bubber):
+ if (dx, dy) in self.bubbles:
+ return
+ from bubbles import Bubble
+ if len(self.bubbles) & 1:
+ MegabonusBubble = Bubble
+ elif self.coverwithbonus:
+ self.coverwithbonus -= 1
+ outcome = self.outcome
+ outcome_image = self.outcome_image
+ class MegabonusBubble(Bubble):
+ def popped(self, dragon):
+ BonusMaker(self.x, self.y, [outcome_image], outcome=outcome)
+ return 10
+ else:
+ MegabonusBubble = Bubble
+
+ nimages = GreenAndBlue.normal_bubbles[bubber.pn]
+ b = MegabonusBubble(images.sprget(nimages[1]), self.x+dx, self.y+dy)
+ b.dx = dx
+ b.dy = dy
+ b.bubber = bubber
+ b.nimages = nimages
+ self.bubbles[dx, dy] = b
+ self.timeout = 0
+ f = float(len(self.bubbles)) / len(self.bubbles_pos)
+ self.vspeed = -0.73*f + self.vspeed*(1.0-f)
+ self.points = int(self.fullpoints*(1.0-f) / 10000.0 + 0.9999) * 10000
+
+ def animate_bubbles(self):
+ if 0: # disabled clipping
+ d = {}
+ for dx, dy in self.bubbles_pos:
+ d[dx] = d[dy] = None
+ north = d.copy()
+ south = d.copy()
+ west = d.copy()
+ east = d.copy()
+ del d
+ for dx, dy in self.bubbles_pos:
+ lst = [y for x, y in self.bubbles_pos if x==dx and y<dy]
+ if lst: north[dy] = max(lst)
+ lst = [y for x, y in self.bubbles_pos if x==dx and y>dy]
+ if lst: south[dy] = min(lst)
+ lst = [x for x, y in self.bubbles_pos if x<dx and y==dy]
+ if lst: west[dx] = max(lst)
+ lst = [x for x, y in self.bubbles_pos if x>dx and y==dy]
+ if lst: east[dx] = min(lst)
+ W = 2*CELL
+ H = 2*CELL
+ bubbles = self.bubbles
+ while 1:
+ for cycle in [1]*8 + [2]*10 + [1]*8 + [0]*10:
+ yield None
+ for (dx, dy), bubble in bubbles.items():
+ if not hasattr(bubble, 'poplist'):
+ if 0: # disabled clipping
+ if (dx, north[dy]) in bubbles:
+ margin_n = (north[dy]+H-dy)//2
+ else:
+ margin_n = 0
+ if (dx, south[dy]) in bubbles:
+ margin_s = (dy+H-south[dy])//2
+ else:
+ margin_s = 0
+ if (west[dx], dy) in bubbles:
+ margin_w = (west[dx]+W-dx)//2
+ else:
+ margin_w = 0
+ if (east[dx], dy) in bubbles:
+ margin_e = (dx+W-east[dx])//2
+ else:
+ margin_e = 0
+ r = (margin_w,
+ margin_n,
+ W-margin_w-margin_e,
+ H-margin_n-margin_s)
+ bubble.move(self.x + bubble.dx + margin_w,
+ self.y + bubble.dy + margin_n,
+ images.sprget_subrect(
+ bubble.nimages[cycle], r))
+ else:
+ bubble.move(self.x + bubble.dx,
+ self.y + bubble.dy,
+ images.sprget(bubble.nimages[cycle]))
+ elif len(bubbles) == len(self.bubbles_pos):
+ self.pop_bubbles(bubble.poplist)
+ return
+
+ def pop_bubbles(self, poplist):
+ def bubble_timeout(bubble, vspeed):
+ ny = bubble.y
+ for t in range(random.randrange(15,25)):
+ if hasattr(bubble, 'poplist'):
+ return
+ ny += vspeed
+ bubble.move(bubble.x, int(ny))
+ yield None
+ bubble.pop(poplist)
+
+ for bubble in self.bubbles.values():
+ bubble.gen.append(bubble_timeout(bubble, self.vspeed))
+ self.bubbles.clear()
+ self.kill()
+
+class Cactus(RandomBonus):
+ "Cactus. Drop a big version of a random bonus."
+ points = 600
+ nimage = 'cactus'
+ extra_cheat_arg = None
+ bigbonus = {'multiply': 3}
+ bigdoc = "Let's get more big bonuses!"
+
+ def taken1(self, dragons):
+ count = 0
+ while count < self.multiply:
+ args = ()
+ if self.extra_cheat_arg:
+ cls = globals()[self.extra_cheat_arg]
+ self.extra_cheat_arg = None
+ elif bigclockticker and bigclockticker.state == 'pre':
+ cls = Clock
+ args = (1,)
+ else:
+ cls = random.choice(Classes)
+ if makecactusbonus(cls, *args):
+ count += 1
+ cactusbonussound()
+
+#Cactus1 = Cactus # increase probability
+
+OFFSCREEN = -3*CELL
+def makecactusbonus(cls, *args):
+ bonus = cls(OFFSCREEN, 0, *args)
+ if not bonus.alive or getattr(bonus, 'bigbonus', None) is None:
+ if bonus.alive:
+ bonus.kill()
+ return None
+ bonus.__dict__.update(bonus.bigbonus)
+ bonus.untouchable()
+ bonus.gen = []
+ megacls = bonus.bigbonus.get('megacls', Cactusbonus)
+ mb = megacls(0, -3*CELL, 'cactus', 10000) # temp image
+ mb.outcome = (cls,) + (args or bonus.bigbonus.get('outcome_args', ()))
+ mb.outcome_image = bonus.nimage
+ mb.bonus = bonus
+ mb.gen.append(mb.prepare_image())
+ mb.gen.append(mb.remove_if_no_bonus())
+ return mb
+
+def cactusbonussound():
+ gamesrv.set_musics([], [])
+ boards.curboard.set_musics(prefix=[images.music_modern])
+ boards.curboard.set_musics()
+
+class Cactusbonus(Megabonus):
+ coverwithbonus = 5
+
+ def prepare_image(self):
+ while images.computebiggericon(self.bonus.ico) is None:
+ yield None
+
+ def remove_if_no_bonus(self):
+ while self.bonus.alive:
+ yield None
+ self.kill()
+
+ def ready_to_go(self):
+ ico = images.biggericon(self.bonus.ico)
+ x = random.randrange(0, boards.bwidth-ico.w)
+ self.move(x, -ico.h, ico)
+
+ def taken1(self, dragons):
+ d1 = list(dragons)
+ Megabonus.taken1(self, dragons)
+ if self.bonus.alive:
+ x = self.x + self.ico.w//2 - CELL
+ y = self.y + self.ico.h//2 - CELL
+ self.bonus.move(x, y)
+ res = self.bonus.taken1(d1)
+ self.untouchable()
+ if res == -1:
+ self.taken_by = []
+ self.gen.append(self.touchdelay(10))
+ self.bonus.move(OFFSCREEN, 0)
+ else:
+ self.bonus.kill()
+ return res
+
+ def kill(self):
+ Megabonus.kill(self)
+ if self.bonus.alive:
+ self.bonus.kill()
+
+class LongDurationCactusbonus(Cactusbonus):
+ timeout = 500
+ killgens = 0
+
+def starexplosion(x, y, multiplyer, killmonsters=0, outcomes=[]):
+ outcomes = list(outcomes)
+ poplist = [None]
+ for i in range(multiplyer):
+ colors = list(Stars.COLORS)
+ random.shuffle(colors)
+ for colorname in colors:
+ images = getattr(Stars, colorname)
+ if outcomes:
+ outcome = outcomes.pop()
+ extra_stars = []
+ if hasattr(outcome[0], 'extra_stars_location'):
+ for sx, sy in outcome[0].extra_stars_location:
+ extra_stars.append(BonusMakerExtraStar(x, y, sx, sy,
+ colorname))
+ bm = BonusMaker(x, y, images, outcome=outcome)
+ for star in extra_stars:
+ star.gen.append(star.follow_bonusmaker(bm))
+ else:
+ b = Parabolic2(x, y, images)
+ if killmonsters:
+ b.gen.append(b.killmonsters(poplist))
+
+class HomingStar(ActiveSprite):
+ def __init__(self, x, y, colorname, poplist):
+ imglist = getattr(Stars, colorname)
+ ActiveSprite.__init__(self, images.sprget(imglist[0]), x, y)
+ self.colorname = colorname
+ self.setimages(self.cyclic(imglist, 2))
+ self.gen.append(self.homing(poplist))
+
+ def homing(self, poplist):
+ from monsters import Monster
+ target = None
+ vx = (random.random() - 0.5) * 6.6
+ vy = (random.random() - 0.5) * 4.4
+ nx = self.x
+ ny = self.y
+ counter = 10
+ while 1:
+ if random.random() < 0.02:
+ target = None
+ if target is None or not target.alive:
+ bestdist = 1E10
+ for s in BubPlayer.MonsterList:
+ if isinstance(s, Monster):
+ dx = s.x - nx
+ dy = s.y - ny
+ dist = dx*dx + dy*dy + (random.random() * 25432.1)
+ if dist < bestdist:
+ bestdist = dist
+ target = s
+ if target is None:
+ break
+ dx = target.x - nx
+ dy = target.y - ny
+ dist = dx*dx + dy*dy
+ if dist <= 3*CELL*CELL:
+ target.argh(poplist)
+ break
+ yield None
+ vx = (vx + dx * 0.005) * 0.96
+ vy = (vy + dy * 0.005) * 0.96
+ nx += vx
+ ny += vy
+ self.move(int(nx), int(ny))
+ if counter:
+ counter -= 1
+ else:
+ img = ('smstar', self.colorname, random.randrange(2))
+ s = ActiveSprite(images.sprget(img), self.x + 8, self.y + 8)
+ s.gen.append(s.die([None], speed=10))
+ counter = 3
+ self.kill()
+
+class Book(RandomBonus):
+ "Magic Bomb. Makes a magical explosion killing touched monsters."
+ points = 2000
+ nimage = Bonuses.book
+ big = 0
+ bigbonus = {'big': 1}
+ bigdoc = "Homing Magical Stars."
+ def taken1(self, dragons):
+ if self.big:
+ poplist = [None]
+ x = self.x + (self.ico.w - 2*CELL) // 2
+ y = self.y + (self.ico.h - 2*CELL) // 2
+ colors = list(Stars.COLORS)
+ random.shuffle(colors)
+ for colorname in colors + colors[len(colors)//2:]:
+ HomingStar(x, y, colorname, poplist)
+ else:
+ starexplosion(self.x, self.y, self.multiply, killmonsters=1)
+
+class Potion(RandomBonus):
+ "Potions. Clear the level and fill its top with bonuses."
+ nimages = [Bonuses.red_potion, Bonuses.green_potion, Bonuses.yellow_potion,
+ 'potion4']
+ Potions = [(Bonuses.red_potion, 150, [(PotionBonuses.coin, 350),
+ (PotionBonuses.rainbow, 600)]),
+ (Bonuses.green_potion, 350, [(PotionBonuses.flower, 1000),
+ (PotionBonuses.trefle, 2000)]),
+ (Bonuses.yellow_potion, 550, [(PotionBonuses.green_note, 2000),
+ (PotionBonuses.blue_note, 3000)]),
+ ('potion4', 750, None),
+ ]
+ LocalDir = os.path.dirname(__file__) or os.curdir
+ Extensions = [s for s in os.listdir(LocalDir)
+ if s.startswith('ext') and
+ os.path.isdir(os.path.join(LocalDir, s))]
+ random.shuffle(Extensions)
+ extra_cheat_arg = None
+ big = 0
+ bigdoc = "Fill the whole level with bonuses."
+
+ def __init__(self, x, y):
+ p_normal = 3
+ if boards.curboard.bonuslevel:
+ p_extension = 2 # make extensions rare in the bonus level
+ else:
+ p_extension = 5
+ if self.extra_cheat_arg:
+ Potion.Extensions.append(self.extra_cheat_arg)
+ p_normal = 0
+ if not Potion.Extensions:
+ p_extension = 0
+ choices = []
+ for mode in Potion.Potions:
+ if mode[2] is None:
+ p = p_extension
+ else:
+ p = p_normal
+ choices += [mode] * p
+ self.mode = random.choice(choices)
+ if self.mode[2] is not None:
+ self.bigbonus = {'big': 1}
+ RandomBonus.__init__(self, x, y, *self.mode[:2])
+ def taken1(self, dragons):
+ blist = self.mode[2]
+ if blist is not None:
+ if random.random() < 0.6:
+ blist = [random.choice(blist)]
+ boards.replace_boardgen(boards.potion_fill(blist, self.big))
+ else:
+ n_players = len([p for p in BubPlayer.PlayerList if p.isplaying()])
+ while Potion.Extensions:
+ ext = Potion.Extensions.pop()
+ #print "Trying potion:", ext
+ ext = __import__(ext, globals(),locals(), ['run','min_players'])
+ if n_players >= ext.min_players:
+ ext.run()
+ boards.BoardGen.append(boards.extra_bkgnd_black(self.x, self.y))
+ #print "Accepted because:", n_players, ">=", ext.min_players
+ break
+ else:
+ #print "Rejected because:", n_players, "<", ext.min_players
+ pass
+
+class FireBubble(RandomBonus):
+ "Fire Bubbles. Makes you fire napalm bubbles."
+ nimage = Bonuses.hamburger
+ bubkind = 'FireBubble'
+ bubcount = 10
+ bigbonus = {'bubkind': 'BigFireBubble'}
+ bigdoc = "Makes you shoot fire - you're a dragon after all."
+ def taken(self, dragon):
+ dragon.dcap['shootbubbles'] = [self.bubkind] * self.bubcount
+ dragon.carrybonus(self)
+
+class WaterBubble(FireBubble):
+ "Water Bubbles. Your bubbles will now be filled with water."
+ nimage = Bonuses.beer
+ bubkind = 'WaterBubble'
+ bigbonus = {'bubkind': 'SnookerBubble'}
+ bigdoc = "Snooker balls."
+
+class LightningBubble(FireBubble):
+ "Lightning Bubbles."
+ nimage = Bonuses.french_fries
+ bubkind = 'LightningBubble'
+ bigbonus = {'bubkind': 'BigLightBubble'}
+ bigdoc = "Even-more-lightning Bubbles."
+
+class Megadiamond(Megabonus):
+ nimage = BigImages.red
+ points = 20000
+ fallerdelay = 0
+ outcome = (MonsterBonus, -1)
+ outcome_image = Bonuses.monster_bonuses[-1][0]
+ extra_stars_location = [ (-24,-28),(0,-28),(24,-28),
+ (-40,-11), (40,-11),
+ (-32, 8), (32, 8),
+ (-16,23), (16,23),
+ (0,38), ]
+ def __init__(self, x, y):
+ ico = images.sprget(self.nimage)
+ x -= (ico.w - 2*CELL) // 2
+ y -= (ico.h - 2*CELL) // 2
+ Megabonus.__init__(self, x, y)
+
+class Door(RandomBonus):
+ "Magic Door. Let bonuses come in!"
+ points = 1000
+ nimage = Bonuses.door
+ diamond_outcome = (MonsterBonus, -1)
+ bigbonus = {'diamond_outcome': (Megadiamond,)}
+ bigdoc = "Let bigger bonuses come in!"
+ def taken1(self, dragons):
+ starexplosion(self.x, self.y, 2,
+ outcomes = [self.diamond_outcome] * 10)
+
+class LongFire(RandomBonus):
+ "Long Fire. Increase the range of your bubble throw out."
+ nimage = Bonuses.softice1
+ big = 0
+ bigbonus = {'big': 1}
+ bigdoc = "Throw bubbles that split into more bubbles."
+ def taken(self, dragon):
+ if self.big:
+ dragon.dcap['shootbubbles'] = ['MoreBubblesBubble'] * 10
+ else:
+ dragon.dcap['shootthrust'] *= 1.5
+ dragon.carrybonus(self)
+
+class Glue(RandomBonus):
+ "Glue. Triple fire."
+ nimage = 'glue'
+ points = 850
+ big = 0
+ bigbonus = {'big': 1}
+ bigdoc = "Heptuple fire. (That's 7.)"
+ def taken(self, dragon):
+ if self.big:
+ dragon.dcap['flower'] = -16 # heptuple fire
+ elif dragon.dcap['flower'] >= 0:
+ dragon.dcap['flower'] = -1 # triple fire
+ else:
+ dragon.dcap['flower'] -= 1 # cumulative effect
+ dragon.carrybonus(self)
+
+class ShortFire(RandomBonus):
+ "Short Fire. Shorten the range of your bubble throw out."
+ nimage = Bonuses.softice2
+ points = 300
+ factor = 1 / 1.5
+ bigbonus = {'factor': 0}
+ bigdoc = "What occurs if you throw bubbles at range zero?"
+ def taken(self, dragon):
+ dragon.dcap['shootthrust'] *= self.factor
+ dragon.carrybonus(self)
+
+class HighSpeedFire(RandomBonus):
+ "High Speed Fire. Increase your fire rate."
+ nimage = Bonuses.custard_pie
+ points = 700
+ bigbonus = {'multiply': 4}
+ bigdoc = "Machine-gun speed!"
+ def taken(self, dragon):
+ dragon.dcap['firerate'] += 1.5
+ dragon.carrybonus(self)
+
+class Mushroom(TemporaryBonus):
+ "Bouncy Bouncy. Makes you jump continuously."
+ nimage = Bonuses.mushroom
+ points = 900
+ capname = 'pinball'
+ captime = 625
+ bigbonus = {'captime': captime*2, 'multiply': 2}
+ bigdoc = "The same, but even more annoying."
+
+class AutoFire(TemporaryBonus):
+ "Auto Fire. Makes you fire continuously."
+ nimage = Bonuses.rape
+ points = 800
+ capname = 'autofire'
+ captime = 675
+ big = 0
+ bigbonus = {'big': 1}
+ bigdoc = "Adds many bubbles to the level."
+ def taken1(self, dragons):
+ if self.big:
+ boards.extra_boardgen(boards.extra_bubbles(900))
+ else:
+ TemporaryBonus.taken1(self, dragons)
+
+class Insect(RandomBonus):
+ "Crush World."
+ nimage = Bonuses.insect
+ big = 0
+ bigbonus = {'big': 1}
+ bigdoc = "What if the level looked like that instead... Or like that... Or..."
+ def taken1(self, dragons):
+ if self.big:
+ if dragons:
+ d = random.choice(dragons)
+ cx, cy = d.x, d.y
+ else:
+ cx, cy = None, None
+ boards.extra_boardgen(boards.extra_make_random_level(cx, cy))
+ else:
+ boards.extra_boardgen(boards.extra_walls_falling())
+
+class Ring(TemporaryBonus):
+ "The One Ring."
+ nimage = Bonuses.ring
+ points = 4000
+ capname = 'ring'
+ captime = 700
+ bonusleveldivider = 5
+ bigbonus = {'multiply': 3}
+ bigdoc = "Where am I?"
+
+class GreenPepper(TemporaryBonus):
+ "Hot Pepper. Run! Run! That burns."
+ nimage = Bonuses.green_pepper
+ capname = 'hotstuff'
+ captime = 100
+ bigbonus = {'captime': 250, 'multiply': 2}
+ bigdoc = "That burns a lot!"
+
+class Lollipop(TemporaryBonus):
+ "Yo Man! Makes you walk backward."
+ nimage = Bonuses.lollipop
+ big = 0
+ bigbonus = {'big': 1}
+ bigdoc = "Just swapping 'left' and 'right' is not confusing enough."
+ def taken(self, dragon):
+ dragon.dcap['left2right'] = -dragon.dcap['left2right']
+ if self.big:
+ perm = range(4)
+ while perm[0] == 0 or perm[1] == 1 or perm[2] == 2 or perm[3] == 3:
+ random.shuffle(perm)
+ names = ('key_left', 'key_right', 'key_jump', 'key_fire')
+ dragon.dcap['key_right'] = names[perm[0]]
+ dragon.dcap['key_left'] = names[perm[1]]
+ dragon.dcap['key_jump'] = names[perm[2]]
+ dragon.dcap['key_fire'] = names[perm[3]]
+ self.carried(dragon)
+ def endaction(self, dragon):
+ dragon.dcap['left2right'] = -dragon.dcap['left2right']
+ for name in ('key_left', 'key_right', 'key_jump', 'key_fire'):
+ dragon.dcap[name] = name
+
+class Chickpea(TemporaryBonus):
+ "Basilik. Allows you to touch the monsters."
+ nimage = Bonuses.chickpea
+ points = 800
+ capname = 'overlayglasses'
+ captime = 400
+ big = 0
+ bigbonus = {'big': 1}
+ bigdoc = "Turn off the light."
+
+ def taken1(self, dragons):
+ if self.big:
+ boards.extra_boardgen(boards.extra_light_off(597), 1)
+ else:
+ TemporaryBonus.taken1(self, dragons)
+
+ def taken(self, dragon):
+ TemporaryBonus.taken(self, dragon)
+ dragon.dcap['shield'] += 420
+
+class IceCream(RandomBonus):
+ "Icecream. An icecream which is so good you'll always want more."
+ nimages = [Bonuses.icecream6, Bonuses.icecream5,
+ Bonuses.icecream4, Bonuses.icecream3]
+ IceCreams = [(Bonuses.icecream6, 250),
+ (Bonuses.icecream5, 500),
+ (Bonuses.icecream4, 1000),
+ (Bonuses.icecream3, 2000)]
+ big = 0
+ bigbonus = {'big': 1}
+ bigdoc = "BIG ice creams!"
+ def __init__(self, x, y, generation=0):
+ self.generation = generation
+ RandomBonus.__init__(self, x, y, *self.IceCreams[generation])
+ def taken1(self, dragons):
+ nextgen = self.generation + 1
+ if nextgen < len(self.IceCreams):
+ for i in range(2):
+ if self.big:
+ makecactusbonus(IceCream, nextgen)
+ else:
+ x, y = chooseground(200)
+ if x is None:
+ return
+ IceCream(x, y, nextgen)
+ if self.big:
+ cactusbonussound()
+
+class Grenade(RandomBonus):
+ "Barbecue."
+ nimage = Bonuses.grenade
+ points = 550
+ big = 0
+ bigbonus = {'big': 1}
+ bigdoc = "360-degree flames."
+ def taken1(self, dragons):
+ from bubbles import FireFlame
+ poplist = [None]
+ for y in range(1, boards.height-1):
+ for x in range(2, boards.width-2):
+ if bget(x,y) != ' ':
+ continue
+ if bget(x,y+1) == '#':
+ FireFlame(x, y, poplist)
+ elif self.big:
+ if bget(x,y-1) == '#':
+ FireFlame(x, y, poplist, flip='vflip')
+ elif bget(x-1,y) == '#':
+ FireFlame(x, y, poplist, flip='cw')
+ elif bget(x+1,y) == '#':
+ FireFlame(x, y, poplist, flip='ccw')
+
+class Conch(RandomBonus):
+ "Sea Shell. Let's bring the sea here!"
+ nimage = Bonuses.conch
+ points = 650
+ big = 0
+ bigbonus = {'big': 1}
+ bigdoc = "Aquarium."
+ def taken1(self, dragons):
+ if self.big:
+ gen = boards.extra_aquarium
+ else:
+ gen = boards.extra_water_flood
+ boards.extra_boardgen(gen())
+
+def fire_rain(x, poplist):
+ from bubbles import FireDrop
+ FireDrop(x, -CELL, poplist)
+
+def water_rain(x, poplist):
+ from bubbles import watercell
+ watercell(x, 0, poplist)
+
+def ball_rain(x, poplist):
+ from bubbles import SpinningBall
+ SpinningBall(x, -CELL, poplist)
+
+class Umbrella(RandomBonus):
+ "Umbrellas. Beware of what's going to fall on everyone's head!"
+ nimages = [Bonuses.brown_umbrella, Bonuses.grey_umbrella,
+ Bonuses.violet_umbrella]
+ Umbrellas = [(Bonuses.brown_umbrella, 900, fire_rain, 10, 60),
+ (Bonuses.grey_umbrella, 950, water_rain, 5, 60),
+ (Bonuses.violet_umbrella, 1000, ball_rain, 9, 120)]
+ bigbonus = {'multiply': 3.1416}
+ bigdoc = "It's raining hard."
+ def __init__(self, x, y):
+ self.mode = random.choice(Umbrella.Umbrellas)
+ RandomBonus.__init__(self, x, y, *self.mode[:2])
+ def taken1(self, dragons):
+ for i in range(self.multiply):
+ boards.extra_boardgen(self.raining())
+ def raining(self):
+ builder, drops, timemax = self.mode[2:]
+ timemax = int(timemax * math.sqrt(self.multiply))
+ drops = int(drops * self.multiply)
+ times = [random.randrange(0, timemax) for i in range(drops)]
+ poplist = [None]
+ for t in range(timemax):
+ for i in range(times.count(t)):
+ x = random.randrange(2*CELL, bwidth-3*CELL+1)
+ builder(x, poplist)
+ yield 0
+
+class Fruits(RandomBonus):
+ "Fruits. A small little bonus. But the size doesn't matter, does it? If you're lucky enough you might get a great shower!"
+ nimages = [Bonuses.kirsh, Bonuses.erdbeer, Bonuses.tomato,
+ Bonuses.apple, Bonuses.corn, Bonuses.radish]
+ bubblable = 0
+ sound = 'Extra'
+ Fruits = [(Bonuses.kirsh, 100),
+ #(Bonuses.icecream1, 150),
+ (Bonuses.erdbeer, 150),
+ #(Bonuses.fish1, 250),
+ (Bonuses.tomato, 200),
+ #(Bonuses.donut, 250),
+ (Bonuses.apple, 250),
+ (Bonuses.corn, 300),
+ #(Bonuses.icecream2, 600),
+ (Bonuses.radish, 350),
+ ]
+ def __init__(self, x, y): # x and y ignored !
+ fine = 0
+ for i in range(20):
+ x0 = random.randint(3, boards.width-5)
+ y0 = random.randint(1, boards.height-3)
+ for xt in range(x0-1, x0+3):
+ if xt == x0-1 or xt == x0+2:
+ yplus = 1
+ else:
+ yplus = 0
+ for yt in range(y0+yplus, y0+4-yplus):
+ if bget(xt,yt) != ' ':
+ break
+ else:
+ continue
+ break
+ else:
+ x, y = x0*CELL, y0*CELL
+ fine = 1
+ break
+ mode = random.choice(Fruits.Fruits)
+ RandomBonus.__init__(self, x, y, falling=0, *mode)
+ self.repeatcount = 0
+ if not fine:
+ self.kill()
+ elif random.random() < 0.04:
+ self.superfruit = mode
+ self.sound = 'Shh'
+ self.points = 0
+ self.repeatcount = random.randrange(50,100)
+ def taken1(self, dragons):
+ if self.repeatcount:
+ image, points = self.superfruit
+ f = Parabolic2(self.x, self.y, [image], y_amplitude = -1.5)
+ f.points = points
+ f.touchable = 1
+ self.repeatcount -= 1
+ self.gen.append(self.taking(1, 2))
+ return -1
+Fruits1 = Fruits # increase probability
+Fruits2 = Fruits
+Fruits3 = Fruits
+Fruits4 = Fruits
+Fruits5 = Fruits
+Fruits6 = Fruits
+
+class BlueNecklace(RandomBonus):
+ "Self Duplicator. Mirror yourself."
+ points = 1000
+ nimage = Bonuses.blue_necklace
+ copies = 1
+ bigbonus = {'copies': 3}
+ bigdoc = "Mirrors vertically too."
+ def taken(self, dragon):
+ dragons = [dragon]
+ modes = [(-1, 1), (1, -1), (-1, -1)][:self.copies]
+ modes.reverse()
+ dcap = dragon.dcap.copy()
+ for sign, gravity in modes:
+ if len(dragon.bubber.dragons) >= 7:
+ break # avoid burning the server with two much dragons
+ d1 = self.makecopy(dragon, sign, gravity, dcap)
+ dragons.append(d1)
+ d1 = random.choice(dragons)
+ d1.carrybonus(self, 250)
+
+ def makecopy(self, dragon, sign=-1, gravity=1, dcap=None):
+ from player import Dragon
+ dcap = dcap or dragon.dcap
+ d = Dragon(dragon.bubber, dragon.x, dragon.y, -dragon.dir, dcap)
+ d.dcap['left2right'] = sign * dcap['left2right']
+ d.dcap['gravity'] = gravity * dcap['gravity']
+ d.up = dragon.up
+ s = (dcap['shield'] + 12) & ~3
+ dragon.dcap['shield'] = s+2
+ if sign*gravity > 0:
+ s += 2
+ d.dcap['shield'] = s
+ dragon.bubber.dragons.append(d)
+ return d
+
+class Monsterer(RandomBonus):
+ "Monsterificator. Let's play on the other side!"
+ nimages = [Bonuses.red_crux, Bonuses.blue_crux]
+ Sizes = [(Bonuses.red_crux, 800), (Bonuses.blue_crux, 850)]
+ mlist = [['Nasty', 'Monky', 'Springy', 'Orcy'],
+ ['Ghosty', 'Flappy', 'Gramy', 'Blitzy']
+ ]
+ big = 0
+ bigbonus = {'big': 1}
+ bigdoc = "Ta, ta ta, ta, taaaaaa..."
+ def __init__(self, x, y):
+ self.mode = random.choice([0,1])
+ RandomBonus.__init__(self, x, y, *self.Sizes[self.mode])
+ def taken(self, dragon):
+ mcls = random.choice(self.mlist[self.mode])
+ dragon.become_monster(mcls, self.big)
+
+Monsterer1 = Monsterer # increase probability
+
+class Bubblizer(RandomBonus):
+ "Bubblizer."
+ points = 750
+ nimage = Bonuses.gold_crux
+ big = 0
+ bigbonus = {'big': 1}
+ bigdoc = "Special powers for your bubble."
+ def taken(self, dragon):
+ args = (dragon.bubber.pn,)
+ if self.big:
+ from bubbles import SnookerBubble, BigLightBubble
+ bcls = random.choice([SnookerBubble, BigLightBubble])
+ if bcls is SnookerBubble:
+ args = (dragon, dragon.x, dragon.y, 1000000)
+ else:
+ from bubbles import FireBubble, WaterBubble, LightningBubble
+ bcls = random.choice([FireBubble, WaterBubble, LightningBubble])
+ b = bcls(*args)
+ b.move(dragon.x, dragon.y)
+ if not dragon.become_bubblingeyes(b):
+ b.kill()
+
+class Carrot(RandomBonus):
+ "Angry Monster. Turns all free monsters angry."
+ nimage = Bonuses.carrot
+ points = 950
+ ghost = 0
+ bigbonus = {'ghost': 1}
+ bigdoc = "What do angry monsters turn into if you don't hurry up?"
+ def taken1(self, dragons):
+ from monsters import Monster
+ lst = [s for s in images.ActiveSprites
+ if isinstance(s, Monster) and s.regular()]
+ if lst:
+ if self.ghost:
+ images.Snd.Hell.play()
+ for s in lst:
+ s.become_ghost()
+ else:
+ for s in lst:
+ s.angry = [s.genangry()]
+ s.resetimages()
+
+class Egg(RandomBonus):
+ "Teleporter. Exchange yourself with somebody else."
+ nimage = Bonuses.egg
+ big = 0
+ bigbonus = {'big': 1}
+ bigdoc = "Exchange colors too."
+ def taken1(self, dragons):
+ if self.big:
+ self.exchange_bubbers()
+ else:
+ self.exchange_dragons(dragons)
+
+ def exchange_dragons(self, dragons):
+ dragons = [d for d in dragons if d in d.bubber.dragons]
+ alldragons = [d for d in BubPlayer.DragonList if d in d.bubber.dragons]
+ others = [d for d in alldragons if d not in dragons]
+ xchg = {}
+ random.shuffle(dragons)
+ random.shuffle(others)
+ while dragons and others:
+ d1 = dragons.pop()
+ d2 = others.pop()
+ xchg[d1] = d2.bubber
+ xchg[d2] = d1.bubber
+ if len(dragons) > 1:
+ copy = dragons[:]
+ for i in range(10):
+ random.shuffle(copy)
+ for j in range(len(dragons)):
+ if dragons[j] == copy[j]:
+ break
+ else:
+ break
+ for d1, d2 in zip(dragons, copy):
+ xchg[d1] = d2.bubber
+ elif len(dragons) == 1:
+ x, y = chooseground(200)
+ if x is not None:
+ d1 = dragons[0]
+ d1.move(x, y)
+ d1.dcap['shield'] = 50
+ for d1, bubber2 in xchg.items():
+ d1.bubber.dragons.remove(d1)
+ d1.bubber = bubber2
+ bubber2.dragons.append(d1)
+ d1.dcap['shield'] = 50
+
+ def exchange_bubbers(self):
+ self.exchange_dragons(list(BubPlayer.DragonList))
+ players = [p for p in BubPlayer.PlayerList
+ if p.isplaying()]
+ if len(players) > 1:
+ while 1:
+ copy = players[:]
+ random.shuffle(copy)
+ for j in range(len(players)):
+ if players[j] is copy[j]:
+ break
+ else:
+ break
+ for b1, b2 in zip(players, copy):
+ for d in b1.dragons:
+ d.dcap['bubbericons'] = b2
+
+class Bomb(RandomBonus):
+ "Baaoouuuummmm! Explode that wall!"
+ nimage = Bonuses.bomb
+ bigbonus = {'multiply': 3.8}
+ bigdoc = "Makes a BIG hole."
+ def taken1(self, dragons):
+ bomb_explosion(self.x, self.y, self.multiply)
+
+def bomb_explosion(x0, y0, multiply=1, starmul=2):
+ RADIUS = 3.9 * CELL * math.sqrt(multiply)
+ Radius2 = RADIUS * RADIUS
+ brd = boards.curboard
+ cx = x0 + HALFCELL
+ cy = y0 + HALFCELL - RADIUS/2
+ for y in range(0, brd.height):
+ dy1 = abs(y*CELL - cy)
+ dy2 = abs((y-(brd.height-1))*CELL - cy)
+ dy3 = abs((y+(brd.height-1))*CELL - cy)
+ dy = min(dy1, dy2, dy3)
+ for x in range(2, brd.width-2):
+ dx = x*CELL - cx
+ if dx*dx + dy*dy < Radius2:
+ try:
+ brd.killwall(x,y)
+ except KeyError:
+ pass
+ brd.reorder_walls()
+ starexplosion(x0, y0, starmul)
+ gen = boards.extra_display_repulse(x0+CELL, y0+CELL,
+ 15000 * multiply,
+ 1000 * multiply)
+ boards.extra_boardgen(gen)
+
+class Ham(RandomBonus):
+ "Protein. Let's build something!"
+ nimage = Bonuses.ham
+ bigbonus = {'multiply': 3.4}
+ bigdoc = "Builds something BIG."
+ def taken1(self, dragons):
+ RADIUS = 3.9 * CELL * math.sqrt(self.multiply)
+ Radius2 = RADIUS * RADIUS
+ brd = boards.curboard
+ cx = self.x + HALFCELL
+ cy = self.y + HALFCELL - RADIUS/2
+ xylist = []
+ for y in range(0, brd.height):
+ dy1 = abs(y*CELL - cy)
+ dy2 = abs((y-(brd.height-1))*CELL - cy)
+ dy3 = abs((y+(brd.height-1))*CELL - cy)
+ dy = min(dy1, dy2, dy3)
+ for x in range(2, brd.width-2):
+ dx = x*CELL - cx
+ if dx*dx + dy*dy < Radius2:
+ if (y,x) not in brd.walls_by_pos and random.random() < 0.5:
+ brd.putwall(x,y)
+ xylist.append((x, y))
+ brd.reorder_walls()
+ boards.extra_boardgen(boards.single_blocks_falling(xylist))
+ gen = boards.extra_display_repulse(self.x+CELL, self.y+CELL,
+ 5000 * self.multiply,
+ 1000 * self.multiply)
+ boards.extra_boardgen(gen)
+
+class Chestnut(RandomBonus):
+ "Relativity. Speed up or slow down the game."
+ nimage = Bonuses.chestnut
+ sound = None
+ big = 0
+ bigbonus = {'big': 1}
+ bigdoc = "Relative relativity - not the same one for players and monsters."
+ def taken1(self, dragons):
+ timeout = 500
+ if not self.big:
+ ft = random.choice([0.5, 2.0])
+ boards.set_frametime(ft)
+ if ft == 2.0:
+ timeout = 430
+ else:
+ if random.randrange(0, 2) == 1:
+ # super-fast game
+ boards.set_frametime(0.25)
+ timeout = 1800
+ else:
+ # board unchanged, players slower
+ boards.set_frametime(1.0, privtime=250)
+ timeout = 800
+ BubPlayer.MultiplyerReset = BubPlayer.FrameCounter + timeout
+ self.play(images.Snd.Fruit)
+
+
+try:
+ import statesaver
+except ImportError:
+ print "'statesaver' module not compiled, no clock bonus"
+ Clock = None
+else:
+ import new
+ try:
+ from statesaver import standard_build # PyPy
+ except ImportError:
+ def standard_build(self):
+ return new.instance(self.__class__)
+ boards.Copyable.inst_build = standard_build
+ gamesrv.Sprite.inst_build = standard_build
+
+ def copygamestate():
+ # makes a copy of the game state.
+ ps = []
+ for p1 in BubPlayer.PlayerList:
+ #if p1.isplaying():
+ d = p1.__dict__.copy()
+ for key in BubPlayer.TRANSIENT_DATA:
+ if key in d:
+ del d[key]
+ ps.append(d)
+ #else:
+ # ps.append(None)
+ topstate = (
+ [g for g in boards.BoardGen if not g.gi_running],
+ boards.curboard,
+ images.ActiveSprites,
+ images.SpritesByLoc,
+ BubPlayer.__dict__.items(),
+ gamesrv.sprites,
+ gamesrv.sprites_by_n,
+ ps,
+ images.Snd.__dict__.items(),
+ )
+ #import pdb; pdb.set_trace()
+ return statesaver.copy(topstate)
+
+ def restoregamestate(savedstate):
+ (boards.BoardGen,
+ boards.curboard,
+ images.ActiveSprites,
+ images.SpritesByLoc,
+ BubPlayerdictitems,
+ gamesrv.sprites,
+ gamesrv.sprites_by_n,
+ ps,
+ imagesSnddictitems,
+ ) = savedstate
+ for key, value in BubPlayerdictitems:
+ try:
+ setattr(BubPlayer, key, value)
+ except (AttributeError, TypeError):
+ pass
+ for key, value in imagesSnddictitems:
+ try:
+ setattr(images.Snd, key, value)
+ except (AttributeError, TypeError):
+ pass
+
+ for p, d in zip(BubPlayer.PlayerList, ps):
+ #if d is None:
+ # p.reset()
+ #else:
+ p.__dict__.update(d)
+ if not p.isplaying():
+ p.zarkoff()
+
+ class Clock(RandomBonus):
+ "Time Machine. Let's do it again!"
+ touchable = 0
+ points = 0
+ nimage = Bonuses.clock
+ ticker = None
+ bigdoc = "Let's do the whole level again - with the help of ghosts from your own past."
+
+ def __init__(self, x, y, big=0):
+ RandomBonus.__init__(self, -boards.bwidth, 0)
+ #print "starting clock"
+ self.savedstate = None
+ self.savedscreens = []
+ if bigclockticker:
+ if not big:
+ self.kill() # confusion between the two levels of saving
+ return
+ if x == OFFSCREEN:
+ if bigclockticker.state == 'pre':
+ self.bigbonus = {'ticker': bigclockticker}
+ bigclockticker.state = 'seen'
+ else:
+ # when taken, this has the same effect as the big clock
+ self.ticker = bigclockticker
+ self.move(x, y)
+ self.touchable = 1
+ return
+ self.gen = [self.delayed_show()]
+
+ def delayed_show(self):
+ boards.extra_boardgen(self.state_saver())
+ for i in range(10):
+ yield None
+ if self.savedstate is not None:
+ for i in range(55):
+ yield None
+ x, y = chooseground(200)
+ if x is not None:
+ self.move(x, y)
+ self.touchable = 1
+ self.gen.append(self.timeouter())
+ self.gen.append(self.faller())
+ return
+ self.kill()
+
+ def taken1(self, dragons):
+ if self.ticker:
+ return self.ticker.taken(dragons)
+ savedstate = self.savedstate
+ self.savedstate = None
+ if savedstate is not None:
+ boards.replace_boardgen(self.state_restorer(savedstate,
+ self.savedscreens,
+ self))
+ self.untouchable()
+ return -1
+
+ def state_saver(self):
+ # called from BoardGen
+ self.savedstate = copygamestate()
+ while self.alive:
+ gamesrv.sprites[0] = ''
+ data = ''.join(gamesrv.sprites)
+ self.savedscreens.append(data)
+ yield 0
+ yield 0
+ self.savedscreens.append(data)
+ yield 0
+ yield 0
+ self.savedscreens = []
+ def state_restorer(self, savedstate, savedscreens, blinkme):
+ # called from BoardGen
+ from player import scoreboard
+ status = 0
+ for t in range(10):
+ if not (t & 1):
+ gamesrv.sprites[0] = ''
+ savedscreens.append(''.join(gamesrv.sprites))
+ time = boards.normal_frame()
+ for i in range(t):
+ status += 1
+ if status % 3 == 0 and blinkme.alive:
+ if status % 6 == 0:
+ blinkme.step(boards.bwidth, 0)
+ else:
+ blinkme.step(-boards.bwidth, 0)
+ yield time
+ yield boards.force_singlegen()
+ yield 15.0
+ for p1 in BubPlayer.PlayerList:
+ del p1.dragons[:]
+ delay = 8.5
+ gamesrv.clearsprites()
+ while savedscreens:
+ gamesrv.sprites[:] = ['', savedscreens.pop()]
+ if delay > 0.6:
+ delay *= 0.9
+ yield delay
+ yield 15.0
+ restoregamestate(savedstate)
+ scoreboard()
+ yield 2.5
+
+ class DragonGhost(ActiveSprite):
+ def __init__(self, entry):
+ ActiveSprite.__init__(self, entry.ico, entry.x, entry.y)
+
+ def setentry(self, entry):
+ #ico = images.make_darker(entry.ico, True)
+ self.lastx = self.x
+ self.lasty = self.y
+ self.move(entry.x, entry.y, entry.ico)
+ self.entry = entry
+ self.bubber = entry.d.bubber
+ self.dir = entry.dir
+ self.poplist = [self]
+
+ def integrate(self):
+ self.play(images.Snd.Shh)
+ for j in range(15):
+ DustStar(self.x, self.y, 0, -3, clock=j==14)
+
+ def disintegrate(self):
+ self.play(images.Snd.Shh)
+ dx = self.x - self.lastx
+ dy = self.y - self.lasty
+ if dx < -4: dx = -4
+ if dy < -4: dy = -4
+ if dx > 4: dx = 4
+ if dy > 4: dy = 4
+ for j in range(15):
+ DustStar(self.x, self.y, dx, dy, clock=j==14)
+ self.kill()
+
+ def bottom_up(self):
+ return self.entry.dcap['gravity'] < 0.0
+
+ class SavedDragonEntry(object):
+ __slots__ = ['d', 'x', 'y', 'ico', 'flag', 'dir', 'dcap']
+ def __init__(self, d, flag, dir, dcap):
+ self.d = d
+ self.x = d.x
+ self.y = d.y
+ self.ico = d.ico
+ self.flag = flag
+ self.dir = dir
+ self.dcap = dcap
+
+ class SavedFrameEntry(object):
+ __slots__ = ['saved_next', 'tick', 'dragonlist', 'shoots1']
+ def __init__(self, tick, dragonlist):
+ self.saved_next = None
+ self.tick = tick
+ self.dragonlist = dragonlist
+ self.shoots1 = []
+
+ class BigClockTicker:
+ dragonlist = None
+ tick = 1000
+
+ def __init__(self):
+ global random
+ random = random_module.Random()
+ localrandom = DustStar.localrandom
+ self.state = 'pre'
+ self.randombase1 = hash(localrandom.random()) * 914971L
+ self.randombase2 = hash(localrandom.random()) * 914971L
+ self.saved_next = None
+ self.saved_last = self
+ random.seed(self.randombase1)
+ random_module.seed(self.randombase2)
+ self.latest_entries = {}
+
+ def common_tick(self, entry):
+ self.dragonlist = entry.dragonlist
+ random.seed(self.randombase1 - entry.tick)
+ random_module.seed(self.randombase2 - entry.tick)
+ bonus_frame_tick()
+ random.seed(self.randombase1 + entry.tick)
+ random_module.seed(self.randombase2 + entry.tick)
+
+ def save_frame_tick(self):
+ entry = self.save_frame()
+ self.common_tick(entry)
+
+ def save_frame(self):
+ from player import Dragon
+ from bubbles import DragonBubble
+ tick = self.saved_last.tick + 1
+ dragonlist = []
+ new_entries = {}
+ for bubber in BubPlayer.PlayerList:
+ if bubber.isplaying():
+ for d in bubber.dragons:
+ try:
+ dcap = self.latest_entries[d].dcap
+ except KeyError:
+ dcap = None
+ dir = getattr(d, 'dir', 1)
+ cur_dcap = getattr(d, 'dcap', Dragon.DCAP)
+ if dcap != cur_dcap:
+ dcap = cur_dcap.copy()
+ if isinstance(d, Dragon):
+ if d.monstervisible():
+ flag = 'visible'
+ else:
+ flag = 'hidden'
+ else:
+ flag = 'other'
+ entry = SavedDragonEntry(d, flag, dir, dcap)
+ new_entries[d] = entry
+ dragonlist.append(entry)
+ self.latest_entries = new_entries
+ entry = SavedFrameEntry(tick, dragonlist)
+ self.saved_last.saved_next = entry
+ self.saved_last = entry
+ return entry
+
+ def taken(self, dragons):
+ boards.replace_boardgen(self.jump_to_past())
+
+ def jump_to_past(self):
+ self.state = 'restoring'
+ boards.replace_boardgen(boards.next_board(fastreenter=True), 1)
+
+ def restore(self):
+ self.ghosts = {}
+ random.seed(self.randombase1)
+ random_module.seed(self.randombase2)
+
+ def show_ghosts(self, dragonlist, interact):
+ new_ghosts = {}
+ for entry in dragonlist:
+ try:
+ ghost = self.ghosts[entry.d]
+ except KeyError:
+ ghost = DragonGhost(entry)
+ ghost.setentry(entry)
+ new_ghosts[entry.d] = ghost
+ if (interact and entry.flag != 'other' and
+ not entry.dcap.get('infinite_shield')):
+ touching = images.touching(entry.x+1, entry.y+1, 30, 30)
+ touching.reverse()
+ for s in touching:
+ if isinstance(s, interact):
+ s.touched(ghost)
+ for d, ghost in self.ghosts.items():
+ if d not in new_ghosts:
+ ghost.kill()
+ self.ghosts = new_ghosts
+
+ def restore_frame_tick(self):
+ from bubbles import Bubble, DragonBubble
+ interact = (Bonus, Parabolic2, Bubble)
+ self.save_frame()
+ entry = self.saved_next
+ self.saved_next = entry.saved_next
+ self.common_tick(entry)
+ self.show_ghosts(entry.dragonlist, interact)
+ for args in entry.shoots1:
+ DragonBubble(*args)
+ if self.state == 'restoring' and self.ghosts:
+ self.state = 'post'
+ for ghost in self.ghosts.values():
+ ghost.integrate()
+
+ def flush_ghosts(self):
+ if self.latest_entries:
+ for ghost in self.ghosts.values():
+ ghost.disintegrate()
+ self.latest_entries.clear()
+ self.dragonlist = None
+
+bigclockticker = None
+
+class MultiStones(RandomBonus):
+ "Gems. Very demanded stones. It will take time to pick it up."
+ nimages = [Bonuses.emerald, Bonuses.sapphire, Bonuses.ruby]
+ Stones = [(Bonuses.emerald, 1000),
+ (Bonuses.sapphire, 2000),
+ (Bonuses.ruby, 3000),
+ ]
+ killgens = 0
+ big = 0
+ bigdoc = "Stones so big you will jump of joy picking them up."
+ def __init__(self, x, y, mode=None):
+ mode = mode or random.choice(MultiStones.Stones)
+ RandomBonus.__init__(self, x, y, *mode)
+ self.bigbonus = {'big': 1, 'outcome_args': (mode,),
+ 'megacls': LongDurationCactusbonus}
+ self.multi = 10
+ def taken1(self, dragons):
+ if self.big:
+ self.repulse(dragons)
+ self.multi -= (len(dragons) or 1)
+ if self.multi > 0:
+ self.taken_by = []
+ self.untouchable()
+ self.gen.append(self.touchdelay(5))
+ return -1 # don't go away
+ def repulse(self, dragons):
+ for d in dragons:
+ repulse_dragon(d)
+
+def repulse_dragon(d):
+ if hasattr(d, 'become_bubblingeyes'):
+ from bubbles import Bubble
+ ico = images.sprget(GreenAndBlue.normal_bubbles[d.bubber.pn][0])
+ b = Bubble(ico, d.x, d.y)
+ d.become_bubblingeyes(b)
+ b.pop()
+
+class Slippy(TemporaryBonus):
+ "Greased Feet. Do you want some ice skating?"
+ nimage = Bonuses.orange_thing
+ points = 900
+ capname = 'slippy'
+ captime = 606
+ bigbonus = {'multiply': 3}
+ bigdoc = "Zip zip zip bouncing off walls!"
+
+class Aubergine(TemporaryBonus):
+ "Mirror. The left hand is the one with the thumb on the right, right?"
+ nimage = Bonuses.aubergine
+ big = 0
+ bigbonus = {'big': 1, 'multiply': 2}
+ bigdoc = "Super Bonus-catching teleport ability."
+ def taken(self, dragon):
+ if self.big:
+ dragon.dcap['teleport'] = dragon.bubber.pcap['teleport'] = 1
+ else:
+ dragon.dcap['lookforward'] = -dragon.dcap['lookforward']
+ self.carried(dragon)
+ def endaction(self, dragon):
+ if self.big:
+ pass
+ else:
+ dragon.dcap['lookforward'] = -dragon.dcap['lookforward']
+
+class WhiteCarrot(TemporaryBonus):
+ "Fly. Become a great flying dragon!"
+ nimage = Bonuses.white_carrot
+ points = 650
+ capname = 'fly'
+ captime = 650
+ bigbonus = {'capname': 'jumpdown',
+ 'captime': 999999}
+ bigdoc = "Jump down off the ground!"
+ def taken(self, dragon):
+ TemporaryBonus.taken(self, dragon)
+ dragon.bubber.pcap['jumpdown'] = dragon.dcap['jumpdown']
+
+class AmphetamineSpeed(TemporaryBonus):
+ "Amphetamine Dose. Increase of your general speed!"
+ nimage = Bonuses.tin
+ points = 700
+ bigbonus = {'multiply': 3}
+ bigdoc = "Let's move!"
+ def taken(self, dragon):
+ dragon.angry = dragon.angry + [dragon.genangry()]
+ dragon.carrybonus(self, 633)
+ def endaction(self, dragon):
+ dragon.angry = dragon.angry[1:]
+
+class Sugar1(Bonus):
+ nimage = Bonuses.yellow_sugar
+ timeout = 2600
+ points = 250
+ def taken(self, dragon):
+ #if boards.curboard.wastingplay is None:
+ dragon.carrybonus(self, 99999)
+ #else:
+ # from player import scoreboard
+ # dragon.bubber.bonbons += 1
+ # scoreboard()
+
+class Sugar2(Sugar1):
+ timeout = 2500
+ points = 500
+ nimage = Bonuses.blue_sugar
+
+class Pear(RandomBonus):
+ "Pear. Will explode into sugars for your pockets but watch out or you'll lose them!"
+ points = 1000
+ nimage = Bonuses.green_thing
+ bigbonus = {'multiply': 4}
+ bigdoc = "The more the better."
+ def taken1(self, dragons):
+ starexplosion(self.x, self.y, 3 * self.multiply,
+ outcomes = [random.choice([(Sugar1,), (Sugar2,)])
+ for i in range(18 * self.multiply)])
+
+class Megalightning(ActiveSprite):
+ def __init__(self, dragon):
+ ActiveSprite.__init__(self, images.sprget(BigImages.blitz),
+ gamesrv.game.width, gamesrv.game.height)
+ self.gen.append(self.killing(dragon))
+
+ def killing(self, dragon):
+ from monsters import Monster
+ from bubbles import Bubble
+ poplist = [dragon]
+ while 1:
+ for s in self.touching(10):
+ if isinstance(s, Monster):
+ s.argh(poplist)
+ elif isinstance(s, Bubble):
+ s.pop(poplist)
+ yield None
+ yield None
+
+ def moving_to(self, x1, y1):
+ x0 = self.x
+ y0 = self.y
+ x1 += CELL - self.ico.w//2
+ y1 += CELL - self.ico.h//2
+ deltax = x1 - x0
+ if deltax > -100:
+ deltax = -100
+ deltay = y1 - y0
+ a = - deltay / float(deltax*deltax)
+ b = 2 * deltay / float(deltax)
+ for x in range(self.x, -self.ico.w, -13):
+ x1 = x - x0
+ self.move(x, y0 + int((a*x1+b)*x1))
+ yield None
+ self.kill()
+
+class Fish2(RandomBonus):
+ "Rotten Fish. Will blast monsters up to here, so move it around!"
+ points = 3000
+ nimage = Bonuses.fish2
+ big = 0
+ bigbonus = {'big': 1}
+ bigdoc = "Gives seven blasts."
+ def taken1(self, dragons):
+ dragon = random.choice(dragons or [None])
+ if not self.big:
+ m = Megalightning(dragon)
+ m.gen.append(m.moving_to(self.x, self.y))
+ else:
+ N = 7
+ base = random.random() * 2*math.pi
+ angles = [base + (math.pi*2 * n)/N for n in range(N)]
+ random.shuffle(angles)
+ for angle in angles:
+ m = Megalightning(dragon)
+ dx = 13 * math.cos(angle)
+ dy = 12 * math.sin(angle)
+ maxlive = max((gamesrv.game.width + m.ico.w) // 13,
+ (gamesrv.game.height + m.ico.h) // 12)
+ m.move(self.x + (self.ico.w - m.ico.w) // 2 - int(dx*maxlive),
+ self.y + (self.ico.h - m.ico.h) // 2 - int(dy*maxlive))
+ m.gen.append(m.straightline(dx, dy))
+ m.gen.append(m.die([None], maxlive*2))
+
+
+class Sheep(RandomBonus):
+ "Sheep. What a stupid beast!"
+ nimage = 'sheep-sm'
+ points = 800
+ big = 0
+ bigbonus = {'big': 1}
+ bigdoc = "You're a sheep. Let's bounce around."
+ def __init__(self, x, y):
+ RandomBonus.__init__(self, x, y)
+ if boards.curboard.bonuslevel:
+ self.kill()
+ def taken1(self, dragons):
+ if not self.big:
+ self.points0 = {}
+ for p in BubPlayer.PlayerList:
+ self.points0[p] = p.points
+ BubPlayer.LeaveBonus = self.boardleave()
+ else:
+ from player import Dragon
+ BubPlayer.SuperSheep = True
+ for p in BubPlayer.PlayerList:
+ for d in p.dragons:
+ if isinstance(d, Dragon):
+ d.become_monster('Sheep')
+
+ def boardleave(self):
+ from player import BubPlayer
+ BubPlayer.OverridePlayerIcon = images.sprget(self.nimage)
+ gamesrv.set_musics([], [])
+ images.Snd.Yippee.play()
+ slist = []
+ ico = images.sprget('sheep-big')
+ for p in BubPlayer.PlayerList:
+ if p.isplaying() and p.dragons:
+ d = random.choice(p.dragons)
+ dx = (d.ico.w - ico.w) // 2
+ dy = (d.ico.h - ico.h) // 2
+ s = ActiveSprite(ico, d.x + dx, d.y + dy)
+ dir = getattr(d, 'dir', None)
+ if dir not in [-1, 1]:
+ dir = random.choice([-1, 1])
+ s.gen.append(s.parabolic([dir, -2.0]))
+ slist.append(s)
+ for d in p.dragons[:]:
+ d.kill()
+ delta = {}
+ for p in BubPlayer.PlayerList:
+ if p.points or p.isplaying():
+ delta[p] = 2 * (self.points0[p] - p.points)
+ vy = 0
+ while delta or slist:
+ ndelta = {}
+ for p, dp in delta.items():
+ if dp:
+ d1 = max(-250, min(250, dp))
+ p.givepoints(d1)
+ if p.points > 0:
+ ndelta[p] = dp - d1
+ delta = ndelta
+ images.action(slist)
+ slist = [s for s in slist if s.y < boards.bheight]
+ yield 1
+
+class Flower(RandomBonus):
+ "Flower. Fire in all directions."
+ nimage = 'flower'
+ points = 800
+ big = 0
+ bigbonus = {'big': 1, 'multiply': 5}
+ bigdoc = "Rotational Bubble Thrower (tm)."
+ def taken(self, dragon):
+ if self.big:
+ dragon.dcap['bigflower'] = -99
+ dragon.dcap['autofire'] = 22
+ else:
+ dragon.dcap['flower'] += 12
+ dragon.carrybonus(self)
+
+class Flower2(TemporaryBonus):
+ "Bottom-up Flower. Turn you upside-down."
+ nimage = 'flower2'
+ points = 1000
+ big = 0
+ bigbonus = {'big': 1}
+ bigdoc = "Turn the level upside-down."
+ def __init__(self, *args):
+ RandomBonus.__init__(self, *args)
+ if self.x != OFFSCREEN:
+ while not underground(self.x, self.y):
+ self.step(0, -CELL)
+ if self.y < 0:
+ self.kill()
+ return
+ def taken1(self, dragons):
+ if self.big:
+ boards.extra_boardgen(boards.extra_swap_up_down())
+ else:
+ RandomBonus.taken1(self, dragons)
+ def faller(self):
+ while self.y >= 0:
+ if underground(self.x, self.y):
+ yield None
+ yield None
+ else:
+ self.move(self.x, (self.y-1) & ~3)
+ yield None
+ self.kill()
+ def taken(self, dragon):
+ dragon.dcap['gravity'] *= -1.0
+ self.carried(dragon)
+ def endaction(self, dragon):
+ dragon.dcap['gravity'] *= -1.0
+ def is_on_ground(self):
+ return underground(self.x, self.y)
+
+##class Moebius(RandomBonus):
+## "Moebius Band. Bottom left is top right and bottom right is top left... or vice-versa."
+## nimage = 'moebius'
+## points = 900
+## def taken1(self, dragons):
+## BubPlayer.Moebius = not BubPlayer.Moebius
+
+class StarBubble(FireBubble):
+ "Star Bubbles. Makes you fire bonus bubbles."
+ nimage = 'moebius'
+ bubkind = 'StarBubble'
+ bubcount = 3
+ bigbonus = {'bubcount': 10}
+ bigdoc = "More bonus bubbles => more confusion."
+
+class Donut(RandomBonus):
+ "Donut. Catch every free monster in a bubble."
+ nimage = Bonuses.donut
+ points = 950
+ big = 0
+ bigbonus = {'big': 1}
+ bigdoc = "Catch dragons too."
+
+ def taken1(self, dragons):
+ extra_boardgen(boards.extra_catch_all_monsters(dragons, self.big))
+ if self.big:
+ # catch all dragons as well
+ from bubbles import NormalBubble
+ for dragon in BubPlayer.DragonList[:]:
+ b = NormalBubble(dragon, dragon.x, dragon.y, 542)
+ if not dragon.become_bubblingeyes(b):
+ b.kill()
+
+
+Classes = [c for c in globals().values()
+ if type(c)==type(RandomBonus) and issubclass(c, RandomBonus)]
+Classes.remove(RandomBonus)
+Classes.remove(TemporaryBonus)
+Cheat = []
+#Classes = [Cactus, Insect] # CHEAT
+
+AllOutcomes = ([(c,) for c in Classes if c is not Fruits] +
+ 2 * [(MonsterBonus, lvl)
+ for lvl in range(len(Bonuses.monster_bonuses))])
+
+for c in Classes:
+ assert (getattr(c, 'points', 0) or 100) in GreenAndBlue.points[0], c
+
+def getdragonlist():
+ if bigclockticker and bigclockticker.dragonlist is not None:
+ return [entry for entry in bigclockticker.dragonlist
+ if entry.flag != 'other']
+ else:
+ return BubPlayer.DragonList
+
+def getvisibledragonlist():
+ if bigclockticker and bigclockticker.dragonlist is not None:
+ return [entry for entry in bigclockticker.dragonlist
+ if entry.flag == 'visible']
+ else:
+ return [d for d in BubPlayer.DragonList if d.monstervisible()]
+
+def record_shot(args):
+ if bigclockticker:
+ entry = bigclockticker.saved_last
+ if hasattr(entry, 'shoots1'):
+ entry.shoots1.append(args)
+
+
+def chooseground(tries=15):
+ avoidlist = getdragonlist()
+ for i in range(tries):
+ x0 = random.randint(2, boards.width-4)
+ y0 = random.randint(1, boards.height-3)
+ if (' ' == bget(x0,y0+1) == bget(x0+1,y0+1) and
+ '#' == bget(x0,y0+2) == bget(x0+1,y0+2)):
+ x0 *= CELL
+ y0 *= CELL
+ for dragon in avoidlist:
+ if abs(dragon.x-x0) < 3*CELL and abs(dragon.y-y0) < 3*CELL:
+ break
+ else:
+ return x0, y0
+ else:
+ return None, None
+
+def newbonus():
+ others = [s for s in images.ActiveSprites if isinstance(s, RandomBonus)]
+ if others:
+ return
+ if BubPlayer.SuperSheep:
+ return
+ x, y = chooseground()
+ if x is None:
+ return
+ cls = random.choice(Classes)
+ cls(x, y)
+
+##def newbonus():
+## others = [s for s in images.ActiveSprites if isinstance(s, RandomBonus)]
+## if others:
+## return
+## for cls in Classes:
+## x, y = chooseground(200)
+## if x is not None:
+## cls(x, y)
+
+def cheatnew():
+ if Cheat:
+ x, y = chooseground()
+ if x is None:
+ return
+ cls = random.choice(Cheat)
+ if not isinstance(cls, tuple):
+ cls = cls,
+ else:
+ Cheat.remove(cls)
+ if len(cls) > 1:
+ class C(cls[0]):
+ extra_cheat_arg = cls[1]
+ cls = (C,)
+ cls[0](x, y)
+
+def bonus_frame_tick():
+ if random.random() < 0.04:
+ cheatnew()
+ if random.random() < 0.15:
+ newbonus()
+ else:
+ import bubbles
+ bubbles.newbubble()
+
+def start_normal_play():
+ global bigclockticker
+ if bigclockticker and bigclockticker.state == 'restoring':
+ bigclockticker.restore()
+ return bigclockticker.restore_frame_tick
+ if (Clock and not boards.curboard.bonuslevel and
+ random.choice(Classes) is Clock):
+ bigclockticker = BigClockTicker()
+ return bigclockticker.save_frame_tick
+ else:
+ bigclockticker = None
+ return bonus_frame_tick
+
+def end_normal_play():
+ if bigclockticker and bigclockticker.state == 'post':
+ bigclockticker.flush_ghosts()
+
+# hack hack hack!
+def __cheat(c):
+ c = c.split(',')
+ c[0] = globals()[c[0]]
+ assert issubclass(c[0], Bonus)
+ Cheat.append(tuple(c))
+import __builtin__
+__builtin__.__cheat = __cheat
diff --git a/bubbob/bubbles.py b/bubbob/bubbles.py
new file mode 100644
index 0000000..9734826
--- /dev/null
+++ b/bubbob/bubbles.py
@@ -0,0 +1,1284 @@
+from __future__ import generators
+import random, math
+import gamesrv
+import images
+import boards
+from boards import *
+from images import ActiveSprite
+from mnstrmap import GreenAndBlue, LetterBubbles, Stars
+from mnstrmap import Lightning, Water, Fire, SpinningBalls, PlayerBubbles
+
+
+bubble_wind = {
+ '<': (-1, 0),
+ '>': (+1, 0),
+ '^': ( 0,-1),
+ 'v': ( 0,+1),
+ 'x': ( 0, 0),
+ }
+
+
+class Bubble(ActiveSprite):
+ exploding_bubbles = range(131,136)
+ red_bubbles = [156, 157, 156, 155]
+ white_bubbles = [164, 165, 164, 163]
+ pink_bubbles = [172, 173, 172, 171]
+ check_onbubble = ([(0,-1)], [(0,1)])
+
+ touchable = 1
+ warp = 0
+ default_windless = None
+ catch_dragons = None
+ nimages = GreenAndBlue.normal_bubbles
+
+ def touched(self, dragon):
+ dx, dy = dragon.x, dragon.y
+ o = []
+ if abs(self.x - dx) >= 25:
+ if self.x < dx:
+ o.append((1,0))
+ else:
+ o.append((-1,0))
+ if abs(self.y - dy) >= 25:
+ if self.y < dy:
+ o.append((0,1))
+ else:
+ o.append((0,-1))
+ if o:
+ self.obstacle = o
+ elif (self.catch_dragons and
+ abs(self.x - dx) < 15 and abs(self.y - dy) < 15):
+ if dragon not in self.catch_dragons:
+ self.catch_dragons.append(dragon)
+## elif not self.pop(getattr(dragon, 'poplist', None)):
+## if self.x < dx:
+## o.append((1,0))
+## else:
+## o.append((-1,0))
+## if self.y < dy:
+## o.append((0,1))
+## else:
+## o.append((0,-1))
+## self.obstacle = o
+ else:
+ self.pop(getattr(dragon, 'poplist', None))
+ return o == self.check_onbubble[dragon.bottom_up()]
+
+ def can_catch_dragons(self, author, catch_myself=0):
+ self.catch_dragons = [author]
+ if catch_myself:
+ self.catch_dragons.append(author)
+ self.move(author.x, author.y)
+ self.gen.append(self.catching_dragons())
+
+ def catching_dragons(self):
+ from player import Dragon
+ yield None # time to catch several dragons
+ dragons = [d for d in self.catch_dragons if isinstance(d, Dragon)]
+ self.catch_dragons = None
+ if len(dragons) >= 2:
+ import bonuses
+ author = dragons.pop(0)
+ imglist = [self.nimages[d.dcap.get('bubbericons',
+ d.bubber).pn][i]
+ for d in dragons for i in [1,2,1,0]]
+ self.setimages(self.cyclic(imglist))
+ self.warp = 1
+ caught = [(bonus.points, bonus) for d in dragons
+ if d.bubber is not author.bubber
+ for bonus in d.listcarrybonuses()
+ if isinstance(bonus, CatchNote)]
+ caught.sort()
+ # count caught dragons, excluding team mates, but including self
+ count = 0
+ for d in dragons:
+ if (d.bubber is author.bubber or
+ not d.bubber.sameteam(author.bubber)):
+ count += 1
+ if count:
+ if count == 1:
+ points = 250
+ else:
+ self.play(images.Snd.Extra)
+ if count == 2:
+ points = 10000
+ elif count == 3:
+ points = 30000
+ else:
+ points = 70000
+ caught.append((points, CatchNote(points)))
+ caught = caught[-3:]
+ for points, bonus in caught:
+ author.carrybonus(bonus, 111)
+ bonuses.points(self.x, self.y-HALFCELL, author, points)
+ for d in dragons:
+ d.become_bubblingeyes(self)
+ s_catch = author.bubber.stats.setdefault('catch', {})
+ s_catch[d.bubber] = s_catch.get(d.bubber, 0) + 1
+
+ def pop(self, poplist=None):
+ if self.touchable:
+ self.play(images.Snd.Pop)
+ self.poplist = poplist
+ self.untouchable()
+ self.gen = [self.die(Bubble.exploding_bubbles)]
+ if poplist:
+ dragon = poplist[0]
+ points = self.popped(dragon)
+ if dragon:
+ dragon.bubber.givepoints(points)
+ dragon.bubber.stats['bubble'] += 1
+ self.gen.append(self.poprec())
+ return 1
+ else:
+ return 0
+
+ def popped(self, dragon):
+ return 10
+
+ def poprec(self):
+ yield None
+ for s in self.touching(0):
+ if isinstance(s, Bubble):
+ s.pop(self.poplist)
+
+ def normal_movements(self, dx=0, dy=-1, timeout=800):
+ self.obstacle = []
+ time = 0
+ touchbubble = timeout = (timeout or 0) * 2
+ while 1:
+ del self.obstacle[:]
+ yield None
+ timeout -= 2
+ if not timeout:
+ self.setimages(self.bubble_red())
+ if timeout > touchbubble:
+ continue
+ if timeout&2:
+ for s in self.touching(13+(timeout&6)):
+ if isinstance(s, Bubble) and s is not self:
+ if (s.x-self.x)*dx > 0 or (s.y-self.y)*dy > 0:
+ touchbubble = timeout - (timeout&12) - 3
+ break
+ if timeout > touchbubble:
+ continue
+ if (dx,dy) not in self.obstacle:
+ if dx==dy==0:
+ if len(self.obstacle)==1:
+ dx1, dy1 = self.obstacle[0]
+ self.step(-dx1, -dy1)
+ else:
+ self.step(dx, dy)
+ if self.y < -32 or self.y >= boards.bheight:
+ if not self.warp:
+ self.poplist = None
+ self.kill()
+ return
+ self.vertical_warp()
+## dx = -dx
+ w = wget(self.x, self.y)
+ if w != ' ':
+ dx, dy = bubble_wind[w]
+ elif self.default_windless:
+ dx, dy = self.default_windless
+
+ if dx == dy == 0:
+ # this is the same as the whole loop but runs faster
+ while len(self.obstacle) != 1:
+ del self.obstacle[:]
+ yield None
+ timeout -= 2
+ if not timeout:
+ self.setimages(self.bubble_red())
+
+ def bubble_red(self, speed=5):
+ for n in self.imgseq(Bubble.white_bubbles, repeat=3):
+ yield n
+ for n in self.imgseq(Bubble.pink_bubbles, repeat=4):
+ yield n
+ for n in self.imgseq(Bubble.red_bubbles, repeat=4):
+ yield n
+ for n in self.imgseq([Bubble.pink_bubbles[0], Bubble.red_bubbles[0]],
+ speed=2, repeat=10):
+ yield n
+ self.pop()
+
+ def startnormalbubble(self, dx=0, dy=-1, timeout=800):
+ self.touchable = 1
+ self.gen.append(self.normal_movements(dx=dx, dy=dy, timeout=timeout))
+ imglist = GreenAndBlue.normal_bubbles[self.d.bubber.pn]
+ self.setimages(self.cyclic([imglist[1],
+ imglist[2],
+ imglist[1],
+ imglist[0]]))
+
+ def startsnookerbubble(self, timeout, hspeed, monsterpoplist=None):
+ self.gen.append(self.snooker_movements(dir=hspeed, timeout=timeout))
+ self.gen.append(self.kill_touching_monsters(monsterpoplist))
+ colorname = random.choice(Stars.COLORS)
+ imglist = [('smstar', colorname, k) for k in range(2)]
+ self.to_front()
+ s = images.ActiveSprite(images.sprget(imglist[-1]), self.x, self.y)
+ s.setimages(s.cyclic(imglist, speed=2))
+ s.gen.append(s.following(self))
+ def to_front():
+ Bubble.to_front(self)
+ s.to_front()
+ self.to_front = to_front
+
+ def snooker_movements(self, dir, dy=0.3, timeout=500):
+ icons = [images.sprget(n)
+ for n in GreenAndBlue.normal_bubbles[self.d.bubber.pn]]
+ icotimeout = 0
+ ico = icons[1]
+ yfrac = 0.0
+ self.dragon_jumped = False
+ for i in xrange(timeout):
+ hspeed = random.randrange(2, 4)
+ if ico is not icons[1]:
+ icotimeout += 1
+ if icotimeout >= 16:
+ ico = icons[1]
+ icotimeout = 0
+ elif abs(dy) > 16:
+ if dy > 0:
+ dy = 16.0
+ else:
+ dy = -16.0
+ self.touchable = 1
+ if self.dragon_jumped:
+ self.untouchable()
+ if self.dragon_jumped[1]: # bottom-up dragon
+ dy = -abs(dy)
+ else:
+ dy = abs(dy)
+ self.dragon_jumped = False
+ stepx = 0
+ y1 = (self.y + 27) // CELL
+ if dir < 0:
+ x1 = (self.x + 5) // CELL
+ if bget(x1, y1) == ' ' == bget(x1, y1-1):
+ stepx = -hspeed
+ else:
+ ico = icons[0]
+ dir = 1
+ else:
+ x1 = (self.x + 26) // CELL
+ if bget(x1, y1) == ' ' == bget(x1, y1-1):
+ stepx = hspeed
+ else:
+ ico = icons[0]
+ dir = -1
+ x = self.x
+ deltay = yfrac
+ if x < 32:
+ x += hspeed
+ dir = 1
+ elif x > boards.bwidth - 64:
+ x -= hspeed
+ dir = -1
+ else:
+ x += stepx
+ dy += 0.21
+ deltay = yfrac + dy
+ y = self.y
+ while deltay >= 1.0:
+ deltay -= 1.0
+ if onground(x, y-4):
+ ico = icons[2]
+ deltay = -deltay
+ dy = -abs(dy)
+ else:
+ y += 1
+ while deltay < 0.0:
+ deltay += 1.0
+ if underground(x, y+4):
+ ico = icons[2]
+ dy = abs(dy) * 0.95
+ break
+ y -= 1
+ self.move(x, y, ico)
+ self.vertical_warp()
+ yfrac = deltay
+ yield None
+ self.pop()
+
+ def kill_touching_monsters(self, poplist=None):
+ from monsters import Monster
+ poplist = poplist or [self.d]
+ while 1:
+ yield None
+ for s in self.touching(10):
+ if isinstance(s, Monster):
+ s.argh(poplist)
+ yield None
+
+
+class NormalBubble(Bubble):
+ warp = 1
+
+ def __init__(self, dragon, x, y, timeout=800):
+ imglist1 = GreenAndBlue.new_bubbles[dragon.bubber.pn]
+ Bubble.__init__(self, images.sprget(imglist1[0]), x, y)
+ self.d = dragon
+ self.startnormalbubble(timeout=timeout)
+
+class SnookerBubble(Bubble):
+ warp = 1
+ touchable = 0
+
+ def __init__(self, dragon, x, y, timeout=500):
+ imglist1 = GreenAndBlue.new_bubbles[dragon.bubber.pn]
+ Bubble.__init__(self, images.sprget(imglist1[0]), x, y)
+ self.d = dragon
+ self.startsnookerbubble(timeout=timeout, hspeed=dragon.dir)
+
+
+class BigBubbleCatcher(ActiveSprite):
+ numlist = [(PlayerBubbles.explosion[2], 1),
+ (PlayerBubbles.explosion[1], 2),
+ (PlayerBubbles.explosion[0], 2),
+ (PlayerBubbles.bubble[1], 5),
+ (PlayerBubbles.appearing[4], 3),
+ (PlayerBubbles.appearing[3], 3),
+ (PlayerBubbles.appearing[2], 2),
+ (PlayerBubbles.appearing[1], 2),
+ (PlayerBubbles.appearing[0], 2)]
+
+ def __init__(self, dragon, target, timeout):
+ img = images.sprget(PlayerBubbles.explosion[2])
+ ActiveSprite.__init__(self, img, -img.w, 0)
+ self.dragon = dragon
+ self.target = target
+ self.timeout = timeout
+ self.gen.append(self.follow())
+ self.recenter(PlayerBubbles.explosion[2])
+ for imgnum, delay in self.numlist:
+ images.sprget(imgnum) # preload
+
+ def recenter(self, imgnum):
+ s = self.target
+ if not s.alive or not s.touchable:
+ self.kill()
+ else:
+ img = images.sprget(imgnum)
+ self.move(s.x + (s.ico.w - img.w) // 2,
+ s.y + (s.ico.h - img.h) // 2,
+ img)
+
+ def follow(self):
+ for imgnum, delay in self.numlist:
+ for t in range(delay):
+ self.recenter(imgnum)
+ yield None
+ self.recenter(imgnum)
+ if self.alive:
+ s = self.target
+ s.in_bubble(NormalBubble(self.dragon, s.x, s.y, self.timeout))
+ self.kill()
+
+
+class CatchNote:
+ def __init__(self, points):
+ self.points = points
+ def endaction(self, dragon):
+ pass
+
+
+class DragonBubble(Bubble):
+ touchable = 0
+
+ def __init__(self, d, x, y, dir, special_bubble=None, angle=0,
+ thrustfactor=None, shootthrust=None):
+ self.d = d
+ pn = d.bubber.pn
+ imglist1 = GreenAndBlue.new_bubbles[pn]
+ imglist2 = GreenAndBlue.normal_bubbles[pn]
+ if angle:
+ asin, acos = math.sin(angle), math.cos(angle)
+ else:
+ asin, acos = 0, 1
+ Bubble.__init__(self, images.sprget(imglist1[0]), x + 12*dir, y)
+ self.setimages(self.imgseq(imglist1[1:] + imglist2[2:3], 4))
+ if shootthrust is None:
+ shootthrust = d.dcap['shootthrust']
+ hspeed = dir * shootthrust
+ if thrustfactor is not None:
+ negative = hspeed < 0
+ hspeed = (abs(hspeed) - 4.0) * thrustfactor + 4.0
+ if negative:
+ hspeed = -hspeed
+ self.gen.append(self.throw_bubble(hspeed, special_bubble, (acos,asin)))
+
+ def throw_bubble(self, hspeed, special_bubble=None, (acos,asin)=(1,0)):
+ from monsters import Monster
+ nx = self.x
+ ny = self.y
+ stop = 0
+ withmonster = 0
+ specialangle = (acos,asin) != (1,0)
+ if special_bubble == 'BigFireBubble':
+ if not specialangle:
+ BigFireBubble(self.x, self.y, hspeed, self.d)
+ self.kill()
+ return
+
+ self.warp = 0
+ monsterpoplist = [self.d]
+ while abs(hspeed) >= 4.0:
+ touched_monsters = [s for s in self.touching(9)
+ if isinstance(s, Monster)]
+ if touched_monsters:
+ if special_bubble == 'SnookerBubble':
+ for monster in touched_monsters:
+ monster.argh(monsterpoplist)
+ else:
+ monster = random.choice(touched_monsters)
+ in_bubble = monster.in_bubble(self)
+ withmonster = self.withmonster = 1
+ if in_bubble is None:
+ self.warp = 1
+ try:
+ key = monster.mdef.jailed[0]
+ except AttributeError:
+ pass
+ else:
+ s_monster = self.d.bubber.stats.setdefault(
+ 'monster', {})
+ s_monster[key] = s_monster.get(key, 0) + 1
+ break
+ if specialangle:
+ nx, ny = vertical_warp(nx + hspeed*acos, ny + hspeed*asin)
+## if moebius:
+## acos = -acos
+ else:
+ nx += hspeed
+ hspeed *= 0.965
+ xc = int(nx-3.8)//CELL+1
+ yc = (self.y+HALFCELL)//CELL
+ if bget(xc,yc) == '#' == bget(xc, yc+1):
+ stop += 1
+ if stop <= 1:
+ self.move(int(nx+0.5), int(ny+0.5))
+ yield None
+
+ if special_bubble == 'SnookerBubble':
+ if stop > 1:
+ hspeed = -hspeed
+ self.startsnookerbubble(self.d.dcap['bubbledelay'] or 800,
+ hspeed, monsterpoplist)
+ return
+ if not withmonster:
+ from bonuses import Bonus, BonusMaker
+ touched_bonuses = [s for s in self.touching(15)
+ if isinstance(s, Bonus) and s.bubblable]
+ if touched_bonuses:
+ random.choice(touched_bonuses).in_bubble(self)
+ withmonster = 1
+ else:
+ touched_bonuses = [s for s in self.touching(7)
+ if isinstance(s, BonusMaker)]
+ if touched_bonuses:
+ bonusmaker = random.choice(touched_bonuses)
+ if bonusmaker.in_bubble(self):
+ withmonster = 1
+ if not self.alive:
+ return
+ if special_bubble:
+ cls = globals()[special_bubble]
+ if not withmonster:
+ b = cls(self.d.bubber.pn)
+ b.move(self.x, self.y)
+ b.can_catch_dragons(self.d, hspeed == 0)
+ self.kill()
+ return
+ bubbledelay = self.d.dcap['bubbledelay']
+ if bubbledelay:
+ timeout = 1
+ if bubbledelay > 1:
+ self.gen.append(self.delayed_pop(7))
+ else:
+ timeout = 800
+ self.startnormalbubble(timeout=timeout)
+ if not withmonster:
+ self.can_catch_dragons(self.d, hspeed == 0)
+
+ def delayed_pop(self, delay):
+ for i in range(delay):
+ yield None
+ self.pop()
+
+
+class FishBubble(Bubble):
+ touchable = 0
+
+ def __init__(self, dragon):
+ ico = images.sprget(GreenAndBlue.new_bubbles[dragon.bubber.pn][0])
+ Bubble.__init__(self, ico, dragon.x + dragon.dir*12, dragon.y)
+ timeout = random.randrange(50, 150)
+ self.gen.append(self.normal_movements(timeout=timeout))
+ self.gen.append(self.fuzz())
+
+ def fuzz(self):
+ while 1:
+ prevx = self.x
+ yield None
+ yield None
+ yield None
+ if prevx == self.x:
+ self.step(random.choice([-1, 1]), 0)
+
+ def bubble_red(self, *args, **kwds):
+ self.gen.append(self.die([]))
+
+
+class BubblingEyes(ActiveSprite):
+
+ def __init__(self, bubber, saved_caps, bubble):
+ ico = images.sprget(('eyes', 0, 0))
+ ActiveSprite.__init__(self, ico, bubble.x, bubble.y)
+ self.bubber = bubber
+ self.dcap = saved_caps
+ self.gen = [self.playing_bubble(bubble)]
+
+ def bottom_up(self):
+ return self.dcap['gravity'] < 0.0
+
+ def playing_bubble(self, bubble):
+ from player import Dragon
+ bottom_up = self.bottom_up()
+ flip = 'vflip'*bottom_up
+ timer = 0
+ red = 0
+ normalbub = bubble.imgsetter
+ redblinker = bubble.cyclic([Bubble.pink_bubbles[0], Bubble.red_bubbles[0]], 2)
+ bubber = self.bubber
+ ndir = random.choice([-1, 1])
+ prev_dx_dy = None
+ while not hasattr(bubble, 'poplist'):
+ dx = bubber.wannago(self.dcap)
+ if dx:
+ ndir = dx
+ if bubber.key_jump:
+ dy = -1
+ else:
+ dy = 0
+ if bubber.key_fire:
+ red += 1
+ if red > 20:
+ d = Dragon(bubber, self.x, self.y, ndir, self.dcap)
+ Bubble.pop(bubble, [d]) # hack to pop SolidBubbles too
+ d.kill()
+ break
+ if bubble.imgsetter is not redblinker:
+ normalbub = bubble.imgsetter
+ bubble.setimages(redblinker)
+ else:
+ #red = 0
+ if bubble.imgsetter is redblinker:
+ bubble.setimages(normalbub)
+ key = ('eyes', dx, dy)
+ if timer < 50:
+ if (timer % 9) < 3:
+ key = 'eyes-blink'
+ elif random.random() < 0.1:
+ key = 'eyes-blink'
+ timer += 1
+ if bubble.x <= 3*HALFCELL and dx < 0:
+ dx = 0
+ if bubble.x >= boards.bwidth - 7*HALFCELL and dx > 0:
+ dx = 0
+ if bottom_up:
+ dy = -dy
+ nx = bubble.x + dx
+ ny = bubble.y + dy
+ if timer&1:
+ nx += dx
+ else:
+ ny += dy
+ nx, ny = vertical_warp(nx, ny)
+ bubble.move(nx, ny)
+ self.move(nx+dx, ny+dy, images.sprget((flip, key)))
+## if moebius:
+## self.dcap['left2right'] *= -1
+ if dx == dy == 0:
+ bubble.default_windless = prev_dx_dy
+ else:
+ prev_dx_dy = dx, dy
+ bubble.default_windless = 0, 0
+ yield None
+ # jumping out of the bubble
+ if bottom_up:
+ kw = {'gravity': -0.3}
+ else:
+ kw = {}
+ from player import BubPlayer
+ bi = self.dcap.get('bubbericons', bubber)
+ if BubPlayer.SuperFish and 'fish' in bi.transformedicons:
+ self.setimages(None)
+ self.seticon(bi.transformedicons['fish'][0, +1])
+ else:
+ self.setimages(self.cyclic(
+ [(flip, n) for n in GreenAndBlue.comming[bi.pn]], 2))
+ dxy = [(random.random()-0.5) * 9.0,
+ (random.random()+0.5) * (-5.0,5.0)[bottom_up]]
+ for n in self.parabolic(dxy, 1, **kw):
+ yield n
+ if dxy[1] * (1,-1)[bottom_up] >= 4.0:
+ break
+ if dxy[0] < 0:
+ ndir = -1
+ else:
+ ndir = 1
+ d = Dragon(bubber, self.x, self.y, ndir, self.dcap)
+ d.dcap['shield'] = 50
+ bubber.dragons.append(d)
+ self.kill()
+
+ def kill(self):
+ try:
+ self.bubber.dragons.remove(self)
+ except ValueError:
+ pass
+ ActiveSprite.kill(self)
+
+
+class BonusBubble(Bubble):
+ max = None
+ timeout = None
+ flip = ''
+
+ def __init__(self, pn, nimages=None, top=None):
+ if nimages is None:
+ nimages = self.nimages[pn]
+ b = boards.curboard
+ if top is None:
+ top = b.top
+ if top == 0:
+ testline = b.walls[-1]
+ x, y = self.findhole(testline), boards.bheight
+ dx, dy = 0, -1
+ elif top == 1:
+ testline = b.walls[0]
+ x, y = self.findhole(testline), -2*CELL
+ dx, dy = 0, 1
+ elif top == 2:
+ x, y = -2*CELL, random.randint(2*CELL, boards.bheight-4*CELL)
+ dx, dy = 1, 0
+ else: # top == 3:
+ x, y = (boards.bwidth - CELL,
+ random.randint(2*CELL, boards.bheight-4*CELL))
+ dx, dy = -1, 0
+ Bubble.__init__(self, images.sprget((self.flip, nimages[0])), x, y)
+ self.gen.append(self.normal_movements(dx=dx, dy=dy,
+ timeout=self.timeout))
+ if len(nimages) == 3:
+ nimages = [nimages[1], nimages[2], nimages[1], nimages[0]]
+ if len(nimages) > 1:
+ self.setimages(self.cyclic([(self.flip, n) for n in nimages]))
+
+ def findhole(self, testline):
+ holes = [x for x in range(len(testline)-1) if testline[x:x+2]==' ']
+ if not holes:
+ holes = range(2, len(testline)-3)
+ return random.choice(holes) * CELL
+
+ def thrown_bubble(self, x, y, hspeed, acossin):
+ self.untouchable()
+ self.move(x, y)
+ self.gen = [self.throwing_bubble(hspeed, acossin, self.imgsetter)]
+
+ def throwing_bubble(self, hspeed, (acos,asin), restore_img):
+ nx = self.x
+ ny = self.y
+ while abs(hspeed) >= 4.0:
+ nx, ny = vertical_warp(nx + hspeed*acos, ny + hspeed*asin)
+## if moebius:
+## acos = -acos
+ if nx <= CELL:
+ acos = abs(acos)
+ if nx >= boards.bwidth-3*CELL:
+ acos = -abs(acos)
+ hspeed *= 0.965
+ self.move(int(nx+0.5), int(ny+0.5))
+ yield None
+ self.touchable = 1
+ self.gen.append(self.normal_movements(timeout=self.timeout))
+ self.setimages(restore_img)
+
+
+class PlainBubble(BonusBubble):
+ timeout = 500
+ def condition():
+ return boards.curboard.holes
+
+def extend_name(l):
+ text = 'extend'
+ return text[:l] + text[l].upper() + text[l+1:]
+
+class LetterBubble(BonusBubble):
+ max = 2
+ def condition():
+ return boards.curboard.letter
+ def __init__(self, pn, l=None):
+ if l is None:
+ l = random.randint(0,5)
+ self.l = l
+ lettername = extend_name(self.l)
+ BonusBubble.__init__(self, pn, nimages=getattr(LetterBubbles, lettername))
+ def popped(self, dragon):
+ if dragon:
+ dragon.bubber.giveletter(self.l)
+ return 50
+
+class FireFlame(ActiveSprite):
+ timeout = 17
+ def __init__(self, x0, y0, poplist, dirs=None, countdown=0, flip=''):
+ ico = images.sprget((flip, Fire.ground[0]))
+ ActiveSprite.__init__(self, ico, x0*CELL, y0*CELL)
+ if not countdown:
+ dirs = []
+ self.poplist = poplist
+ self.gen.append(self.burning(dirs, countdown))
+ self.setimages(self.cyclic([(flip, n) for n in Fire.ground], 1))
+ def burning(self, dirs, countdown):
+ from monsters import Monster
+ x0 = self.x//CELL
+ y0 = self.y//CELL
+ for dir in dirs:
+ if bget(x0+dir, y0+1) == '#' and bget(x0+dir, y0) == ' ':
+ FireFlame(x0+dir, y0, self.poplist, [dir], countdown-1)
+ for i in range(self.timeout):
+ yield None
+ if self.poplist:
+ for s in self.touching(0):
+ if isinstance(s, Monster):
+ s.argh(self.poplist)
+ yield None
+ self.kill()
+
+class FireDrop(ActiveSprite):
+ def __init__(self, x, y, poplist=None):
+ ActiveSprite.__init__(self, images.sprget(Fire.drop), x, y)
+ self.poplist = poplist or [None]
+ self.gen.append(self.dropping())
+ def dropping(self):
+ x0 = self.x//CELL
+ while bget(x0, self.y//CELL) == '#' or bget(x0, self.y//CELL+1) != '#':
+ if self.y >= boards.bheight:
+ self.kill()
+ return
+ self.move(self.x, (self.y + 8) & ~7)
+ yield None
+ y0 = self.y//CELL
+ #if bget(x0-1, y0) == ' ':
+ FireFlame(x0, y0, self.poplist, [-1, 1], 5)
+ self.kill()
+
+class FireBubble(BonusBubble):
+ max = 4
+ nimages = GreenAndBlue.fire_bubbles
+ def condition():
+ return boards.curboard.fire
+ def popped(self, dragon):
+ if dragon:
+ x0 = self.x // CELL + 1
+ y0 = self.y // CELL + 1
+ if bget(x0, y0) == '#':
+ x1 = (self.x + HALFCELL) // CELL
+ if x1 == x0:
+ tries = [x1+1, x1-1]
+ else:
+ tries = [x1, x1+2]
+ for x1 in tries:
+ if bget(x1, y0) == ' ':
+ x0 = x1
+ break
+ FireDrop(x0*CELL, self.y)
+ return 10
+
+##class BombBubble(FireBubble):
+## flip = 'vflip'
+## def popped(self, dragon):
+## if dragon:
+## import bonuses
+## bonuses.bomb_explosion(self.x, self.y + CELL, starmul=1)
+## return 100
+
+##class WaterCell(ActiveSprite):
+## ICONS = {
+## ( 0,1, None) : Water.bottom,
+## ( 1,0, None) : Water.start_left,
+## (-1,0, None) : Water.start_right,
+## ( 0,0, None) : Water.bottom,
+
+## (0,1, 0,1) : Water.v_flow,
+## (0,1, 1,0) : Water.bl_corner,
+## (0,1, -1,0) : Water.br_corner,
+
+## (-1,0, 0,1) : Water.tl_corner,
+## (-1,0, 1,0) : Water.start_right,
+## #(-1,0, -1,0) : Water.h_flow,
+
+## (1,0, 0,1) : Water.tr_corner,
+## #(1,0, 1,0) : Water.h_flow,
+## (1,0, -1,0) : Water.start_left,
+
+## (0,0, 0,1) : Water.top,
+## (0,0, 1,0) : Water.top,
+## (0,0, -1,0) : Water.top,
+
+## (None, 0,1) : Water.top,
+## (None, -1,0) : Water.start_left,
+## (None, 1,0) : Water.start_right,
+## (None, 0,0) : Water.top,
+## }
+
+## def __init__(self, x, y):
+## ActiveSprite.__init__(self, images.sprget(Water.top), x, y)
+## self.touchable = 1
+
+## def ready(self, celllist):
+## self.gen.append(self.flooding(celllist))
+
+## def flooding(self, celllist):
+## from monsters import Monster
+## x0 = self.x // 16
+## y0 = self.y // 16
+## ping = 0
+## dir = random.choice([-1, 1])
+## take_with_us = [[] for cell in celllist]
+## poplist = [None]
+## icons = {}
+## for key, value in self.ICONS.items():
+## icons[key] = images.sprget(value)
+## icodef = images.sprget(Water.h_flow)
+## stop = 0
+## while not stop:
+## dx = dy = 0
+## if bget(x0, y0+1) == ' ':
+## dy = y0*16 < boards.bheight
+## ping = 0
+## elif bget(x0+dir, y0) == ' ':
+## dx = dir
+## elif bget(x0-dir, y0) == ' ':
+## ping += 1
+## if ping < 3:
+## dir = -dir
+## dx = dir
+## # change the head icon
+## head = celllist[0]
+## second = celllist[1]
+## head.seticon(icons.get((x0-second.x//16, y0-second.y//16,
+## dx, dy), icodef))
+## # move the tail to the new head position
+## x0 += dx
+## y0 += dy
+## newhead = celllist.pop()
+## celllist.insert(0, newhead)
+## newhead.move(x0*16, y0*16, icons.get((dx,dy, None), icodef))
+## # change the new tail icon
+## tail = celllist[-1]
+## second = celllist[-2]
+## tail.seticon(icons.get((None, (second.x-tail.x)//16,
+## (second.y-tail.y)//16), icodef))
+## # take monsters with us
+## for i in range(0, len(celllist), 3):
+## for s in celllist[i].touching(0):
+## if isinstance(s, Monster):
+## s.untouchable()
+## s.gen = []
+## take_with_us[i].append(s)
+## elif isinstance(s, Bubble):
+## s.pop(poplist)
+## yield 0
+## stop = dx == dy == 0
+## for cell, takelist in zip(celllist, take_with_us):
+## stop &= cell.x == newhead.x and cell.y == newhead.y
+## for s in takelist:
+## if s.alive:
+## s.move(x2bounds(cell.x-8), cell.y-16)
+## if stop:
+## s.argh(poplist, onplace=1)
+## for c in celllist:
+## c.kill()
+## def touched(self, dragon):
+## dragon.watermove(x2bounds(self.x-HALFCELL), self.y-CELL+1)
+## return 1
+
+class WaterCell(ActiveSprite):
+ TESTLIST = [(-CELL,0), (CELL,0), (0,CELL), (0,-CELL)]
+ ICONS = [Water.v_flow,
+ Water.start_left,
+ Water.start_right,
+ Water.h_flow,
+ Water.top,
+ Water.tr_corner,
+ Water.tl_corner,
+ Water.h_flow,
+
+ Water.bottom,
+ Water.br_corner,
+ Water.bl_corner,
+ Water.h_flow,
+ Water.v_flow,
+ Water.v_flow,
+ Water.v_flow,
+ Water.v_flow]
+
+ def __init__(self, x, y, dir, watercells, poplist, repeat):
+ ActiveSprite.__init__(self, images.sprget(Water.top), x, y)
+ self.poplist = poplist
+ self.take_with_me = []
+ self.ping = 0
+ self.repeat = repeat
+ self.watercells = watercells
+ self.touchable = repeat % 3 == 1
+ if (x, y, dir) not in watercells:
+ watercells[x,y,dir] = self
+ if None not in watercells or not watercells[None].alive:
+ self.in_charge()
+ else:
+ watercells[x,y,dir].join(self)
+
+ def join(self, other):
+ self.take_with_me += other.take_with_me
+ self.ping = min(self.ping, other.ping)
+ self.repeat += other.repeat
+ self.touchable = self.touchable or other.touchable
+ del other.take_with_me[:]
+ other.kill()
+
+ def in_charge(self):
+ self.gen = [self.flooding()]
+ self.watercells[None] = self
+
+ def kill(self):
+ from monsters import Monster
+ for s in self.take_with_me[:]:
+ if isinstance(s, Monster) and s.alive:
+ s.argh(self.poplist, onplace=1)
+ del self.take_with_me[:]
+ ActiveSprite.kill(self)
+ if not self.watercells[None].alive:
+ del self.watercells[None]
+ for s in self.watercells.values():
+ if s.alive:
+ s.in_charge()
+ break
+
+ def flooding(self):
+ from monsters import Monster
+ watercells = self.watercells
+ while watercells[None] is self:
+
+ new = []
+ nwatercells = {None: self}
+ for key, s in watercells.items():
+ if key:
+ x, y, dir = key
+ if s.repeat:
+ new.append((x, y, dir, watercells,
+ s.poplist, s.repeat-1))
+ s.repeat = 0
+ x0 = x // CELL
+ y0 = y // CELL
+ if bget(x0, y0+1) == ' ':
+ if y >= boards.bheight:
+ s.kill()
+ continue
+ s.ping = 0
+ y += CELL
+ elif bget(x0+dir, y0) == ' ':
+ x += dir*CELL
+ elif bget(x0-dir, y0) == ' ':
+ s.ping += 1
+ if s.ping == 3:
+ s.kill()
+ continue
+ dir = -dir
+ x += dir*CELL
+ else:
+ s.kill()
+ continue
+ key = x, y, dir
+ if key in nwatercells:
+ nwatercells[key].join(s)
+ else:
+ nwatercells[key] = s
+
+ watercells.clear()
+ watercells.update(nwatercells)
+ for args in new:
+ WaterCell(*args)
+
+ for key, s in watercells.items():
+ if key:
+ x, y, dir = key
+ flag = 0
+ for k in range(4):
+ dx, dy = s.TESTLIST[k]
+ if ((x+dx, y+dy, -1) in watercells or
+ (x+dx, y+dy, 1) in watercells):
+ flag += 1<<k
+ ico = images.sprget(s.ICONS[flag])
+ s.move(x, y, ico)
+ if s.touchable:
+ for s1 in s.touching(0):
+ if isinstance(s1, Monster):
+ s1.untouchable()
+ s1.gen = []
+ s.take_with_me.append(s1)
+ elif isinstance(s1, Bubble):
+ s1.pop(s.poplist)
+ for s1 in s.take_with_me:
+ if s1.alive:
+ s1.move(x2bounds(x-HALFCELL), y-CELL)
+ yield None
+ if not watercells[None].alive:
+ self.in_charge()
+
+ def touched(self, dragon):
+ dragon.watermove(x2bounds(self.x-HALFCELL), self.y-CELL+1)
+ return 1
+
+def watercell(x, y, poplist, dir=None, repeat=4):
+ b = boards.curboard
+ if not hasattr(b, 'watercells'):
+ b.watercells = {}
+ dir = dir or random.choice([-1, 1])
+ WaterCell(x, y, dir, b.watercells, poplist, repeat)
+
+class WaterBubble(BonusBubble):
+ max = 4
+ nimages = GreenAndBlue.water_bubbles
+ def condition():
+ return boards.curboard.water
+ def popped(self, dragon):
+ if dragon:
+ x0 = self.x // CELL + 1
+ y0 = self.y // CELL + 1
+ for x1 in [x0, x0+1, x0-1]:
+ if bget(x1,y0) == ' ' or bget(x1,y0+1) == ' ':
+ x0 = x1
+ break
+ watercell(x0*CELL, y0*CELL, [None], repeat=19)
+ return 10
+
+##class SolidBubble(WaterBubble):
+## timeout = 450
+## solidbubble = 1
+## flip = 'vflip'
+
+## def bubble_red(self, *args):
+## self.solidbubble = 0
+## return WaterBubble.bubble_red(self, *args)
+
+## def pop(self, poplist=None):
+## return (not (self.solidbubble and poplist is not None)
+## and WaterBubble.pop(self, poplist))
+
+## def popped(self, dragon):
+## return 100
+
+class FiredLightning(ActiveSprite):
+ def __init__(self, x, y, dir, poplist, diry=0):
+ ActiveSprite.__init__(self, images.sprget(Lightning.fired), x, y)
+ self.dir = int(13*dir)
+ self.diry = int(13*diry)
+ self.gen.append(self.moving(poplist))
+ def moving(self, poplist):
+ from monsters import Monster
+ while (-2*CELL < self.x < boards.bwidth and
+ -2*CELL < self.y < boards.bheight):
+ for s in self.touching(2):
+ if isinstance(s, Monster):
+ s.argh(poplist)
+ elif isinstance(s, Bubble):
+ s.pop(poplist)
+ self.step(self.dir, self.diry)
+ yield None
+ self.kill()
+
+class LightningBubble(BonusBubble):
+ max = 4
+ nimages = GreenAndBlue.light_bubbles
+ def condition():
+ return boards.curboard.lightning
+ def popped(self, dragon):
+ if dragon:
+ FiredLightning(self.x, self.y, -dragon.dir, self.poplist)
+ return 10
+
+class BigLightBubble(LightningBubble):
+ flip = 'vflip'
+ def popped(self, dragon):
+ base = random.random() * 2.0*math.pi
+ for a in range(7):
+ FiredLightning(self.x, self.y, math.cos(base+a), self.poplist,
+ diry = -math.sin(base+a))
+ return 100
+
+
+class BigFireBubble(ActiveSprite):
+ touchable = 1
+
+ def __init__(self, x, y, hspeed, author):
+ if hspeed > 0:
+ imgs = PlayerBubbles.right_weapon
+ else:
+ imgs = PlayerBubbles.left_weapon
+ ActiveSprite.__init__(self, images.sprget(imgs[-1]), x, y)
+ self.setimages(self.cyclic(imgs, 2))
+ self.author = author
+ self.gen.append(self.moving(hspeed))
+ self.play(images.Snd.Shh)
+
+ def moving(self, hspeed):
+ from monsters import Monster
+ if abs(hspeed) < 3:
+ if hspeed > 0:
+ hspeed = 3
+ else:
+ hspeed = -3
+ fx = self.x
+ poplist = [self.author]
+ while 1:
+ fx += hspeed
+ self.move(int(fx), self.y)
+ xc = int(fx)//CELL + 1
+ yc = (self.y+HALFCELL)//CELL
+ if bget(xc,yc) == '#' == bget(xc, yc+1):
+ break
+ yield None
+ for s in self.touching(4):
+ if isinstance(s, Monster):
+ s.argh(poplist)
+ self.kill()
+
+ def touched(self, dragon):
+ if dragon is not self.author:
+ import bonuses
+ bonuses.repulse_dragon(dragon)
+
+
+class SpinningBall(ActiveSprite):
+ def __init__(self, x, y, poplist):
+ ActiveSprite.__init__(self, images.sprget(SpinningBalls.free[-1]), x,y)
+ self.poplist = poplist
+ self.gen.append(self.dropping())
+ imgs = SpinningBalls.free
+ if random.random() < 0.5:
+ imgs = list(imgs)
+ imgs.reverse()
+ self.setimages(self.cyclic(imgs, random.randrange(2,5)))
+ self.touchable = 1
+ def dropping(self):
+ from monsters import Monster
+ for ny in range(self.y, boards.bheight, 5):
+ self.move(self.x, ny)
+ yield None
+ for s in self.touching(0):
+ if isinstance(s, Monster):
+ s.argh(self.poplist)
+ elif isinstance(s, Bubble):
+ s.pop(self.poplist)
+ self.kill()
+ def touched(self, dragon):
+ dragon.die()
+
+class StarBubble(BonusBubble):
+ timeout = 250
+ def __init__(self, pn):
+ self.colorname = random.choice(Stars.COLORS)
+ BonusBubble.__init__(self, pn, [('starbub', self.colorname, i)
+ for i in range(3)])
+## def __init__(self, pn):
+## BonusBubble.__init__(self, pn)
+## self.colorname = random.choice(Stars.COLORS)
+## starimg = [('smstar', self.colorname, 0),
+## ('smstar', self.colorname, 1)]
+## smallstar = ActiveSprite(images.sprget(starimg[-1]),
+## self.x+8, self.y+8)
+## smallstar.setimages(smallstar.cyclic(starimg))
+## smallstar.gen.append(smallstar.following(self, 8, 8))
+ def popped(self, dragon):
+ if dragon:
+ from bonuses import BonusMaker, AllOutcomes, Parabolic2
+ BonusMaker(self.x, self.y, getattr(Stars, self.colorname),
+ outcome=random.choice(AllOutcomes))
+ for i in range(2):
+ Parabolic2(self.x, self.y, [('smstar', self.colorname, i)
+ for i in range(2)])
+ return 100
+
+class MonsterBubble(BonusBubble):
+ timeout = 100
+ def __init__(self, pn, mcls):
+ import monsters, mnstrmap
+ BonusBubble.__init__(self, pn)
+ mdef = getattr(mnstrmap, mcls.__name__)
+ m = mcls(mdef, self.x, self.y, 1)
+ m.in_bubble(self)
+
+class MoreBubblesBubble(BonusBubble):
+
+ def can_catch_dragons(self, author, catch_myself=0):
+ # at this point, explode the bubble into more bubbles
+ d = author
+ for angle in [math.pi, math.pi/2, -math.pi/2, 0]:
+ for factor in [0.2, 0.55, 0.9, 1.25, 1.6]:
+ DragonBubble(d, self.x, self.y, d.dir,
+ angle = angle, thrustfactor = factor)
+ self.pop()
+
+
+Classes = ([PlainBubble] * 7 +
+ [FireBubble, WaterBubble, LightningBubble] * 4 +
+ [LetterBubble])
+
+def newbubble(): #force=0):
+ #if force:
+ #cls = PlainBubble
+ #else:
+ cls = random.choice(Classes)
+ if not cls.__dict__['condition']():
+ return
+ if cls.max is not None:
+ others = [s for s in images.ActiveSprites if isinstance(s, cls)]
+ if len(others) >= cls.max:
+ return
+ sendbubble(cls)
+
+def newforcedbubble():
+ choices = [PlainBubble] * 5
+ for cls in [FireBubble, WaterBubble, LightningBubble]:
+ if cls.__dict__['condition']():
+ n = 4
+ else:
+ n = 1
+ choices.extend([cls] * n)
+ cls = random.choice(choices)
+ return sendbubble(cls)
+
+def sendbubble(cls, *args, **kw):
+ from player import BubPlayer
+ players = [p for p in BubPlayer.PlayerList if p.isplaying()]
+ if not players:
+ return None
+ pn = random.choice(players).pn
+ return cls(pn, *args, **kw)
+
+def newbonusbubble():
+ boards.curboard.top = random.choice([0,0,0, 1,1,1, 2,2, 3,3])
+ r = random.random()
+ if r < 0.14:
+ sendbubble(random.choice(Classes))
+ elif r < 0.16:
+ from player import BubPlayer
+ import monsters
+ mcls = random.choice(monsters.MonsterClasses)
+ for d in BubPlayer.DragonList:
+ sendbubble(MonsterBubble, mcls)
+ else:
+ sendbubble(StarBubble)
diff --git a/bubbob/command.py b/bubbob/command.py
new file mode 100644
index 0000000..71239ed
--- /dev/null
+++ b/bubbob/command.py
@@ -0,0 +1,10 @@
+import os, sys
+
+levels, ext = os.path.splitext(os.path.basename(sys.argv[1]))
+for ext in ['.py', '.bin']:
+ levelfile = 'levels/%s%s' % (levels, ext)
+ if os.path.exists(levelfile):
+ break
+sys.argv[1] = levelfile
+
+execfile('bb.py')
diff --git a/bubbob/doc/.cvsignore b/bubbob/doc/.cvsignore
new file mode 100644
index 0000000..2d19fc7
--- /dev/null
+++ b/bubbob/doc/.cvsignore
@@ -0,0 +1 @@
+*.html
diff --git a/bubbob/doc/bonus-doc.py b/bubbob/doc/bonus-doc.py
new file mode 100755
index 0000000..f03b013
--- /dev/null
+++ b/bubbob/doc/bonus-doc.py
@@ -0,0 +1,170 @@
+#!/usr/bin/env python
+
+import os, sys, string, struct
+os.chdir(os.pardir)
+sys.path.insert(0, os.getcwd())
+sys.path.insert(1, os.path.abspath(os.path.join(os.pardir, 'common')))
+
+from images import sprmap
+import bonuses, images
+
+try:
+ import psyco; psyco.full()
+except ImportError:
+ pass
+
+def create_image(name,source,extralines=0,alt=''):
+ if len(sys.argv) == 2 and sys.argv[1] == '-i':
+ return
+ print name, source
+ src = open(source[0],'r')
+ assert src.readline().strip() == 'P6'
+ line = src.readline()
+ while line[0] == '#':
+ line = src.readline()
+ size = string.split(line)
+ w = string.atoi(size[0])
+ h = string.atoi(size[1])
+ c = src.readline().strip()
+ data = src.read()
+ src.close()
+ img = os.popen("convert PPM:- doc/images/"+name+'.png','w')
+ print >> img, 'P6'
+ print >> img, source[1][2], source[1][3]+extralines
+ print >> img, c
+ cx = source[1][0]+source[1][2]//2
+ cy = source[1][1]+source[1][3]*6//7
+ for y in range(source[1][1],source[1][1]+source[1][3]):
+ for x in range(source[1][0],source[1][0]+source[1][2]):
+ rgb = data[y*3*w+3*x:y*3*w+3*x+3]
+ if rgb == '\x01\x01\x01':
+ d = (x-cx)*(x-cx)+(y-cy)*(y-cy)*6
+ if d > 255: d = 255
+ rgb = chr(d)*3
+ img.write(rgb)
+ for y in range(y+1, y+1+extralines):
+ for x in range(source[1][0],source[1][0]+source[1][2]):
+ d = (x-cx)*(x-cx)+(y-cy)*(y-cy)*6
+ if d > 255: d = 255
+ rgb = chr(d)*3
+ img.write(rgb)
+ img.close()
+ return html_tag(name, alt)
+
+def html_tag(name, alt):
+ url = 'images/%s.png' % (name,)
+ f = open('doc/'+url, 'rb')
+ alldata = f.read()
+ f.close()
+ url = 'data:image/png;base64,' + alldata.encode('base64').replace('\n','')
+ return '<IMG SRC="%s" ALT="%s">' % (url, alt)
+
+
+def split_name(name):
+ "Split a name into its words based on capitalisation."
+ words = []
+ word = ''
+ for c in name:
+ if c.isupper() and word != '':
+ words.append(word)
+ word = c
+ else:
+ word += c
+ words.append(word)
+ return words
+
+dfile = open('doc/bonuses.html','w')
+print >> dfile, """<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<HTML>
+ <HEAD>
+ <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
+ <META NAME="Author" CONTENT="The Bub's Brothers">
+ <TITLE>The Bub's Brothers Bonuses</TITLE>
+ </HEAD>
+ <BODY bgcolor=white text=black>
+ <TABLE cellspacing=0 border=0>
+ <TR>
+ <TD width=132 align=right>
+ </TD>
+ <TD width=20>
+ </TD>
+ <TD bgcolor="#80FF80" align=center>
+ <I>regular bonus</I>
+ </TD>
+ <TD bgcolor="#80FF80" width=20>
+ </TD>
+ <TD bgcolor="#80FF80" align=center>
+ <I>big bonus</I>
+ </TD>
+ </TR>
+"""
+#" A stupid comment to stop emacs from mis-fontifying.
+
+class Potion4:
+ "Subgame! For a while, let's play one of the seven mini-games."
+ nimage = 'potion4'
+
+# Some classes exists in more than one example just to increase their
+# probability. Removes the duplicate with the help of this dict.
+processed = {}
+
+for bonus in bonuses.Classes:
+ if processed.has_key(bonus):
+ continue
+ name = split_name(bonus.__name__)
+ name.reverse()
+ processed[bonus] = string.join(name)
+ if bonus is bonuses.Potion:
+ processed[Potion4] = string.join(name) + '.'
+
+def sorter(a,b):
+ if a[1] == b[1]:
+ return 0
+ elif a[1] < b[1]:
+ return -1
+ else:
+ return 1
+
+sorted_classes = processed.items()
+sorted_classes.sort(sorter)
+
+for clasindex, clas in enumerate(sorted_classes):
+ bonus = clas[0]
+ images = ''
+ name = bonus.__name__
+ if bonus.__dict__.has_key('nimages'):
+ # A multi image entry.
+ i = 0
+ l = len(bonus.nimages)
+ for image in bonus.nimages:
+ if image == 'potion4':
+ continue
+ images += create_image(name+`i`, sprmap[image], alt=name)
+ i += 1
+ if (l-3*(i/3) >= 3) and (i % 3) == 0:
+ images += '<br>'
+ elif bonus.__dict__.has_key('nimage'):
+ images = create_image(name, sprmap[bonus.nimage], alt=name)
+ doc = bonus.__doc__
+ if doc == None:
+ doc = ''
+ bigdoc = getattr(bonus, 'bigdoc', None) or ''
+ if hasattr(bonus, 'bigbonus'):
+ assert bigdoc, "missing 'bigdoc' on %r" % (bonus,)
+ if clasindex % 2 == 1:
+ bgcolor = '"#E0FFE0"'
+ else:
+ bgcolor = 'white'
+ print >> dfile, '<TR><TD width=132 align=right>',
+ print >> dfile, images,
+ print >> dfile, '</TD><TD width=20></TD>',
+ print >> dfile, '<TD bgcolor=%s>%s</TD>' % (bgcolor, doc),
+ print >> dfile, '</TD><TD width=20 bgcolor=%s></TD>' % bgcolor,
+ print >> dfile, '<TD bgcolor=%s>%s</TD></TR>' % (bgcolor, bigdoc)
+
+print >> dfile, """ </TABLE>
+ </BODY>
+</HTML>
+"""
+
+dfile.close()
diff --git a/bubbob/doc/images/.cvsignore b/bubbob/doc/images/.cvsignore
new file mode 100644
index 0000000..e33609d
--- /dev/null
+++ b/bubbob/doc/images/.cvsignore
@@ -0,0 +1 @@
+*.png
diff --git a/bubbob/ext1/.cvsignore b/bubbob/ext1/.cvsignore
new file mode 100644
index 0000000..553d0ec
--- /dev/null
+++ b/bubbob/ext1/.cvsignore
@@ -0,0 +1,2 @@
+*.py[co]
+image1-[2-9].ppm
diff --git a/bubbob/ext1/__init__.py b/bubbob/ext1/__init__.py
new file mode 100644
index 0000000..9f90b0c
--- /dev/null
+++ b/bubbob/ext1/__init__.py
@@ -0,0 +1,403 @@
+from __future__ import generators
+import os, math, random
+import images, gamesrv
+from images import ActiveSprite
+from boards import CELL, HALFCELL
+from mnstrmap import GreenAndBlue
+from bubbles import BubblingEyes, Bubble
+from bonuses import Bonus
+
+LocalDir = os.path.basename(os.path.dirname(__file__))
+
+
+localmap = {
+ 'ark-paddle': ('image1-%d.ppm', (0, 0, 80, 32)),
+ }
+
+music = gamesrv.getmusic(os.path.join(LocalDir, 'music.wav'))
+snd_wall = gamesrv.getsample(os.path.join(LocalDir, 'wall.wav'))
+snd_brick = gamesrv.getsample(os.path.join(LocalDir, 'brick.wav'))
+
+
+def aget(x, y):
+ if 0 <= x < curboard.width and y >= 0:
+ if y >= curboard.height:
+ return ' '
+ return curboard.walls[y][x]
+ else:
+ return '#'
+
+def sign(x):
+ if x >= 0.0:
+ return 1
+ else:
+ return -1
+
+
+class PaddleEyes(BubblingEyes):
+
+ def __init__(self, bubber, saved_caps, paddle):
+ BubblingEyes.__init__(self, bubber, saved_caps, paddle)
+ self.deltax = (paddle.ico.w - self.ico.w) // 2
+ self.deltay = (paddle.ico.h - self.ico.h) // 2
+ self.step(self.deltax, self.deltay)
+
+ def playing_bubble(self, paddle, accel=0.75, vmax=4.5):
+ import boards
+ dx = self.deltax
+ dy = self.deltay
+ bubber = paddle.bubber
+ vx = 0.0
+ fx = paddle.x
+ while paddle.alive:
+ wannago = bubber.wannago(self.dcap)
+ if paddle.timeleft is None:
+ keydy = 0
+ else:
+ keydy = -1
+ key = ('eyes', wannago, keydy)
+ if fx < 2*CELL:
+ if vx < 0.0:
+ vx = -vx * 0.45
+ wannago = 1
+ elif fx + paddle.ico.w > boards.bwidth - 2*CELL:
+ if vx > 0.0:
+ vx = -vx * 0.45
+ wannago = -1
+ if not wannago:
+ if -accel <= vx <= accel:
+ vx = 0
+ elif vx < 0.0:
+ wannago = 0.7
+ else:
+ wannago = -0.7
+ vx += accel * wannago
+ if vx < -vmax:
+ vx = -vmax
+ elif vx > vmax:
+ vx = vmax
+ fx += vx
+ paddle.move(int(fx), paddle.y)
+ self.move(paddle.x+dx, paddle.y+dy, images.sprget(key))
+ yield None
+ self.kill()
+
+ def bottom_up(self):
+ return 0
+
+
+class Paddle(ActiveSprite):
+
+ def __init__(self, arkanoid, bubber, px, py):
+ ico = images.sprget(('ark-paddle', bubber.pn))
+ ActiveSprite.__init__(self, ico, px - (ico.w-2*CELL)//2,
+ py - (ico.h-2*CELL)//2)
+ self.arkanoid = arkanoid
+ self.bubber = bubber
+ self.timeleft = None
+ self.gen.append(self.bounce_down())
+ self.gen.append(self.bkgndstuff())
+ self.arkanoid.paddles.append(self)
+
+ def bounce_down(self):
+ import boards
+ target_y = boards.bheight - self.ico.h
+ fy = self.y
+ vy = 0.0
+ while fy < target_y or abs(vy) > 0.3:
+ if fy < target_y:
+ vy += 0.3
+ elif vy > 0.0:
+ vy = -vy / 3.0
+ fy += vy
+ self.move(self.x, int(fy))
+ yield None
+ while self.y > target_y:
+ self.step(0, -2)
+ yield None
+ self.move(self.x, target_y)
+ self.gen.append(self.wait_and_shoot())
+
+ def wait_and_shoot(self):
+ timeout = 30
+ while timeout > 0:
+ timeout -= self.arkanoid.ready
+ yield None
+ self.gen.append(self.catch(Ball(self)))
+
+ def catch(self, ball):
+ import boards
+ while ball.alive:
+ if ball.y > boards.bheight//2+1 and ball.vy > 0.0:
+ deltay = self.y - Ball.Y_MARGIN - ball.y
+ self.timeleft = deltay / ball.vy
+ #if -1.25 <= self.timeleft <= 0.5:
+ if -12 <= deltay <= 1:
+ ball.bouncepad(self.arkanoid.paddles)
+ yield None
+ self.timeleft = None
+ if ball.missed:
+ self.kill()
+
+ def kill(self):
+ images.Snd.Pop.play(1.0, pad=0.0)
+ images.Snd.Pop.play(1.0, pad=1.0)
+ ico = images.sprget(Bubble.exploding_bubbles[0])
+ for i in range(11):
+ s = ActiveSprite(ico,
+ self.x + random.randrange(self.ico.w) - CELL,
+ self.y + random.randrange(self.ico.h) - CELL)
+ s.gen.append(s.die(Bubble.exploding_bubbles))
+ try:
+ self.arkanoid.paddles.remove(self)
+ except ValueError:
+ pass
+ ActiveSprite.kill(self)
+
+ def bkgndstuff(self):
+ while 1:
+ if self.timeleft is not None:
+ self.arkanoid.order.append((self.timeleft, self))
+ yield None
+ touching = images.touching(self.x+1, self.y+1,
+ self.ico.w-2, self.ico.h-2)
+ touching.reverse()
+ for s in touching:
+ if isinstance(s, Bonus):
+ s.touched(self)
+
+ def score(self, hits):
+ bricks = self.arkanoid.bricks
+ bricks[self.bubber] = bricks.get(self.bubber, 0) + hits
+ self.bubber.givepoints(125*(2**hits))
+
+
+class Ball(ActiveSprite):
+
+ Y_MARGIN = 20
+ SPEED = 5.8
+
+ def __init__(self, paddle):
+ self.paddle = paddle
+ imglist1 = GreenAndBlue.new_bubbles[paddle.bubber.pn]
+ ActiveSprite.__init__(self, images.sprget(imglist1[0]),
+ paddle.x + paddle.ico.w//2,
+ paddle.y - Ball.Y_MARGIN)
+ self.missed = 0
+ self.setimages(self.imgseq(imglist1[1:], 6))
+ self.bounceangle(0.2)
+ self.gen.append(self.flying())
+
+ def bouncepad(self, paddles):
+ for paddle in paddles:
+ dx = (self.x + self.ico.w//2) - (paddle.x + paddle.ico.w//2)
+ dxmax = paddle.ico.w//2
+ angle = float(dx) / dxmax
+ if 0.0 <= angle <= 1.0:
+ angle = angle * 1.111 + 0.07
+ elif -1.0 <= angle <= 0.0:
+ angle = angle * 1.111 - 0.07
+ else:
+ continue
+ self.bounceangle(angle)
+ self.play(snd_wall)
+ break
+
+ def bounceangle(self, angle):
+ self.vx = math.sin(angle) * self.SPEED
+ self.vy = - math.cos(angle) * self.SPEED
+
+ def flying(self):
+ import boards
+ fx = self.x
+ fy = self.y
+ while self.y < boards.bheight:
+ fx += self.vx
+ fy += self.vy
+ self.move(int(fx), int(fy))
+ yield None
+ cx = self.x // CELL + 1
+ cy = self.y // CELL + 1
+ dx = sign(self.vx)
+ dy = sign(self.vy)
+ hits = 0.0
+ if aget(cx, cy) == '#':
+ hits += self.ahit(cx, cy, 0, 0)
+ if aget(cx+dx, cy) == '#':
+ hits += self.ahit(cx+dx, cy, 0, dy)
+ self.vx = -self.vx
+ if aget(cx, cy+dy) == '#':
+ hits += self.ahit(cx, cy+dy, dx, 0)
+ self.vy = -self.vy
+ if hits:
+ hits = int(hits)
+ if hits:
+ self.paddle.score(hits)
+ self.play(snd_brick)
+ else:
+ self.play(snd_wall)
+ self.missed = 1
+ self.kill()
+
+ def ahit(self, cx, cy, dx, dy):
+ total = 0.01
+ for i in (-1, 0, 1):
+ x = cx + i*dx
+ y = cy + i*dy
+ if (2 <= x < curboard.width - 2 and 0 <= y < curboard.height and
+ aget(x, y) == '#'):
+ curboard.killwall(x, y)
+ self.paddle.arkanoid.killedbricks += 1
+ total += 1.0
+ return total
+
+ def pop(self):
+ self.play(images.Snd.Pop)
+ self.gen = [self.die(Bubble.exploding_bubbles)]
+
+
+class Arkanoid:
+
+ def bgen(self, limittime = 60.1): # 0:60
+ import boards
+ from player import BubPlayer
+
+ self.bricks = {}
+ for t in boards.initsubgame(music, self.displaypoints):
+ yield t
+
+ tc = boards.TimeCounter(limittime)
+ self.ready = 0
+ self.builddelay = {}
+ self.nbbricks = 0
+ self.order = []
+ self.paddles = []
+ #finish = 0
+ for t in self.frame():
+ self.order_paddles()
+ t = boards.normal_frame()
+ self.build_paddles()
+ yield t
+ #if len(self.paddles) == 0:
+ # finish += 1
+ # if finish == 20:
+ # break
+ #else:
+ # finish = 0
+ tc.update(t)
+ if tc.time == 0.0:
+ break
+ if (BubPlayer.FrameCounter & 15) == 7:
+ for s in images.ActiveSprites:
+ if isinstance(s, Bonus):
+ s.timeout = 0 # bonuses stay
+ elif isinstance(s, Bubble):
+ s.pop()
+
+ tc.restore()
+ self.ready = 0
+ for s in images.ActiveSprites[:]:
+ if isinstance(s, Ball):
+ s.pop()
+ for t in boards.result_ranking(self.bricks, self.nbbricks):
+ self.build_paddles()
+ yield t
+ self.remove_paddles()
+ self.unframe()
+
+ def displaypoints(self, bubber):
+ return self.bricks.get(bubber, 0)
+
+ def frame(self):
+ for y in range(curboard.height-1, curboard.height//2, -1):
+ yield None
+ yield None
+ for x in range(2, curboard.width-2):
+ if aget(x, y) == '#':
+ curboard.killwall(x, y)
+ brickline = curboard.width-4
+ expected = (brickline * curboard.height) // 5
+ y = curboard.height//2
+ nbbricks = 0
+ while y>=0 and nbbricks + (y+1)*brickline >= expected:
+ yield None
+ for x in range(2, curboard.width-2):
+ if aget(x, y) == '#':
+ nbbricks += 1
+ y -= 1
+ while y >= -1:
+ yield None
+ yield None
+ for x in range(2, curboard.width-2):
+ if y < 0 or aget(x, y) == ' ':
+ curboard.putwall(x, y)
+ nbbricks += brickline
+ curboard.reorder_walls()
+ y -= 1
+
+ nbbricks -= brickline
+ self.ready = 1
+ self.nbbricks = nbbricks
+ self.killedbricks = 0
+ while self.killedbricks < self.nbbricks:
+ yield None
+
+ def unframe(self):
+ for x in range(2, curboard.width-2):
+ curboard.killwall(x, -1)
+
+ def build_paddles(self):
+ from player import BubPlayer
+ for p in BubPlayer.PlayerList:
+ dragons = [d for d in p.dragons if not isinstance(d, PaddleEyes)]
+ if dragons and len(p.dragons) == len(dragons):
+ if self.builddelay.get(p):
+ self.builddelay[p] -= 1
+ else:
+ self.builddelay[p] = 53
+ dragon = random.choice(dragons)
+ paddle = Paddle(self, p, dragon.x, dragon.y)
+ eyes = PaddleEyes(p, dragon.dcap, paddle)
+ p.dragons.append(eyes)
+ p.emotic(dragon, 4)
+ for d in dragons:
+ d.kill()
+
+ def order_paddles(self):
+ self.order.sort()
+ self.order.reverse()
+ for timeleft, paddle in self.order:
+ try:
+ self.paddles.remove(paddle)
+ except ValueError:
+ pass
+ else:
+ self.paddles.insert(0, paddle)
+ paddle.to_front()
+ del self.order[:]
+
+ def remove_paddles(self):
+ killclasses = (Paddle, PaddleEyes, Ball, Bonus)
+ for s in images.ActiveSprites[:]:
+ if isinstance(s, killclasses):
+ s.kill()
+
+# This game is suitable for at least min_players players
+min_players = 1
+
+def run():
+ global curboard
+ import boards
+ from boards import curboard
+ boards.replace_boardgen(Arkanoid().bgen())
+
+def setup():
+ from player import BubPlayer
+ for key, (filename, rect) in localmap.items():
+ filename = os.path.join(LocalDir, filename)
+ if filename.find('%d') >= 0:
+ for p in BubPlayer.PlayerList:
+ images.sprmap[key, p.pn] = (filename % p.pn, rect)
+ else:
+ images.sprmap[key] = (filename, rect)
+setup()
diff --git a/bubbob/ext1/brick.wav b/bubbob/ext1/brick.wav
new file mode 100644
index 0000000..5f62625
--- /dev/null
+++ b/bubbob/ext1/brick.wav
Binary files differ
diff --git a/bubbob/ext1/image1-0.ppm b/bubbob/ext1/image1-0.ppm
new file mode 100644
index 0000000..03d6b3a
--- /dev/null
+++ b/bubbob/ext1/image1-0.ppm
Binary files differ
diff --git a/bubbob/ext1/music.wav b/bubbob/ext1/music.wav
new file mode 100644
index 0000000..261b9cf
--- /dev/null
+++ b/bubbob/ext1/music.wav
Binary files differ
diff --git a/bubbob/ext1/wall.wav b/bubbob/ext1/wall.wav
new file mode 100644
index 0000000..f93df15
--- /dev/null
+++ b/bubbob/ext1/wall.wav
Binary files differ
diff --git a/bubbob/ext2/.cvsignore b/bubbob/ext2/.cvsignore
new file mode 100644
index 0000000..539da74
--- /dev/null
+++ b/bubbob/ext2/.cvsignore
@@ -0,0 +1 @@
+*.py[co]
diff --git a/bubbob/ext2/__init__.py b/bubbob/ext2/__init__.py
new file mode 100644
index 0000000..81cd833
--- /dev/null
+++ b/bubbob/ext2/__init__.py
@@ -0,0 +1,578 @@
+from __future__ import generators
+import os, math, random
+import images, gamesrv
+from images import ActiveSprite
+from boards import CELL, HALFCELL, bget
+from mnstrmap import GreenAndBlue, Ghost
+from bonuses import Bonus
+from bubbles import Bubble
+
+LocalDir = os.path.basename(os.path.dirname(__file__))
+
+
+localmap = {
+ ('pac-lg', -1,0) : ('image1.ppm', ( 0, 0, 32, 32)),
+ ('pac-sm', -1,0) : ('image1.ppm', (32, 0, 32, 32)),
+ ('pac-lg', 0,-1) : ('image1.ppm', ( 0, 32, 32, 32)),
+ ('pac-sm', 0,-1) : ('image1.ppm', (32, 32, 32, 32)),
+ ('pac-lg', 1,0) : ('image1.ppm', ( 0, 64, 32, 32)),
+ ('pac-sm', 1,0) : ('image1.ppm', (32, 64, 32, 32)),
+ ('pac-lg', 0,1) : ('image1.ppm', ( 0, 96, 32, 32)),
+ ('pac-sm', 0,1) : ('image1.ppm', (32, 96, 32, 32)),
+ 'pac-black' : ('image2.ppm', ( 0, 0, 32, 32)),
+ 'pac-dot' : ('image2.ppm', ( 0, 32, 8, 8)),
+ }
+
+music = gamesrv.getmusic(os.path.join(LocalDir, 'music.wav'))
+
+
+class PacSprite(ActiveSprite):
+
+ def __init__(self, ico, x, y):
+ import boards
+ if y < -2*CELL:
+ y = -2*CELL
+ elif y > boards.bheight:
+ y = boards.bheight
+ x = (x+HALFCELL) & -CELL
+ y = (y+HALFCELL) & -CELL
+ ActiveSprite.__init__(self, ico, x, y)
+ self.wannadx = self.wannady = 0
+
+ def moving(self):
+ import boards
+ dx = dy = 0
+ turned = None
+ was_clear = 0
+ while 1:
+ if dx or dy:
+ frontx = self.x+CELL+dx*(CELL+1)
+ fronty = self.y+CELL+dy*(CELL+1)
+ clear = (bget((frontx+dy)//CELL, (fronty-dx)//CELL) == ' ' and
+ bget((frontx-dy)//CELL, (fronty+dx)//CELL) == ' ')
+ if clear:
+ blocked = 0
+ else:
+ blocked = (was_clear or (self.x<=2*CELL and dx<0) or
+ (self.x>=boards.bwidth-4*CELL and dx>0))
+ else:
+ blocked = 1
+ if blocked:
+ if turned:
+ dx, dy = turned
+ turned = None
+ continue
+ self.lastmove = None
+ else:
+ if turned:
+ self.resetimages(dx, dy)
+ turned = None
+ self.lastmove = dx, dy
+ self.step(2*dx, 2*dy)
+ self.vertical_warp()
+ was_clear = clear
+ yield None
+ if self.wannadx != dx or self.wannady != dy:
+ if ((self.wannadx and not (self.y % CELL)) or
+ (self.wannady and not (self.x % CELL))):
+ turned = dx, dy
+ dx = self.wannadx
+ dy = self.wannady
+
+
+class Pac(PacSprite):
+ no_hat = 1
+
+ def __init__(self, pacman, bubber, x, y, dcap):
+ ico = GreenAndBlue.normal_bubbles[bubber.pn][1]
+ PacSprite.__init__(self, images.sprget(('eyes', 0, 0)), x, y)
+ self.bubble = ActiveSprite(images.sprget(ico), x, y)
+ self.bubber = bubber
+ self.pacman = pacman
+ self.ready = 0
+ self.gen.append(self.playing())
+ self.pacman.pacs.append(self)
+ self.dcap = dcap
+
+ def resetimages(self, dx, dy):
+ self.ready = 1
+ self.setimages(self.cyclic([('pac-lg', dx, dy),
+ 'pac-black',
+ ('pac-sm', dx, dy)], 5))
+
+ def to_front(self):
+ self.bubble.to_front()
+ ActiveSprite.to_front(self)
+
+ def kill(self):
+ self.play(images.Snd.Pop)
+ self.bubble.gen = [self.bubble.die(Bubble.exploding_bubbles)]
+ self.pacman.latestposition[self.bubber] = self.x, self.y
+ try:
+ self.bubber.dragons.remove(self)
+ except ValueError:
+ pass
+ try:
+ self.pacman.pacs.remove(self)
+ except ValueError:
+ pass
+ ActiveSprite.kill(self)
+
+ def playing(self):
+ bubber = self.bubber
+ for t in self.moving():
+ if self.pacman.ready:
+ d = [(bubber.key_left, -1, 0),
+ (bubber.key_right, 1, 0),
+ (bubber.key_jump, 0,-1),
+ (bubber.key_fire, 0, 1)]
+ d.sort()
+ if d[-1][0] > d[-2][0]:
+ self.wannadx, self.wannady = d[-1][1:]
+
+ self.bubble.move(self.x, self.y)
+ yield None
+
+ if self.ready:
+ touching = images.touching(self.x+CELL-3, self.y+CELL-3, 6, 6)
+ touching.reverse()
+ for s in touching:
+ if isinstance(s, Bonus):
+ s.touched(self)
+ elif isinstance(s, PacGhost):
+ self.kill()
+ return
+
+
+class PacGhost(PacSprite):
+
+ def __init__(self, pacman, x, y):
+ left = random.random() < 0.5
+ if left:
+ ico = Ghost.left[0]
+ else:
+ ico = Ghost.right[0]
+ PacSprite.__init__(self, images.sprget(ico), x, y)
+ self.pacman = pacman
+ self.gen.append(self.waiting())
+ if left:
+ self.wannadx = -1
+ else:
+ self.wannadx = 1
+ self.resetimages(self.wannadx, self.wannady)
+
+ def resetimages(self, dx, dy):
+ if dx > 0:
+ self.setimages(self.cyclic(Ghost.right, 3))
+ elif dx < 0:
+ self.setimages(self.cyclic(Ghost.left, 3))
+ #else: don't change image
+
+ def waiting(self, delay=45):
+ while not self.pacman.ready:
+ yield None
+ for i in range(delay):
+ yield None
+ self.gen.append(self.walking())
+
+ def walking(self):
+ round = 0
+ self.touchable = 1
+ lastmove_x = 0
+ for t in self.moving():
+ if random.random() < 0.1:
+ lastmove_x = self.wannadx
+ if self.lastmove is None and random.random() < 0.75:
+ round = 0 # try to move again immediately
+ if self.lastmove is None or random.random() < 0.01:
+ dragons = self.pacman.pacs or images.ActiveSprites
+ distances = [(abs(dragon.x-self.x)+abs(dragon.y-self.y),
+ dragon) for dragon in dragons]
+ distance, dragon = min(distances)
+ if lastmove_x:
+ self.wannadx = 0
+ if (dragon.y < self.y) ^ (random.random() < 0.3):
+ self.wannady = -1
+ else:
+ self.wannady = 1
+ else:
+ self.wannady = 0
+ if (dragon.x < self.x) ^ (random.random() < 0.3):
+ self.wannadx = -1
+ else:
+ self.wannadx = 1
+ else:
+ lastmove_x = self.lastmove[0]
+## for i in range(10):
+## dragon = random.choice(dragons)
+## dx = dragon.x - self.x
+## dy = dragon.y - self.y
+## if dx or dy:
+## dist = math.sqrt(dx*dx+dy*dy)
+## dx /= dist
+## dy /= dist
+## wx, wy = random.choice([(-1,0), (1,0), (0,-1), (0,1)])
+## ex = wx-dx
+## ey = wy-dy
+## if ex*ex + ey*ey < random.random()*3.14:
+## break
+## self.wannadx = wx
+## self.wannady = wy
+ if round == 0: # go just a bit faster than the players
+ round = 6
+ else:
+ round -= 1
+ yield None
+
+
+class FruitBonus(Bonus):
+ pass
+
+
+class Pacman:
+
+ def bgen(self, limittime = 45.1): # 0:45
+ import boards
+ from player import BubPlayer
+
+ self.ready = 0
+ self.dots = []
+ monsters = BubPlayer.MonsterList[:]
+ random.shuffle(monsters)
+ keep = len([p for p in BubPlayer.PlayerList if p.isplaying()])
+ monsters = monsters[:2 + keep//2]
+ for d in monsters:
+ PacGhost(self, d.x, d.y)
+
+ for t in boards.initsubgame(music, self.displaypoints):
+ yield t
+
+ tc = boards.TimeCounter(limittime)
+ self.builddelay = {}
+ self.latestposition = {}
+ self.pacs = []
+ #finish = 0
+ for t in self.frame():
+ t = boards.normal_frame()
+ self.build_pacs()
+ yield t
+ #if len(self.pacs) == 0:
+ # finish += 1
+ # if finish == 20:
+ # break
+ #else:
+ # finish = 0
+ tc.update(t)
+ if tc.time == 0.0:
+ break
+ if (BubPlayer.FrameCounter & 15) == 7:
+ for s in images.ActiveSprites:
+ if isinstance(s, Bubble):
+ s.pop()
+
+ tc.restore()
+ self.ready = 0
+ results = {}
+ for b in self.dots:
+ for d in b.taken_by:
+ bubber = d.bubber
+ results[bubber] = results.get(bubber, 0) + 1
+ for t in boards.result_ranking(results, len(self.dots)):
+ self.remove_pacs()
+ yield t
+ for s in images.ActiveSprites[:]:
+ if isinstance(s, Bonus):
+ s.kill()
+
+ def displaypoints(self, bubber):
+ result = 0
+ for b in self.dots:
+ for d in b.taken_by:
+ if d.bubber is bubber:
+ result += 1
+ return result
+
+ def frame(self):
+ import boards
+ from bonuses import Fruits
+ for t in self.digwalls():
+ yield t
+
+ def anywall(x1,y1,x2,y2):
+ for tx in range(x1, x2):
+ for ty in range(y1, y2):
+ if bget(tx, ty) == '#':
+ return 1
+ return 0
+
+ def give_one_point(dragon):
+ dragon.bubber.displaypoints += 1
+ scoreboard()
+
+ dots = self.dots
+ ico = images.sprget('pac-dot')
+ for x in range(boards.width):
+ for y in range(boards.height):
+ if not anywall(x, y, x+2, y+2):
+ if anywall(x-1, y-1, x+3, y+3):
+ b = Bonus((x+1)*CELL - ico.w//2,
+ (y+1)*CELL - ico.h//2,
+ 'pac-dot', points=-100, falling=0)
+ b.sound = 'Extra'
+ b.timeout = 0
+ b.taken = give_one_point
+ dots.append(b)
+
+ for s in images.ActiveSprites:
+ if isinstance(s, PacGhost):
+ s.to_front()
+ yield None
+
+ self.ready = 1
+
+ for i in range(len([s for s in images.ActiveSprites
+ if isinstance(s, PacGhost)])):
+ for j in range(32):
+ yield None
+ for j in range(100):
+ x = random.randrange(4, boards.width-6)
+ y = random.randrange(3, boards.height-5)
+ if not anywall(x-2, y-2, x+4, y+4):
+ nimage, points = random.choice(Fruits.Fruits)
+ points += 650 # boost to the range 750-1000
+ b = FruitBonus(x*CELL, y*CELL,
+ nimage, points, falling=0)
+ b.timeout = 0
+ break
+
+ while dots:
+ dots = [b for b in dots if b.alive]
+ yield None
+
+ def build_pacs(self):
+ from player import BubPlayer
+ for p in BubPlayer.PlayerList:
+ dragons = [d for d in p.dragons if not isinstance(d, Pac)]
+ if dragons and len(p.dragons) == len(dragons):
+ if self.builddelay.get(p):
+ self.builddelay[p] -= 1
+ else:
+ self.builddelay[p] = 109
+ dragon = random.choice(dragons)
+ if p in self.latestposition:
+ dragon.move(*self.latestposition[p])
+ pac = Pac(self, p, dragon.x, dragon.y, dragon.dcap)
+ p.dragons.append(pac)
+ p.emotic(dragon, 4)
+ for d in dragons:
+ d.kill()
+
+ def remove_pacs(self):
+ from player import Dragon
+ killclasses = (PacSprite, FruitBonus, Dragon)
+ for s in images.ActiveSprites[:]:
+ if isinstance(s, killclasses):
+ s.kill()
+
+ def digwalls(self):
+ import boards
+ holes = {}
+ for x in range(2, boards.width-2):
+ y = boards.height-1
+ if bget(x, 0) == '#' or bget(x, y) == '#':
+ if bget(x, 0) == ' ': curboard.putwall(x, 0)
+ if bget(x, y) == ' ': curboard.putwall(x, y)
+ curboard.reorder_walls()
+ for y in range(1, boards.height-1):
+ if bget(x, y) == ' ':
+ holes[x, y] = 0
+ if x % 7 == 0:
+ yield None
+
+ # 'holes' maps coordinates (x,y) to 0 (not processed)
+ # or 1 (processed).
+ # All processed holes are pacman-connected.
+
+ def rdig(x1,y1,x2,y2, reversed, holes=holes):
+ # digs the rectangle (x1,y1,x2,y2) and marks it as
+ # processed. Also recursively mark as processed all existing
+ # holes that are pacman-connected to the rectangle.
+ xrange = range(x1,x2)
+ yrange = range(y1,y2)
+ if not reversed:
+ xrange.reverse()
+ yrange.reverse()
+ if len(xrange) > len(yrange):
+ xylist = [(x, y) for x in xrange for y in yrange]
+ else:
+ xylist = [(x, y) for y in yrange for x in xrange]
+ t = 0
+ for x, y in xylist:
+ if bget(x, y) == '#':
+ curboard.killwall(x, y)
+ if t == 0:
+ yield None
+ t = 2
+ else:
+ t -= 1
+ holes.setdefault((x, y), 0)
+ fill = []
+ for x in range(x1,x2-1):
+ for y in range(y1,y2-1):
+ fill.append((x, y))
+ for x, y in fill:
+ if ((x, y) in holes and
+ (x, y+1) in holes and
+ (x+1, y) in holes and
+ (x+1, y+1) in holes):
+ if (holes[x, y] == 0 or
+ holes[x, y+1] == 0 or
+ holes[x+1, y] == 0 or
+ holes[x+1, y+1] == 0):
+
+ holes[x, y] = 1
+ holes[x, y+1] = 1
+ holes[x+1, y] = 1
+ holes[x+1, y+1] = 1
+ fill.append((x+1,y))
+ fill.append((x-1,y))
+ fill.append((x,y+1))
+ fill.append((x,y-1))
+
+ def joined(x1,y1,x2,y2, holes=holes, boards=boards):
+ # returns
+ # 1 if the rectangle (x1,y1,x2,y2) is pac-connected to
+ # some already-processed holes
+ # 0 if it is not
+ # -1 if (x1,y1,x2,y2) is out of the screen
+ if x1<2 or y1<1 or x2>boards.width-2 or y2>boards.height-1:
+ return -1
+ accum1 = accum2 = 0
+ for x in range(x1,x2):
+ if holes.get((x,y1-1)):
+ accum1 += 1
+ if accum1 == 2:
+ return 1
+ else:
+ accum1 = 0
+ if holes.get((x,y2)):
+ accum2 += 1
+ if accum2 == 2:
+ return 1
+ else:
+ accum2 = 0
+ accum1 = accum2 = 0
+ for y in range(y1,y2):
+ if holes.get((x1-1,y)):
+ accum1 += 1
+ if accum1 == 2:
+ return 1
+ else:
+ accum1 = 0
+ if holes.get((x2,y)):
+ accum2 += 1
+ if accum2 == 2:
+ return 1
+ else:
+ accum2 = 0
+ return 0
+
+ if not holes:
+ holes[boards.width//2, boards.height//2] = 0
+ holeslist = holes.keys()
+ random.shuffle(holeslist)
+ startx, starty = holeslist.pop()
+ # make the hole larger (2x2) towards the center of the board
+ if startx > boards.width//2:
+ startx -= 1
+ if starty > boards.height//2:
+ starty -= 1
+ # initial 2x2 hole
+ for t in rdig(startx, starty, startx+2, starty+2, 0):
+ yield t
+
+ dlist = [
+ (0,0,1,0, 0,-1), (0,0,1,0, 0,0), # right
+ (0,0,0,1, -1,0), (0,0,0,1, 0,0), # bottom
+ (-1,0,0,0, -1,-1), (-1,0,0,0, -1,0), # left
+ (0,-1,0,0, -1,-1), (0,-1,0,0, 0,-1), # top
+ ]
+ while holeslist:
+ random.shuffle(dlist)
+ pending = holeslist
+ holeslist = []
+ progress = 0
+ for x, y in pending:
+ if holes[x, y] != 0:
+ continue
+ for dx1, dy1, dx2, dy2, dx, dy in dlist:
+ x1 = x + dx
+ y1 = y + dy
+ x2 = x1 + 2
+ y2 = y1 + 2
+ result = 0
+ while result == 0:
+ result = joined(x1,y1,x2,y2)
+ if result == 1:
+ # rectangle (x1,y1,x2,y2) is good
+ for t in rdig(x1, y1, x2, y2,
+ dx1<0 or dy1<0 or dx2<0 or dy2<0):
+ yield t
+ progress = 1
+ break
+ x1 += dx1
+ y1 += dy1
+ x2 += dx2
+ y2 += dy2
+ else:
+ # rectangle (x1,y1,x2,y2) is too large for the screen
+ # failure
+ continue
+ break
+ else:
+ # no successful direction found from this point
+ holeslist.append((x, y)) # try again later
+ if not progress:
+ # deadlocked situation, add a new random hole
+ x = random.randrange(2, boards.width-2)
+ y = random.randrange(1, boards.height-1)
+ holeslist.insert(0, (x, y))
+ holes.setdefault((x, y), 0)
+ yield None
+
+ # pattern transformation:
+ # X. ..
+ # ... --> ...
+ # .X .X
+ progress = 1
+ while progress:
+ progress = 0
+ for y in range(1, boards.height-1):
+ for x in range(3, boards.width-3):
+ if (' ' == bget(x, y)
+ == bget(x+1, y)
+ == bget(x-1, y)
+ == bget(x, y+1)
+ == bget(x, y-1)):
+ if '#' == bget(x-1, y-1) == bget(x+1, y+1):
+ curboard.killwall(x-1, y-1)
+ progress = 1
+ elif '#' == bget(x+1, y-1) == bget(x-1, y+1):
+ curboard.killwall(x+1, y-1)
+ progress = 1
+ yield None
+
+# This game is suitable for at least min_players players
+min_players = 1
+
+def run():
+ global curboard
+ import boards
+ from boards import curboard
+ boards.replace_boardgen(Pacman().bgen())
+
+def setup():
+ for key, (filename, rect) in localmap.items():
+ filename = os.path.join(LocalDir, filename)
+ images.sprmap[key] = (filename, rect)
+setup()
diff --git a/bubbob/ext2/image1.ppm b/bubbob/ext2/image1.ppm
new file mode 100644
index 0000000..1daa864
--- /dev/null
+++ b/bubbob/ext2/image1.ppm
Binary files differ
diff --git a/bubbob/ext2/image2.ppm b/bubbob/ext2/image2.ppm
new file mode 100644
index 0000000..4ed7060
--- /dev/null
+++ b/bubbob/ext2/image2.ppm
Binary files differ
diff --git a/bubbob/ext2/music.wav b/bubbob/ext2/music.wav
new file mode 100644
index 0000000..921322e
--- /dev/null
+++ b/bubbob/ext2/music.wav
Binary files differ
diff --git a/bubbob/ext3/.cvsignore b/bubbob/ext3/.cvsignore
new file mode 100644
index 0000000..553d0ec
--- /dev/null
+++ b/bubbob/ext3/.cvsignore
@@ -0,0 +1,2 @@
+*.py[co]
+image1-[2-9].ppm
diff --git a/bubbob/ext3/__init__.py b/bubbob/ext3/__init__.py
new file mode 100644
index 0000000..f5408c1
--- /dev/null
+++ b/bubbob/ext3/__init__.py
@@ -0,0 +1,367 @@
+from __future__ import generators
+import os, math, random
+import images, gamesrv
+from images import ActiveSprite
+import boards
+from boards import CELL, HALFCELL, bget
+from mnstrmap import GreenAndBlue, Fire
+from bonuses import Bonus
+from player import Dragon, BubPlayer
+import monsters
+from bubbles import Bubble
+
+LocalDir = os.path.basename(os.path.dirname(__file__))
+
+localmap = {
+ 'gala': ('image1-%d.ppm', (0, 0, 32, 32)),
+ }
+
+music = gamesrv.getmusic(os.path.join(LocalDir, 'music.wav'))
+snd_shoot = gamesrv.getsample(os.path.join(LocalDir, 'shoot.wav'))
+
+
+class Ship(ActiveSprite):
+
+ def __init__(self, galaga, bubber, x, y):
+ ico = images.sprget(('gala', bubber.pn))
+ ActiveSprite.__init__(self, ico, x, y)
+ self.galaga = galaga
+ self.bubber = bubber
+ self.gen.append(self.movedown())
+ self.gen.append(self.playing_ship())
+ self.gen.append(self.doomed())
+ self.galaga.ships.append(self)
+
+ def movedown(self):
+ import boards
+ target_y = boards.bheight - self.ico.h
+ while self.y < target_y:
+ yield None
+ self.move(self.x, self.y + 3)
+ self.move(self.x, target_y)
+
+ def playing_ship(self):
+ import boards
+ bubber = self.bubber
+ xmin = HALFCELL
+ xmax = boards.bwidth - HALFCELL - self.ico.w
+ fire = 0
+ while 1:
+ wannago = bubber.wannago(self.dcap)
+ nx = self.x + 2*wannago
+ if nx < xmin:
+ nx = xmin
+ elif nx > xmax:
+ nx = xmax
+ self.move(nx, self.y)
+ if fire:
+ fire -= 1
+ elif bubber.key_fire:
+ self.firenow()
+ fire = 28
+ yield None
+
+ def firenow(self):
+ ico = images.sprget(GreenAndBlue.new_bubbles[self.bubber.pn][0])
+ s = Shot(ico, self.x, self.y)
+ s.d = self
+ s.gen = [s.straightup(self)]
+ self.play(snd_shoot)
+
+ def doomed(self):
+ dangerous = Alien, monsters.MonsterShot
+ while 1:
+ touching = images.touching(self.x+3, self.y+3, 26, 26)
+ for s in touching:
+ if isinstance(s, dangerous):
+ self.kill()
+ return
+ yield None
+ yield None
+
+ def kill(self):
+ try:
+ self.bubber.dragons.remove(self)
+ except ValueError:
+ pass
+ images.Snd.Pop.play(1.0, pad=0.0)
+ images.Snd.Pop.play(1.0, pad=1.0)
+ ico = images.sprget(Bubble.exploding_bubbles[0])
+ for i in range(11):
+ s = ActiveSprite(ico,
+ self.x + random.randrange(self.ico.w) - CELL,
+ self.y + random.randrange(self.ico.h) - CELL)
+ s.gen.append(s.die(Bubble.exploding_bubbles))
+ try:
+ self.galaga.ships.remove(self)
+ except ValueError:
+ pass
+ ActiveSprite.kill(self)
+
+
+class Shot(Bubble):
+ touchable = 0
+
+ def straightup(self, ship):
+ ymin = -self.ico.h
+ while self.y > ymin:
+ self.step(0, -10)
+ touching = images.touching(self.x+CELL-1, self.y+CELL-1, 2, 2)
+ touching = [s for s in touching if isinstance(s, Alien)]
+ if touching:
+ alien = random.choice(touching)
+ self.gen = []
+ self.touchable = 1
+ self.move(alien.x, alien.y)
+ self.pop([ship])
+ alien.kill()
+ scores = ship.galaga.scores
+ scores[ship.bubber] = scores.get(ship.bubber, 0) + 1
+ ship.bubber.givepoints(100)
+ return
+ yield None
+
+ def popped(self, dragon):
+ return 200
+
+
+class Alien(monsters.Monster):
+ ANGLES = 32
+ SPEED = 5
+ ANGLE_TABLE = [(SPEED * math.cos(a*2.0*math.pi/ANGLES),
+ -SPEED * math.sin(a*2.0*math.pi/ANGLES))
+ for a in range(ANGLES)]
+ touchable = 0
+
+ def __init__(self, galaga, squadron, rank, relativey):
+ centerx = boards.bwidth // 2
+ go_left = squadron % 2
+ dx = (1,-1)[go_left]
+ halfspan = centerx*7//12
+ relativex = - halfspan + 4*CELL*rank
+ if relativex > halfspan:
+ raise StopIteration
+
+ if squadron % 3 == 2:
+ from mnstrmap import Ghosty as mcls
+ else:
+ from mnstrmap import Flappy as mcls
+ mdef = mcls(centerx // CELL - 1, -7, go_left)
+ mdef.left_weapon = mdef.right_weapon = [Fire.drop]
+ monsters.Monster.__init__(self, mdef)
+
+ self.path = [(None, centerx + (dx*centerx)*2//3, boards.bheight//3),
+ (None, centerx - (dx*centerx)*4//5, boards.bheight//6),
+ (galaga, -dx*relativex, -relativey)]
+ self.gen = [self.waiting(rank * 20)]
+ self.in_place = 0
+ galaga.nbmonsters += 1
+
+ def default_mode(self, angle=ANGLES//4):
+ self.touchable = 1
+ speed = self.SPEED
+ relative, tx, ty = self.path[0]
+ fx = self.x
+ fy = self.y
+ ymax = boards.bheight - self.ico.h
+ cont = 1
+ if relative:
+ shoot_prob = 0.0085
+ else:
+ shoot_prob = 0.021
+ while cont:
+ if self.angry:
+ self.kill() # never getting out of a bubble
+ return
+ if relative:
+ dx = relative.globalx + tx
+ dy = relative.globaly + ty
+ else:
+ dx = tx
+ dy = ty
+ dx -= self.x
+ dy -= self.y
+
+ tests = []
+ for a1 in (-1, 0, 1):
+ a1 = (angle+a1) % self.ANGLES
+ testx, testy = self.ANGLE_TABLE[a1]
+ testx -= dx
+ testy -= dy
+ tests.append((testx*testx+testy*testy, a1))
+ ignored, angle = min(tests)
+ if dx*dx+dy*dy > speed*speed:
+ dx, dy = self.ANGLE_TABLE[angle]
+ elif relative:
+ self.in_place = 1
+ if self.y > ymax and relative.ships:
+ for ship in relative.ships[:]:
+ ship.kill()
+ relative.builddelay[ship.bubber] = 9999
+ relative.gameover = 1
+ #x0 = self.x//CELL + 1
+ #if x0 < 2: x0 = 0
+ #if x0 >= boards.width-2: x0 = boards.width-3
+ #bubbles.FireFlame(x0, boards.height-2, None, [-1, 1],
+ # boards.width)
+ else:
+ self.path.pop(0)
+ self.gen.append(self.default_mode(angle))
+ cont = 0
+ fx += dx
+ fy += dy
+ self.move(int(fx), int(fy))
+ if dx and (self.dir > 0) != (dx > 0):
+ self.dir = -self.dir
+ self.resetimages()
+ if random.random() < shoot_prob and self.y >= 0:
+ monsters.DownShot(self)
+ yield None
+
+
+class Galaga:
+ gameover = 0
+
+ def bgen(self):
+ self.scores = {}
+ for t in boards.initsubgame(music, self.displaypoints):
+ yield t
+
+ self.ships = []
+ self.builddelay = {}
+ self.nbmonsters = 0
+ #finish = 0
+ for t in self.frame():
+ t = boards.normal_frame()
+ self.build_ships()
+ yield t
+ #if len(self.ships) == 0:
+ # finish += 1
+ # if finish == 50:
+ # break
+ #else:
+ # finish = 0
+ if (BubPlayer.FrameCounter & 15) == 7:
+ for s in images.ActiveSprites:
+ if isinstance(s, Bubble) and not isinstance(s, Shot):
+ s.pop()
+
+ for t in boards.result_ranking(self.scores, self.nbmonsters):
+ self.build_ships()
+ yield t
+ for s in images.ActiveSprites[:]:
+ if isinstance(s, (Alien, Ship)):
+ s.kill()
+
+ def displaypoints(self, bubber):
+ return self.scores.get(bubber, 0)
+
+ def frame(self):
+ curboard.walls_by_pos.clear()
+ curboard.winds = ['v' * curboard.width] * curboard.height
+ for y in range(len(curboard.walls)):
+ curboard.walls[y] = ' ' * len(curboard.walls[y])
+ l1 = curboard.sprites['walls']
+ l2 = curboard.sprites['borderwalls']
+ while l1 or l2:
+ for l in [l1, l2]:
+ for w in l[:]:
+ w.step(0, 5)
+ if w.y >= boards.bheight:
+ l.remove(w)
+ w.kill()
+ yield None
+
+ self.globalx = boards.bwidth // 2
+ self.globaly = 0
+ shifter = self.shifter()
+ squadrons = len([p for p in BubPlayer.PlayerList if p.isplaying()])
+ squadrons = 3 + (squadrons+1)//3
+ nextsquad = 0
+ relativey = 0
+ squadtime = 0
+ while not self.gameover:
+ yield None
+ #if random.random() < 0.015:
+ # bubbles.sendbubble(bubbles.PlainBubble, top=0)
+ in_place = {0: [], 1: [], 2: []}
+ for s in BubPlayer.MonsterList:
+ if isinstance(s, Alien):
+ in_place[s.in_place].append(s)
+ toohigh = self.globaly - relativey < -3*CELL
+ if in_place[1]:
+ xbounds = [s.x for s in in_place[1]]
+ self.alien_bounds = min(xbounds), max(xbounds)
+ shifter.next()
+ elif toohigh:
+ self.globaly += 1
+ squadtime -= 1
+ if nextsquad >= squadrons:
+ if not (in_place[0] or in_place[1]):
+ break
+ elif squadtime < 0 and not toohigh:
+ squadtime = 200
+ try:
+ rank = 0
+ while 1:
+ Alien(self, nextsquad, rank, relativey)
+ rank += 1
+ except StopIteration:
+ pass
+ nextsquad += 1
+ relativey += 4*CELL
+ for t in range(20):
+ yield None
+
+ def shifter(self):
+ while 1:
+ # go right
+ while self.alien_bounds[1] < boards.bwidth-5*CELL:
+ self.globalx += 2
+ yield None
+ # go down
+ for i in range(3*CELL):
+ self.globaly += 1
+ yield None
+ # go left
+ while self.alien_bounds[0] > 3*CELL:
+ self.globalx -= 2
+ yield None
+ # go down
+ for i in range(3*CELL):
+ self.globaly += 1
+ yield None
+
+ def build_ships(self):
+ for p in BubPlayer.PlayerList:
+ dragons = [d for d in p.dragons if not isinstance(d, Ship)]
+ if dragons and len(p.dragons) == len(dragons):
+ if self.builddelay.get(p):
+ self.builddelay[p] -= 1
+ else:
+ self.builddelay[p] = 75
+ dragon = random.choice(dragons)
+ ship = Ship(self, p, dragon.x, dragon.y)
+ ship.dcap = dragon.dcap
+ p.dragons.append(ship)
+ p.emotic(dragon, 4)
+ for d in dragons:
+ d.kill()
+
+# This game is suitable for at least min_players players
+min_players = 1
+
+def run():
+ global curboard
+ from boards import curboard
+ boards.replace_boardgen(Galaga().bgen())
+
+def setup():
+ for key, (filename, rect) in localmap.items():
+ filename = os.path.join(LocalDir, filename)
+ if filename.find('%d') >= 0:
+ for p in BubPlayer.PlayerList:
+ images.sprmap[key, p.pn] = (filename % p.pn, rect)
+ else:
+ images.sprmap[key] = (filename, rect)
+setup()
diff --git a/bubbob/ext3/image1-0.ppm b/bubbob/ext3/image1-0.ppm
new file mode 100644
index 0000000..89e9178
--- /dev/null
+++ b/bubbob/ext3/image1-0.ppm
Binary files differ
diff --git a/bubbob/ext3/music.wav b/bubbob/ext3/music.wav
new file mode 100644
index 0000000..4684177
--- /dev/null
+++ b/bubbob/ext3/music.wav
Binary files differ
diff --git a/bubbob/ext3/shoot.wav b/bubbob/ext3/shoot.wav
new file mode 100644
index 0000000..fb2b099
--- /dev/null
+++ b/bubbob/ext3/shoot.wav
Binary files differ
diff --git a/bubbob/ext4/.cvsignore b/bubbob/ext4/.cvsignore
new file mode 100644
index 0000000..553d0ec
--- /dev/null
+++ b/bubbob/ext4/.cvsignore
@@ -0,0 +1,2 @@
+*.py[co]
+image1-[2-9].ppm
diff --git a/bubbob/ext4/__init__.py b/bubbob/ext4/__init__.py
new file mode 100644
index 0000000..1efa86e
--- /dev/null
+++ b/bubbob/ext4/__init__.py
@@ -0,0 +1,440 @@
+from __future__ import generators
+import os, math, random
+import images, gamesrv
+from images import ActiveSprite
+from boards import CELL, HALFCELL, bget
+from mnstrmap import GreenAndBlue
+from bubbles import BubblingEyes, Bubble
+from bonuses import Bonus, points
+
+LocalDir = os.path.basename(os.path.dirname(__file__))
+
+
+localmap = {
+ 't-brick1': ('image1-%d.ppm', (0, 0, 16, 16)),
+ 't-brick2': ('image1-%d.ppm', (16, 0, 16, 16)),
+ }
+
+music = gamesrv.getmusic(os.path.join(LocalDir, 'music.wav'))
+
+
+class BrickEyes(BubblingEyes):
+
+ Patterns = [[(-2,0), (-1,0), (0,0), (1,0)],
+ [(-2,0), (-1,0), (0,0), (0,-1)],
+ [(-1,-1), (-1,0), (0,0), (1,0)],
+ [(-1,0), (0,0), (0,-1), (1,0)],
+ [(-1,-1), (0,-1), (0,0), (1,0)],
+ [(-2,0), (-1,0), (-1,-1), (0,-1)],
+ [(-1,-1), (-1,0), (0,-1), (0,0)]]
+
+ def __init__(self, tetris, bubber, saved_caps, olddragon):
+ BubblingEyes.__init__(self, bubber, saved_caps, olddragon)
+ self.tetris = tetris
+ self.bricks = []
+
+ def playing_bubble(self, oldsprite):
+ import boards
+ self.pat = random.choice(self.Patterns)
+ self.orientation = 1,0
+ xmin = 2 - min([x for x,y in self.pat])
+ xmax = boards.width-3 - max([x for x,y in self.pat])
+ x = int(random.normalvariate(oldsprite.x, boards.bwidth/4))
+ x = (x+HALFCELL) // CELL
+ if x<xmin: x=xmin
+ if x>xmax: x=xmax
+ y = -1
+ self.tx = x
+ self.ty = y
+ self.move((x-1)*CELL, (y-1)*CELL)
+ for i in range(5):
+ yield None
+ self.bricks = [Brick(self.bubber, px, py)
+ for px, py in self.brick_positions()]
+ self.gen.append(self.step_control())
+ self.gen.append(self.rotate_control())
+ self.gen.append(self.fall_control())
+ self.gen.append(self.move_eyes())
+
+ def bottom_up(self):
+ return 0
+
+ def kill(self):
+ for b in self.bricks:
+ b.stop(self.tetris)
+ b.remove()
+ self.bricks = []
+ BubblingEyes.kill(self)
+
+ def brick_positions(self):
+ ox, oy = self.orientation
+ result = []
+ cx = self.tx*CELL - HALFCELL
+ cy = self.ty*CELL - HALFCELL
+ for px, py in self.pat:
+ px = px*CELL + HALFCELL
+ py = py*CELL + HALFCELL
+ result.append((cx+px*ox-py*oy, cy+px*oy+py*ox))
+ return result
+
+ def save_position(self):
+ return self.tx, self.ty, self.orientation
+
+ def restore_position(self, p):
+ self.tx, self.ty, self.orientation = p
+
+ def moved(self, old_position):
+ for b in self.bricks:
+ b.set(' ')
+ try:
+ for px, py in self.brick_positions():
+ if bget(px//CELL, py//CELL) != ' ':
+ self.restore_position(old_position)
+ return 0
+ for b, (px, py) in zip(self.bricks, self.brick_positions()):
+ b.follow(px, py)
+ finally:
+ for b in self.bricks:
+ b.set('!') # note: we need '!' < '#'
+ return 1
+
+ def onground(self):
+ for b in self.bricks:
+ if b.gen: # brick still moving
+ return 0
+ for px, py in self.brick_positions():
+ if bget(px//CELL, py//CELL+1) >= '#':
+ return 1
+ return 0
+
+ def step_control(self):
+ while 1:
+ while not self.bubber.wannago(self.dcap):
+ yield None
+ pos = self.save_position()
+ self.tx += self.bubber.wannago(self.dcap)
+ if self.moved(pos):
+ for i in range(4):
+ yield None
+ yield None
+
+ def fall_control(self):
+ delay = 1
+ while 1:
+ for i in range(delay and 14):
+ if self.bubber.key_fire:
+ break
+ yield None
+ pos = self.save_position()
+ self.ty += 1
+ delay = self.moved(pos)
+ if delay:
+ for i in range(3):
+ yield None
+ elif self.onground() and self.tetris.ready:
+ self.gen = [self.stopping()]
+ yield None
+
+ def rotate_control(self):
+ while 1:
+ while not self.bubber.key_jump:
+ yield None
+ pos = self.save_position()
+ ox, oy = self.orientation
+ self.orientation = oy, -ox
+ if self.moved(pos):
+ for i in range(7):
+ yield None
+ yield None
+
+ def stopping(self):
+ self.move(self.x, -self.ico.h)
+ positions = [(py//CELL, px//CELL) for px, py in self.brick_positions()
+ if py >= 0]
+ positions.sort()
+ positions = [(px, py) for py, px in positions]
+ for b in self.bricks:
+ b.stop(self.tetris)
+ if b.ty < 0:
+ b.remove()
+ self.bricks = []
+ staticbricks = self.tetris.staticbricks
+ pts = 500
+ while 1:
+ for px, py in positions:
+ y = py
+ x1 = px
+ while (x1-1, y) in staticbricks:
+ x1 -= 1
+ if bget(x1-1, y) != '#':
+ continue
+ x2 = px
+ while (x2, y) in staticbricks:
+ x2 += 1
+ if bget(x2, y) != '#':
+ continue
+ if x2-x1 < 2:
+ continue
+ # full line
+ ico = images.sprget(Bubble.exploding_bubbles[0])
+ self.tetris.score[self.bubber] = self.tetris.score.get(
+ self.bubber, 0) + 1
+ xlist = range(x1, x2)
+ for x in xlist:
+ s = ActiveSprite(ico,
+ x*CELL + random.randrange(CELL) - CELL,
+ y*CELL + random.randrange(CELL) - CELL)
+ s.gen.append(s.die(Bubble.exploding_bubbles))
+ s = staticbricks[x, y]
+ points(x*CELL + HALFCELL, y*CELL + HALFCELL, s, pts)
+ s.remove()
+ if pts == 500:
+ self.play(images.Snd.Fruit)
+ elif pts == 4000:
+ self.play(images.Snd.Extralife)
+ else:
+ self.play(images.Snd.Extra)
+ pts *= 2
+ for y in range(py-1, -1, -1):
+ if not [x for x in xlist if (x, y) in staticbricks]:
+ break
+ for t in range(4):
+ yield None
+ if [x for x in xlist if (x, y+1) in staticbricks]:
+ break
+ for x in xlist:
+ if (x, y) in staticbricks:
+ staticbricks[x, y].shiftdown()
+ yield None
+ break
+ else:
+ break
+ if self.tetris.ready < 2:
+ self.gen.append(self.playing_bubble(self))
+
+ def move_eyes(self):
+ while 1:
+ tx = (self.tx-1) * CELL
+ ty = (self.ty-1) * CELL
+ for i in range(3):
+ if tx < self.x:
+ dx = -1
+ elif tx > self.x:
+ dx = +1
+ else:
+ dx = 0
+ if ty > self.y:
+ dy = +1
+ else:
+ dy = 0
+ self.step(2*dx, 2*dy)
+ key = ('eyes', dx, 0)
+ self.seticon(images.sprget(key))
+ yield None
+
+
+class Brick(ActiveSprite):
+
+ def __init__(self, bubber, x, y):
+ ico = images.sprget(('t-brick1', bubber.pn))
+ ActiveSprite.__init__(self, ico, x, y)
+ self.tx = x//CELL
+ self.ty = y//CELL
+ self.bubber = bubber
+
+ def follow(self, x, y):
+ self.tx = x//CELL
+ self.ty = y//CELL
+ self.gen = [self.following(x, y)]
+
+ def following(self, nx, ny):
+ dx = (nx - self.x) / 7.0
+ dy = (ny - self.y) / 7.0
+ for i in range(6, 0, -1):
+ self.move(nx - int(i*dx), ny - int(i*dy))
+ yield None
+ self.move(nx, ny)
+
+ def set(self, c):
+ x, y = self.tx, self.ty
+ if 0 <= x < curboard.width and 0 <= y < curboard.height:
+ line = curboard.walls[y]
+ curboard.walls[y] = line[:x] + c + line[x+1:]
+
+ def stop(self, tetris):
+ self.set('X')
+ self.seticon(images.sprget(('t-brick2', self.bubber.pn)))
+ images.ActiveSprites.remove(self)
+ tetris.staticbricks[self.tx, self.ty] = self
+ self.staticbricks = tetris.staticbricks
+
+ def remove(self):
+ del self.staticbricks[self.tx, self.ty]
+ self.set(' ')
+ gamesrv.Sprite.kill(self)
+
+ def shiftdown(self):
+ del self.staticbricks[self.tx, self.ty]
+ self.set(' ')
+ self.ty += 1
+ self.set('X')
+ self.staticbricks[self.tx, self.ty] = self
+ self.step(0, CELL)
+
+
+class Tetris:
+
+ def bgen(self, limittime = 90.1): # 1:30
+ import boards
+ from player import BubPlayer
+
+ self.score = {}
+ for t in boards.initsubgame(music, self.displaypoints):
+ yield t
+
+ tc = boards.TimeCounter(limittime)
+ self.ready = 0
+ self.staticbricks = {}
+ finished = 0
+ for t in self.frame():
+ t = boards.normal_frame()
+ self.build_eyes()
+ yield t
+ tc.update(t)
+ if tc.time == 0.0:
+ self.ready = 2
+ finished += not self.still_playing()
+ if finished > 16:
+ break
+ if (BubPlayer.FrameCounter & 15) == 7:
+ for s in images.ActiveSprites:
+ if isinstance(s, Bubble):
+ s.pop()
+ elif isinstance(s, Bonus):
+ s.kill()
+
+ tc.restore()
+ for t in boards.result_ranking(self.score):
+ self.remove_eyes()
+ yield t
+ for s in self.staticbricks.values():
+ s.remove()
+
+ def displaypoints(self, bubber):
+ return self.score.get(bubber, 0)
+
+ def frame(self):
+ heights = {1: curboard.height,
+ curboard.width-2: curboard.height}
+ ymax = curboard.height-1
+ maxheight = curboard.height*3//4
+ for x in range(2, curboard.width-2):
+ if bget(x, ymax) == ' ':
+ curboard.putwall(x, ymax)
+ height = 1
+ for y in range(ymax-1, -1, -1):
+ if bget(x, y) == '#':
+ if height == maxheight:
+ curboard.killwall(x, y)
+ else:
+ height += 1
+ heights[x] = height
+ xlist = range(2, curboard.width-2)
+ random.shuffle(xlist)
+ for x in xlist:
+ h = heights[x]
+ x1 = x2 = x
+ while heights[x1-1] == h:
+ x1 -= 1
+ while heights[x2] == h:
+ x2 += 1
+ parts = (x2-x1) // 8
+ if not parts:
+ continue
+ left = 0
+ if heights[x1-1] > h:
+ x1 -= 1
+ left += 1
+ right = parts+1
+ if heights[x2] > h:
+ x2 += 1
+ right -= 1
+ for p in range(left, right):
+ x = x1 + ((x2-x1-1)*p+parts//2)//parts
+ y = ymax
+ for i in range(2):
+ while bget(x, y) == '#':
+ y -= 1
+ if y >= 3:
+ curboard.putwall(x, y)
+ heights[x] += 1
+ curboard.reorder_walls()
+
+ walls_by_pos = curboard.walls_by_pos
+ moves = 1
+ s = 8.0
+ while moves:
+ moves = 0
+ for y in range(curboard.height-3, -1, -1):
+ for x in range(2, curboard.width-2):
+ if ((y,x) in walls_by_pos and
+ (y+1,x) not in walls_by_pos):
+ y0 = y
+ while (y0-1,x) in walls_by_pos:
+ y0 -= 1
+ w = curboard.killwall(x, y0, 0)
+ curboard.putwall(x, y+1, w)
+ moves = 1
+ curboard.reorder_walls()
+ for i in range(int(s)+2):
+ yield None
+ s *= 0.95
+ self.ready = 1
+ while 1:
+ yield None
+
+ def build_eyes(self):
+ from player import BubPlayer
+ for p in BubPlayer.PlayerList:
+ dragons = [d for d in p.dragons if not isinstance(d, BrickEyes)]
+ if dragons and len(p.dragons) == len(dragons):
+ dragon = random.choice(dragons)
+ eyes = BrickEyes(self, p, dragon.dcap, dragon)
+ p.dragons.append(eyes)
+ #p.emotic(dragon, 4)
+ for d in dragons:
+ d.kill()
+
+ def still_playing(self):
+ from player import BubPlayer
+ for p in BubPlayer.PlayerList:
+ for d in p.dragons:
+ if d.gen:
+ return 1
+ return 0
+
+ def remove_eyes(self):
+ from player import BubPlayer
+ for p in BubPlayer.PlayerList:
+ for d in p.dragons:
+ d.kill()
+
+# This game is suitable for at least min_players players
+min_players = 1
+
+def run():
+ global curboard
+ import boards
+ from boards import curboard
+ boards.replace_boardgen(Tetris().bgen())
+
+def setup():
+ from player import BubPlayer
+ for key, (filename, rect) in localmap.items():
+ filename = os.path.join(LocalDir, filename)
+ if filename.find('%d') >= 0:
+ for p in BubPlayer.PlayerList:
+ images.sprmap[key, p.pn] = (filename % p.pn, rect)
+ else:
+ images.sprmap[key] = (filename, rect)
+setup()
diff --git a/bubbob/ext4/image1-0.ppm b/bubbob/ext4/image1-0.ppm
new file mode 100644
index 0000000..60a86ac
--- /dev/null
+++ b/bubbob/ext4/image1-0.ppm
Binary files differ
diff --git a/bubbob/ext4/music.wav b/bubbob/ext4/music.wav
new file mode 100644
index 0000000..b4b01a4
--- /dev/null
+++ b/bubbob/ext4/music.wav
Binary files differ
diff --git a/bubbob/ext5/.cvsignore b/bubbob/ext5/.cvsignore
new file mode 100644
index 0000000..539da74
--- /dev/null
+++ b/bubbob/ext5/.cvsignore
@@ -0,0 +1 @@
+*.py[co]
diff --git a/bubbob/ext5/__init__.py b/bubbob/ext5/__init__.py
new file mode 100644
index 0000000..ef73f14
--- /dev/null
+++ b/bubbob/ext5/__init__.py
@@ -0,0 +1,289 @@
+from __future__ import generators
+import os, random
+import images, gamesrv
+from images import ActiveSprite
+import boards
+from boards import CELL, HALFCELL, bget
+from monsters import Monster
+from player import Dragon, BubPlayer
+from bubbles import Bubble
+import bonuses
+
+LocalDir = os.path.basename(os.path.dirname(__file__))
+
+localmap = {
+ ('lem-walk', 1,0) : ('image1.ppm', ( 0, 0, 32, 32)),
+ ('lem-walk', 1,1) : ('image1.ppm', ( 32, 0, 32, 32)),
+ ('lem-walk', 1,2) : ('image1.ppm', ( 64, 0, 32, 32)),
+ ('lem-walk', 1,3) : ('image1.ppm', ( 96, 0, 32, 32)),
+ ('lem-walk', 1,4) : ('image1.ppm', (128, 0, 32, 32)),
+ ('lem-walk', 1,5) : ('image1.ppm', (160, 0, 32, 32)),
+ ('lem-walk', 1,6) : ('image1.ppm', (192, 0, 32, 32)),
+ ('lem-walk', 1,7) : ('image1.ppm', (224, 0, 32, 32)),
+ ('lem-fall', 1,0) : ('image1.ppm', (256, 0, 32, 32)),
+ ('lem-fall', 1,1) : ('image1.ppm', (288, 0, 32, 32)),
+ ('lem-fall', 1,2) : ('image1.ppm', (320, 0, 32, 32)),
+ ('lem-fall', 1,3) : ('image1.ppm', (352, 0, 32, 32)),
+
+ ('lem-fall',-1,3) : ('image2.ppm', ( 0, 0, 32, 32)),
+ ('lem-fall',-1,2) : ('image2.ppm', ( 32, 0, 32, 32)),
+ ('lem-fall',-1,1) : ('image2.ppm', ( 64, 0, 32, 32)),
+ ('lem-fall',-1,0) : ('image2.ppm', ( 96, 0, 32, 32)),
+ ('lem-walk',-1,7) : ('image2.ppm', (128, 0, 32, 32)),
+ ('lem-walk',-1,6) : ('image2.ppm', (160, 0, 32, 32)),
+ ('lem-walk',-1,5) : ('image2.ppm', (192, 0, 32, 32)),
+ ('lem-walk',-1,4) : ('image2.ppm', (224, 0, 32, 32)),
+ ('lem-walk',-1,3) : ('image2.ppm', (256, 0, 32, 32)),
+ ('lem-walk',-1,2) : ('image2.ppm', (288, 0, 32, 32)),
+ ('lem-walk',-1,1) : ('image2.ppm', (320, 0, 32, 32)),
+ ('lem-walk',-1,0) : ('image2.ppm', (352, 0, 32, 32)),
+
+ ('lem-jail', 0) : ('image4.ppm', ( 0, 0, 32, 32)),
+ ('lem-jail', 1) : ('image4.ppm', ( 0, 32, 32, 32)),
+ ('lem-jail', 2) : ('image4.ppm', ( 0, 64, 32, 32)),
+ }
+for n in range(16):
+ localmap[('lem-crash', n)] = ('image3.ppm', (32*n, 0, 32, 32))
+
+music = gamesrv.getmusic(os.path.join(LocalDir, 'music.wav'))
+snd_ouch = gamesrv.getsample(os.path.join(LocalDir, 'ouch.wav'))
+
+
+class Lemmy:
+ right = [('lem-walk', 1,n) for n in range(8)]
+ left = [('lem-walk',-1,n) for n in range(8)]
+ jailed = [('lem-jail', n) for n in range(3)]
+
+
+class Lemming(Monster):
+
+ def __init__(self, lemmings, x, y, dir):
+ Monster.__init__(self, Lemmy, x, y, dir, in_list=lemmings.lemlist)
+ self.lemmings = lemmings
+
+ def argh(self, *args, **kwds):
+ self.untouchable()
+ self.gen = [self.jumpout()]
+
+ def resetimages(self):
+ pass
+
+ def touched(self, dragon):
+ if 20 >= abs(self.x - dragon.x) >= 14:
+ if self.x < dragon.x:
+ self.dir = -1
+ else:
+ self.dir = 1
+
+ def in_bubble(self, bubble):
+ self.move(bubble.x, bubble.y)
+ Monster.in_bubble(self, bubble)
+ return -1
+
+ def bubbling(self, bubble):
+ dx = random.randrange(-3, 4)
+ dy = random.randrange(-4, 2)
+ counter = 0
+ while not hasattr(bubble, 'poplist'):
+ if self.y < -CELL and bubble.y > CELL: # bubble wrapped
+ self.leaveboard(bubble)
+ return
+ self.move(bubble.x+dx, bubble.y+dy)
+ yield None
+ if bubble.poplist is None and bubble.y <= -2*CELL+1:
+ self.leaveboard(bubble)
+ return
+ self.setimages(None)
+ self.gen = [self.jumpout()]
+
+ def jumpout(self):
+ # jumping out of the bubble
+ self.seticon(images.sprget(self.mdef.jailed[1]))
+ dxy = [(random.random()-0.5) * 9.0,
+ (random.random()+0.5) * (-5.0)]
+ for n in self.parabolic(dxy):
+ yield n
+ if dxy[1] >= 2.0:
+ break
+ if dxy[0] < 0:
+ self.dir = -1
+ else:
+ self.dir = 1
+ self.touchable = 1
+ self.gen.append(self.falling())
+
+ def falling(self):
+ self.setimages(None)
+ n = 0
+ lemmap = self.lemmings.lemmap
+ while not self.onground():
+ yield None
+ self.move(self.x, (self.y + 4) & ~3,
+ lemmap['lem-fall', self.dir, n&3])
+ n += 1
+ if self.y >= boards.bheight:
+ self.kill()
+ return
+ yield None
+ if n <= 33:
+ self.gen.append(self.walking())
+ else:
+ self.play(snd_ouch)
+ self.untouchable()
+ self.to_front()
+ self.gen = [self.die([('lem-crash', n) for n in range(16)], 2)]
+
+ def walking(self):
+ self.setimages(None)
+ n = 0
+ lemmap = self.lemmings.lemmap
+ y0 = self.y // 16
+ while self.y == y0*16:
+ yield None
+ nx = self.x + self.dir*2
+ x0 = (nx+15) // 16
+ if bget(x0, y0+1) == ' ':
+ if bget(x0, y0+2) == ' ':
+ y0 += 1 # fall
+ elif bget(x0, y0) != ' ':
+ self.dir = -self.dir
+ self.resetimages()
+ continue
+ else: # climb
+ y0 -= 1
+ n2 = 0
+ while self.y > y0*16:
+ self.step(0, -2)
+ if n2:
+ n2 -= 1
+ else:
+ self.seticon(lemmap['lem-walk', self.dir, n&7])
+ n += 1
+ n2 = 2
+ yield None
+ self.move(nx, self.y, lemmap['lem-walk', self.dir, n&7])
+ n += 1
+ yield None
+ yield None
+ self.gen.append(self.falling())
+
+ def onground(self):
+ if self.y & 15:
+ return 0
+ x0 = (self.x+15) // 16
+ y0 = self.y // 16 + 2
+ return bget(x0, y0) != ' ' == bget(x0, y0-1)
+
+ def leaveboard(self, bubble):
+ if hasattr(bubble, 'd'):
+ bubble.play(images.Snd.Extra)
+ score = self.lemmings.score
+ bubber = bubble.d.bubber
+ score[bubber] = score.get(bubber, 0) + 1
+ bonuses.points(bubble.x, bubble.y, bubble.d, 500)
+ self.kill()
+
+ default_mode = walking
+
+
+class Lemmings:
+
+ def bgen(self, limittime = 60.1): # 0:60
+ self.score = {}
+ for t in boards.initsubgame(music, self.displaypoints):
+ yield t
+ self.lemmap = {}
+ for key in localmap:
+ self.lemmap[key] = images.sprget(key)
+
+ tc = boards.TimeCounter(limittime)
+ self.lemlist = []
+ self.lemtotal = 0
+ for t in self.frame():
+ t = boards.normal_frame()
+ yield t
+ tc.update(t)
+ if tc.time == 0.0:
+ break
+
+ tc.restore()
+ for s in self.lemlist[:]:
+ if s.alive:
+ s.kill()
+ for s in images.ActiveSprites[:]:
+ if isinstance(s, Bubble):
+ s.pop()
+ for t in boards.result_ranking(self.score.copy(), self.lemtotal):
+ yield t
+
+ def displaypoints(self, bubber):
+ return self.score.get(bubber, 0)
+
+ def frame(self):
+ windline = '>>' + '^'*(curboard.width-4) + '<<'
+ curboard.winds = [windline] * curboard.height
+
+ countholes = 0
+ ymax = curboard.height-1
+ for x in range(2, curboard.width-2):
+ if bget(x, ymax) == ' ':
+ countholes += 1
+
+ xrange = []
+ try:
+ for delta in range(2, curboard.width):
+ for x in [delta, curboard.width-delta-1]:
+ if x in xrange: raise StopIteration
+ xrange.append(x)
+ except StopIteration:
+ pass
+
+ for x in xrange:
+ if countholes > curboard.width//6 and bget(x, ymax) == ' ':
+ curboard.putwall(x, ymax)
+ curboard.reorder_walls()
+ countholes -= 1
+ for y in range(0, ymax):
+ if bget(x, y) == ' ':
+ break
+ curboard.killwall(x, y)
+ yield None
+
+ testing = {}
+ def addlemming():
+ for x, y in testing.items():
+ if bget(x, y) != ' ' == bget(x, y-1):
+ if x <= curboard.width//2:
+ dir = 1
+ else:
+ dir = -1
+ s = Lemming(self, x*CELL-HALFCELL, (y-2)*CELL, dir)
+ self.lemtotal += 1
+ if y < ymax:
+ testing[x] = y+1
+ else:
+ del testing[x]
+ for x in xrange:
+ testing[x] = 1
+ addlemming()
+ yield None
+ while testing:
+ addlemming()
+ yield None
+
+ while self.lemlist:
+ yield None
+
+# This game is suitable for at least min_players players
+min_players = 1
+
+def run():
+ global curboard
+ from boards import curboard
+ boards.replace_boardgen(Lemmings().bgen())
+
+def setup():
+ for key, (filename, rect) in localmap.items():
+ filename = os.path.join(LocalDir, filename)
+ images.sprmap[key] = (filename, rect)
+setup()
diff --git a/bubbob/ext5/image1.ppm b/bubbob/ext5/image1.ppm
new file mode 100644
index 0000000..bb36c9c
--- /dev/null
+++ b/bubbob/ext5/image1.ppm
@@ -0,0 +1,5 @@
+P6
+# CREATOR: The GIMP's PNM Filter Version 1.0
+384 32
+255
+@ÿ
diff --git a/bubbob/ext5/image2.ppm b/bubbob/ext5/image2.ppm
new file mode 100644
index 0000000..6eef883
--- /dev/null
+++ b/bubbob/ext5/image2.ppm
@@ -0,0 +1,5 @@
+P6
+# CREATOR: The GIMP's PNM Filter Version 1.0
+384 32
+255
+@ÿ
diff --git a/bubbob/ext5/image3.ppm b/bubbob/ext5/image3.ppm
new file mode 100644
index 0000000..5d0eb00
--- /dev/null
+++ b/bubbob/ext5/image3.ppm
@@ -0,0 +1,5 @@
+P6
+# CREATOR: The GIMP's PNM Filter Version 1.0
+512 32
+255
+
diff --git a/bubbob/ext5/image4.ppm b/bubbob/ext5/image4.ppm
new file mode 100644
index 0000000..8f22db8
--- /dev/null
+++ b/bubbob/ext5/image4.ppm
Binary files differ
diff --git a/bubbob/ext5/music.wav b/bubbob/ext5/music.wav
new file mode 100644
index 0000000..de1efb4
--- /dev/null
+++ b/bubbob/ext5/music.wav
Binary files differ
diff --git a/bubbob/ext5/ouch.wav b/bubbob/ext5/ouch.wav
new file mode 100644
index 0000000..5d3dd8b
--- /dev/null
+++ b/bubbob/ext5/ouch.wav
Binary files differ
diff --git a/bubbob/ext6/.cvsignore b/bubbob/ext6/.cvsignore
new file mode 100644
index 0000000..553d0ec
--- /dev/null
+++ b/bubbob/ext6/.cvsignore
@@ -0,0 +1,2 @@
+*.py[co]
+image1-[2-9].ppm
diff --git a/bubbob/ext6/__init__.py b/bubbob/ext6/__init__.py
new file mode 100644
index 0000000..bd4c4d4
--- /dev/null
+++ b/bubbob/ext6/__init__.py
@@ -0,0 +1,268 @@
+from __future__ import generators
+import os, random
+import images, gamesrv
+from images import ActiveSprite
+import boards
+from boards import CELL, HALFCELL, bget
+from player import Dragon, BubPlayer
+from mnstrmap import Monky
+from bubbles import Bubble
+from bonuses import Bonus
+
+LocalDir = os.path.basename(os.path.dirname(__file__))
+
+localmap = {
+ ('trn-head', 0,-1): ('image1-%d.ppm', (0, 0, 8, 8)),
+ ('trn-head',-1, 0): ('image1-%d.ppm', (0, 8, 8, 8)),
+ ('trn-head', 0, 1): ('image1-%d.ppm', (0,16, 8, 8)),
+ ('trn-head', 1, 0): ('image1-%d.ppm', (0,24, 8, 8)),
+ ('trn', 0,-1, 1, 0): ('image1-%d.ppm', (0,32, 8, 8)),
+ ('trn', 0, 1, 1, 0): ('image1-%d.ppm', (0,40, 8, 8)),
+ ('trn', 1, 0, 0,-1): ('image1-%d.ppm', (0,48, 8, 8)),
+ ('trn', 1, 0, 0, 1): ('image1-%d.ppm', (0,56, 8, 8)),
+ ('trn', 1, 0, 1, 0): ('image1-%d.ppm', (0,64, 8, 8)),
+ ('trn', 0, 1, 0, 1): ('image1-%d.ppm', (0,72, 8, 8)),
+ }
+
+music = gamesrv.getmusic(os.path.join(LocalDir, 'music.wav'))
+snd_crash = gamesrv.getsample(os.path.join(LocalDir, 'crash.wav'))
+
+
+class TronHead(ActiveSprite):
+
+ def __init__(self, tron, bubber, dcap, cx, cy, dir):
+ self.tron = tron
+ self.bubber = bubber
+ self.dcap = dcap
+ self.cx = cx
+ self.cy = cy
+ self.dir = dir
+ self.icons = {}
+ for key in localmap:
+ ico = images.sprget((key, bubber.pn))
+ key = key[1:]
+ self.icons[key] = ico
+ if len(key) == 4:
+ dx1, dy1, dx2, dy2 = key
+ key = -dx2, -dy2, -dx1, -dy1
+ self.icons[key] = ico
+ ActiveSprite.__init__(self, self.icons[self.dir],
+ self.cx*8-2, self.cy*8-2)
+ self.gen.append(self.trailing())
+
+ def forward_step(self, dir):
+ s = gamesrv.Sprite(self.icons[self.dir + dir], self.x, self.y)
+ self.tron.trailsprites.append(s)
+ self.dir = dir
+ self.cx += dir[0]
+ self.cy += dir[1]
+ self.move(self.cx*8-2, self.cy*8-2, self.icons[dir])
+
+ def trailing(self):
+ unoccupied = self.tron.unoccupied
+ bubber = self.bubber
+ # first go straight forward until we enter the playing board itself
+ while (self.cx, self.cy) not in unoccupied:
+ self.forward_step(self.dir)
+ yield None
+ yield None
+ # playing!
+ unoccupied[self.cx, self.cy] = False
+ while True:
+ # turn
+ d = [(bubber.key_left, -1, 0),
+ (bubber.key_right, 1, 0),
+ (bubber.key_jump, 0,-1),
+ (bubber.key_fire, 0, 1)]
+ d.sort()
+ newdir = self.dir
+ if d[-1][0] > d[-2][0]:
+ newdir = d[-1][1:]
+ if (self.dir + newdir) not in self.icons:
+ newdir = self.dir # forbidden 180-degree turn
+ # move one step forward
+ self.forward_step(newdir)
+ # crash?
+ if not unoccupied.get((self.cx, self.cy)):
+ self.crash()
+ return
+ unoccupied[self.cx, self.cy] = False
+ yield None
+ yield None
+
+ def to_front(self):
+ if self.gen:
+ ActiveSprite.to_front(self)
+
+ def crash(self):
+ self.move(self.x - self.dir[0], self.y - self.dir[1],
+ self.icons[self.dir+self.dir])
+ self.to_back()
+ self.play(snd_crash)
+ ico = images.sprget(Monky.decay_weapon[1])
+ s = ActiveSprite(ico, self.x + self.ico.w//2 - CELL,
+ self.y + self.ico.h//2 - CELL)
+ s.gen.append(s.die(Monky.decay_weapon[1:], 4))
+ self.stop()
+
+ def stop(self):
+ del self.gen[:]
+ try:
+ self.tron.trons.remove(self)
+ except ValueError:
+ pass
+
+ def kill(self):
+ self.stop()
+ try:
+ self.bubber.dragons.remove(self)
+ except ValueError:
+ pass
+ ActiveSprite.kill(self)
+
+
+class Tron:
+
+ def bgen(self, limittime = 60.1): # 1:00
+ self.score = {}
+ for t in boards.initsubgame(music, self.displaypoints):
+ yield t
+
+ self.ready = 0
+ self.trons = []
+ self.trailsprites = []
+ self.playerlist = BubPlayer.PlayerList[:]
+ tc = boards.TimeCounter(limittime)
+ for t in self.frame(tc):
+ t = boards.normal_frame()
+ self.build_trons()
+ yield t
+ tc.update(t)
+ if (BubPlayer.FrameCounter & 15) == 7:
+ for s in images.ActiveSprites:
+ if isinstance(s, Bubble):
+ s.pop()
+ elif isinstance(s, Bonus):
+ s.kill()
+
+ self.ready = 0
+ tc.restore()
+ for t in boards.result_ranking(self.score):
+ for p in BubPlayer.PlayerList:
+ for d in p.dragons[:]:
+ d.kill()
+ yield t
+ self.remove_trons()
+
+ def displaypoints(self, bubber):
+ return self.score.get(bubber, 0)
+
+ def build_trons(self):
+ if self.ready == 0:
+ self.remove_trons()
+ return
+ for p in self.playerlist:
+ dragons = [d for d in p.dragons if not isinstance(d, TronHead)]
+ if self.ready < 10 and dragons and len(p.dragons) == len(dragons):
+ self.score.setdefault(p, 0)
+ dragon = random.choice(dragons)
+ x, y, dir = self.select_start_point()
+ head = TronHead(self, p, dragon.dcap, x, y, dir)
+ self.trons.append(head)
+ p.dragons.append(head)
+ #p.emotic(head, 4)
+ for d in dragons:
+ d.kill()
+
+ def remove_trons(self):
+ for p in BubPlayer.PlayerList:
+ for d in p.dragons[:]:
+ d.kill()
+ for s in self.trailsprites:
+ s.kill()
+ del self.trailsprites[:]
+
+ def select_start_point(self):
+ distmin = 12
+ while True:
+ x, y, dir = random.choice(self.start_points)
+ for head in self.trons:
+ if abs(x-head.cx//2) + abs(y-head.cy//2) < distmin:
+ break
+ else:
+ break
+ distmin *= 0.95
+ if (y, x) in curboard.walls_by_pos:
+ curboard.killwall(x, y)
+ x = 2*x+1
+ y = 2*y - dir[1]
+ if dir[1] < 0:
+ y += 2
+ return x, y, dir
+
+ def frame(self, tc):
+ y1 = 1
+ y2 = curboard.height-2
+ while y1 <= y2:
+ for y in [y1, y2]:
+ for x in range(2, curboard.width-2):
+ if (y, x) in curboard.walls_by_pos:
+ curboard.killwall(x, y)
+ yield None
+ y1 += 1
+ y2 -= 1
+
+ self.start_points = []
+ for x in range(4, curboard.width-3):
+ self.start_points.append((x, 0, (0, 1)))
+ self.start_points.append((x, curboard.height-1, (0, -1)))
+
+ while tc.time != 0.0:
+ for y in [0, curboard.height-1]:
+ for x in range(2, curboard.width-2):
+ if (y, x) not in curboard.walls_by_pos:
+ curboard.putwall(x, y)
+ curboard.reorder_walls()
+ self.unoccupied = {}
+ for x in range(5, 2*curboard.width-4):
+ for y in range(3, 2*curboard.height-2):
+ self.unoccupied[x, y] = True
+ random.shuffle(self.playerlist)
+ for i in range(5):
+ yield None
+
+ min_players = 1
+ while self.ready < 20 or len(self.trons) >= min_players:
+ if len(self.trons) >= 2:
+ min_players = 2
+ self.ready += 1
+ yield None
+
+ if len(self.trons) == 1:
+ bubber = self.trons[0].bubber
+ self.score[bubber] += 1
+ bubber.givepoints(100)
+ self.trons[0].stop()
+ self.ready = 99
+
+ for i in range(28):
+ yield None
+ self.ready = 0
+
+# This game is suitable for at least min_players players
+min_players = 2
+
+def run():
+ global curboard
+ from boards import curboard
+ boards.replace_boardgen(Tron().bgen())
+
+def setup():
+ for key, (filename, rect) in localmap.items():
+ filename = os.path.join(LocalDir, filename)
+ if filename.find('%d') >= 0:
+ for p in BubPlayer.PlayerList:
+ images.sprmap[key, p.pn] = (filename % p.pn, rect)
+ else:
+ images.sprmap[key] = (filename, rect)
+setup()
diff --git a/bubbob/ext6/crash.wav b/bubbob/ext6/crash.wav
new file mode 100644
index 0000000..65499db
--- /dev/null
+++ b/bubbob/ext6/crash.wav
Binary files differ
diff --git a/bubbob/ext6/image1-0.ppm b/bubbob/ext6/image1-0.ppm
new file mode 100644
index 0000000..bb54ce2
--- /dev/null
+++ b/bubbob/ext6/image1-0.ppm
Binary files differ
diff --git a/bubbob/ext6/music.wav b/bubbob/ext6/music.wav
new file mode 100644
index 0000000..8c55b34
--- /dev/null
+++ b/bubbob/ext6/music.wav
Binary files differ
diff --git a/bubbob/ext7/.cvsignore b/bubbob/ext7/.cvsignore
new file mode 100644
index 0000000..553d0ec
--- /dev/null
+++ b/bubbob/ext7/.cvsignore
@@ -0,0 +1,2 @@
+*.py[co]
+image1-[2-9].ppm
diff --git a/bubbob/ext7/__init__.py b/bubbob/ext7/__init__.py
new file mode 100644
index 0000000..2d157ab
--- /dev/null
+++ b/bubbob/ext7/__init__.py
@@ -0,0 +1,399 @@
+from __future__ import generators
+import os, random, math
+import images, gamesrv
+from images import ActiveSprite
+import boards
+from boards import CELL
+from player import Dragon, BubPlayer, scoreboard
+from bubbles import Bubble
+from bonuses import Bonus
+from mnstrmap import PlayerBubbles
+from mnstrmap import Monky
+import bonuses
+from ext6 import snd_crash
+
+LocalDir = os.path.basename(os.path.dirname(__file__))
+
+ANGLE_COUNT = 24
+ANGLE_STEP = 360 / ANGLE_COUNT
+ANGLE_TABLE = {}
+for i in range(ANGLE_COUNT):
+ a = i*ANGLE_STEP*math.pi/180.0
+ ANGLE_TABLE[i*ANGLE_STEP] = (math.cos(a), math.sin(a))
+
+
+localmap = {}
+for i in range(ANGLE_COUNT):
+ localmap['camel', i] = ('image1-%d.ppm', (0, i*36, 36, 36))
+
+music = gamesrv.getmusic(os.path.join(LocalDir, 'music.wav'))
+snd_fire = gamesrv.getsample(os.path.join(LocalDir, 'fire.wav'))
+snd_hit = gamesrv.getsample(os.path.join(LocalDir, 'hit.wav'))
+
+
+class Plane(ActiveSprite):
+ lock = None
+
+ def __init__(self, camel, bubber, dcap, x, y, dirhint=None):
+ self.bubber = bubber
+ self.dcap = dcap
+ self.camel = camel
+ self.shotlist = camel.score.setdefault(bubber, {})
+
+ if x < x_min:
+ x = x_min
+ elif x > x_max:
+ x = x_max
+
+ if y < 4*CELL:
+ y = 4*CELL
+ elif y > (curboard.height-4)*CELL - 36:
+ y = (curboard.height-4)*CELL - 36
+
+ if dirhint not in (1, -1):
+ controldelay = 5
+ if x < boards.bwidth//2:
+ dir = 1
+ else:
+ dir = -1
+ else:
+ controldelay = 20
+ if x < boards.bwidth//3:
+ dir = 1
+ elif x >= boards.bwidth*2//3:
+ dir = -1
+ else:
+ dir = dirhint
+ if dir > 0:
+ self.angle = 0
+ self.flipped = False
+ else:
+ self.angle = 180
+ self.flipped = True
+
+ ActiveSprite.__init__(self, self.getico(), x, y)
+ self.fx = self.x
+ self.fy = self.y
+ self.controlgen = self.control(delay=controldelay)
+ self.gen.append(self.fly())
+ self.gen.append(self.controlgen)
+ self.gen.append(self.blink())
+ self.gen.append(self.bob())
+
+ def controlled(self):
+ return self.controlgen in self.gen
+
+ def getico(self):
+ a = self.angle // ANGLE_STEP
+ if self.flipped:
+ if a:
+ a = ANGLE_COUNT-a
+ key = 'vflip', ('camel', a, self.bubber.pn)
+ else:
+ key = 'camel', a, self.bubber.pn
+ return images.sprget(key)
+
+ def blink(self):
+ for i in range(10):
+ yield None
+ yield None
+ self.setdisplaypos(-256, -256)
+ yield None
+ self.setdisplaypos(-256, -256)
+ yield None
+ self.touchable = 1
+
+ def bob(self):
+ f = 3.0
+ for i in range(0, 1080, ANGLE_STEP):
+ self.fy += f * ANGLE_TABLE[i % 360][1]
+ f *= 0.98
+ yield None
+
+## def loosealtitude(self, y0, angle0):
+## if 90 <= angle0 < 270 or (angle0 == 270 and not self.flipped):
+## angledir = -1
+## else:
+## angledir = 1
+## for i in range(0, 180, ANGLE_STEP):
+## if i % (4*ANGLE_STEP) == 0 and not (45 <= angle0 <= 135):
+## angle0 += ANGLE_STEP * angledir
+## angle0 = (angle0 + 360) % 360
+## y0 += 4.0 * ANGLE_TABLE[i][1]
+## if y0 > self.fy:
+## self.fy = y0
+## self.angle = angle0
+## yield None
+
+ def turn(self, dir):
+ self.angle += ANGLE_STEP * dir
+ self.angle = (self.angle + 360) % 360
+
+ def control(self, delay=0):
+ bubber = self.bubber
+ prev_key_jump = 0
+ for i in range(delay):
+ yield None
+ shootdelay = 0
+ while True:
+ wannago = bubber.wannago(self.dcap)
+ self.turn(wannago)
+ if shootdelay:
+ shootdelay -= 1
+ elif bubber.key_fire:
+ x = self.x + self.ico.w//2
+ y = self.y + self.ico.h//2
+ acos, asin = ANGLE_TABLE[self.angle]
+ x += acos * 20
+ y += asin * 20
+ if self.flipped:
+ acos = -acos
+ asin = -asin
+ x -= asin * 5
+ y += acos * 5
+ self.play(snd_fire)
+ Shot(self, int(x), int(y), self.angle, 2)
+ Shot(self, int(x), int(y), self.angle)
+ shootdelay = 7
+ for i in range(2):
+ if bubber.key_jump > prev_key_jump:
+ self.flipped = not self.flipped
+ prev_key_jump = bubber.key_jump
+ yield None
+ for s in self.touching(12):
+ if isinstance(s, Plane) and s is not self:
+ ico = images.sprget(Monky.decay_weapon[1])
+ s1 = ActiveSprite(ico,
+ (self.x+s.x)//2 + self.ico.w//2 - CELL,
+ (self.y+s.y)//2 + self.ico.h//2 - CELL)
+ s1.gen.append(s1.die(Monky.decay_weapon[1:], 4))
+ s1.play(snd_crash)
+ self.gen = [self.godowninflames(s)]
+ s.gen = [s.godowninflames(self)]
+
+ def fly(self, speed=3.3):
+ while True:
+ if (self.y < 0 and not (0 < self.angle < 180) and
+ ((abs(270 - self.angle) < -4*self.y) or random.random() < 0.2)):
+ if (90 <= self.angle < 270 or
+ (self.angle == 270 and not self.flipped)):
+ self.turn(-1)
+ else:
+ self.turn(1)
+ ico = self.getico()
+ acos, asin = ANGLE_TABLE[self.angle]
+ self.fx += acos * speed
+ self.fy += asin * speed
+ self.move(int(self.fx), int(self.fy), ico)
+ if self.x < x_min:
+ self.angle = 2 * ANGLE_STEP
+ self.flipped = not self.flipped
+ self.gen = [self.godowninflames()]
+ self.play(images.Snd.Pop)
+ elif self.x > x_max:
+ self.angle = 180 - 2 * ANGLE_STEP
+ self.flipped = not self.flipped
+ self.gen = [self.godowninflames()]
+ self.play(images.Snd.Pop)
+ elif self.y > y_max:
+ self.gen = [self.crashed()]
+ yield None
+
+ def godowninflames(self, hit_by_plane=None):
+ if hit_by_plane and hit_by_plane in self.shotlist:
+ hittime = self.shotlist[hit_by_plane]
+ if BubPlayer.FrameCounter < hittime + 60:
+ del self.shotlist[hit_by_plane]
+ scoreboard()
+ self.seticon(self.getico())
+ self.gen.append(self.fly())
+ trail = [(self.x, self.y)] * 7
+ ico = images.sprget(PlayerBubbles.explosion[0])
+ s = ActiveSprite(ico, self.x + self.ico.w//2 - CELL,
+ self.y + self.ico.h//2 - CELL)
+ s.gen.append(s.die(PlayerBubbles.explosion))
+ self.bubber.emotic(self, 4)
+ while True:
+ yield None
+ if random.random() < 0.37:
+ ico = images.sprget(Bubble.exploding_bubbles[0])
+ x, y = random.choice(trail)
+ x += random.randint(-10, 10)
+ y += random.randint(-10, 10)
+ s = ActiveSprite(ico, x+2, y+2)
+ s.gen.append(s.die(Bubble.exploding_bubbles))
+ if random.random() < 0.5:
+ yield None
+ if 90 <= self.angle < 270:
+ lst = [0, 0, 0, 0, -1, -1, -1, 1, 1]
+ else:
+ lst = [0, 0, 0, 0, -1, -1, 1, 1, 1]
+ self.turn(random.choice(lst))
+ trail.pop(0)
+ trail.append((self.x, self.y))
+
+ def crashed(self):
+ self.untouchable()
+ self.play(snd_crash)
+ ico = images.sprget(Monky.decay_weapon[1])
+ self.seticon(ico)
+ self.step(self.ico.w//2 - CELL,
+ self.ico.h//2 - CELL)
+ self.gen.append(self.die(Monky.decay_weapon[1:], 4))
+ yield None
+
+ def kill(self):
+ try:
+ self.bubber.dragons.remove(self)
+ except ValueError:
+ pass
+ ActiveSprite.kill(self)
+
+
+class Shot(ActiveSprite):
+
+ def __init__(self, plane, x, y, angle, steps=0):
+ ico = images.sprcharacterget('.')
+ ActiveSprite.__init__(self, ico, x-4, y-12)
+ self.plane = plane
+ self.angle = angle
+ self.gen.append(self.moving(steps))
+
+ def moving(self, steps=0):
+ minx = 2*CELL - 4
+ maxx = (curboard.width-2)*CELL - 4
+ maxy = (curboard.height-1)*CELL - 12
+ fx = self.x
+ fy = self.y
+ dx, dy = ANGLE_TABLE[self.angle]
+ dx *= 7.6
+ dy *= 7.6
+ fx += dx * steps
+ fy += dy * steps
+ for i in range(22-steps):
+ for s in images.touching(self.x+3, self.y+11, 2, 2):
+ if isinstance(s, Plane) and s is not self.plane:
+ self.play(snd_hit)
+ self.kill()
+ if s.controlled():
+ s.gen = [s.godowninflames(self.plane)]
+ self.plane.shotlist[s] = BubPlayer.FrameCounter
+ bonuses.points(self.x + 4 - CELL, self.y + 12 - CELL,
+ self.plane, 100)
+ return
+ fx += dx
+ fy += dy
+ self.move(int(fx), int(fy))
+ if self.x < minx or self.x > maxx or self.y > maxy:
+ break
+ yield None
+ self.kill()
+
+
+class Camel:
+
+ def bgen(self, limittime = 90.1): # 1:30
+ self.score = {}
+ for t in boards.initsubgame(music, self.displaypoints):
+ yield t
+
+ tc = boards.TimeCounter(limittime)
+ for t in self.frame(tc):
+ t = boards.normal_frame()
+ self.build_planes()
+ yield t
+ tc.update(t)
+ if (BubPlayer.FrameCounter & 15) == 7:
+ for s in images.ActiveSprites:
+ if isinstance(s, Bubble):
+ s.pop()
+ elif isinstance(s, Bonus):
+ s.kill()
+
+ tc.restore()
+ score = {}
+ for player, shotlist in self.score.items():
+ score[player] = len(shotlist)
+ for t in boards.result_ranking(score):
+ for p in BubPlayer.PlayerList:
+ for d in p.dragons[:]:
+ d.kill()
+ yield t
+ self.remove_planes()
+
+ def displaypoints(self, bubber):
+ return len(self.score.get(bubber, ()))
+
+ def build_planes(self):
+ for p in BubPlayer.PlayerList:
+ dragons = [d for d in p.dragons if not isinstance(d, Plane)]
+ if dragons and len(p.dragons) == len(dragons):
+ dragon = random.choice(dragons)
+ if dragon.dcap['infinite_shield']:
+ start_position = self.select_start_position()
+ dirhint = None
+ else:
+ start_position = dragon.x-2, dragon.y-2
+ dirhint = getattr(dragon, 'dir', None)
+ plane = Plane(self, p, dragon.dcap,
+ start_position[0], start_position[1], dirhint)
+ p.dragons.append(plane)
+ p.emotic(plane, 4)
+ for d in dragons:
+ d.kill()
+
+ def remove_planes(self):
+ for p in BubPlayer.PlayerList:
+ for d in p.dragons[:]:
+ d.kill()
+
+ def select_start_position(self):
+ planes = [d for p in BubPlayer.PlayerList
+ for d in p.dragons
+ if isinstance(d, Plane)]
+ distmin = 180
+ while True:
+ x = random.choice([x_min, x_max])
+ y = random.randint(2*CELL, (curboard.height-4)*CELL - 36)
+ for d in planes:
+ dist = (x-d.x)*(x-d.x) + (y-d.y)*(y-d.y)
+ if dist < distmin*distmin:
+ break
+ else:
+ return x, y
+ distmin = int(distmin * 0.94)
+
+ def frame(self, tc):
+ y = curboard.height-1
+ for x in range(2, curboard.width-2):
+ if (y, x) not in curboard.walls_by_pos:
+ curboard.putwall(x, y)
+ curboard.reorder_walls()
+ for y in range(0, curboard.height-1):
+ yield None
+ for x in range(2, curboard.width-2):
+ if (y, x) in curboard.walls_by_pos:
+ curboard.killwall(x, y)
+ while tc.time != 0.0:
+ yield None
+
+# This game is suitable for at least min_players players
+min_players = 2
+
+def run():
+ global curboard, x_min, x_max, y_max
+ from boards import curboard
+ x_min = 2*CELL - 3
+ x_max = (curboard.width-2)*CELL - 36 + 3
+ y_max = (curboard.height-1)*CELL - 36 + 7
+ boards.replace_boardgen(Camel().bgen())
+
+def setup():
+ for key, (filename, rect) in localmap.items():
+ filename = os.path.join(LocalDir, filename)
+ if filename.find('%d') >= 0:
+ for p in BubPlayer.PlayerList:
+ images.sprmap[key + (p.pn,)] = (filename % p.pn, rect)
+ else:
+ images.sprmap[key] = (filename, rect)
+setup()
diff --git a/bubbob/ext7/fire.wav b/bubbob/ext7/fire.wav
new file mode 100644
index 0000000..c8531a3
--- /dev/null
+++ b/bubbob/ext7/fire.wav
Binary files differ
diff --git a/bubbob/ext7/hit.wav b/bubbob/ext7/hit.wav
new file mode 100644
index 0000000..36d497a
--- /dev/null
+++ b/bubbob/ext7/hit.wav
Binary files differ
diff --git a/bubbob/ext7/image1-0.ppm b/bubbob/ext7/image1-0.ppm
new file mode 100644
index 0000000..2df7b9b
--- /dev/null
+++ b/bubbob/ext7/image1-0.ppm
Binary files differ
diff --git a/bubbob/ext7/music.wav b/bubbob/ext7/music.wav
new file mode 100644
index 0000000..b45f018
--- /dev/null
+++ b/bubbob/ext7/music.wav
Binary files differ
diff --git a/bubbob/images.py b/bubbob/images.py
new file mode 100644
index 0000000..4de0b56
--- /dev/null
+++ b/bubbob/images.py
@@ -0,0 +1,542 @@
+from __future__ import generators
+import gamesrv, os
+from sprmap import sprmap as original_sprmap
+from patmap import patmap
+import mnstrmap
+import pixmap
+
+KEYCOL = 0x010101
+MAX = 10
+
+ActiveSprites = []
+SpritesByLoc = {}
+
+
+class ActiveSprite(gamesrv.Sprite):
+ touchable = 0
+ imgsetter = None
+ angry = []
+ priority = 0
+
+ def __init__(self, *args):
+ gamesrv.Sprite.__init__(self, *args)
+ if self.priority:
+ ActiveSprites.insert(0, self)
+ else:
+ ActiveSprites.append(self)
+ self.ranges = []
+ self.gen = []
+
+ def kill(self):
+ self.untouchable()
+ del self.gen[:]
+ ActiveSprites.remove(self)
+ gamesrv.Sprite.kill(self)
+
+ def untouchable(self):
+ self.touchable = 0
+ for key in self.ranges:
+ del key[self]
+ del self.ranges[:]
+
+ def play(self, snd, volume=0.8):
+ import boards
+ xmin = 2*boards.CELL
+ xmax = boards.bwidth-4*boards.CELL
+ snd.play(volume, pad=float(self.x-xmin)/(xmax-xmin))
+
+ def setimages(self, gen):
+ if self.imgsetter is not None:
+ try:
+ self.gen.remove(self.imgsetter)
+ except ValueError:
+ pass
+ self.imgsetter = gen
+ if gen is not None:
+ self.gen.append(gen)
+
+ def vertical_warp(self):
+ # short-cut this method to boards.py
+ import boards
+ ActiveSprite.vertical_warp = boards.vertical_warp_sprite
+ self.vertical_warp()
+## if moebius:
+## self.moebius()
+## return moebius
+## def moebius(self):
+## pass
+
+ # common generators
+ def cyclic(self, nimages, speed=5):
+ images = [sprget(n) for n in nimages]
+ speed = range(speed)
+ while 1:
+ for img in images:
+ self.seticon(img)
+ for i in speed:
+ yield None
+
+ def imgseq(self, nimages, speed=5, repeat=1):
+ images = [sprget(n) for n in nimages]
+ for r in range(repeat):
+ for img in images:
+ self.seticon(img)
+ for i in range(speed):
+ yield None
+
+ def die(self, nimages, speed=1):
+ for n in nimages:
+ if n is not None:
+ self.seticon(sprget(n))
+ for i in range(speed):
+ yield None
+ self.kill()
+
+ def straightline(self, dx, dy):
+ fx = self.x + 0.5
+ fy = self.y + 0.5
+ while 1:
+ fx += dx
+ fy += dy
+ self.move(int(fx), int(fy))
+ yield None
+
+ def parabolic(self, dxy, warp=0, gravity=0.3):
+ import boards
+ from boards import CELL
+ nx = self.x
+ ny = self.y
+ dx, dy = dxy
+ xmax = boards.bwidth - 2*CELL - self.ico.w
+ while ny < boards.bheight:
+ nx += dx
+ ny += dy
+ dy += gravity
+ if nx < 2*CELL:
+ nx = 2*CELL
+ dx = abs(dx)
+ elif nx >= xmax:
+ nx = xmax
+ dx = -abs(dx)
+ if warp and (ny < -2*CELL or ny >= boards.bheight):
+ nx, ny = boards.vertical_warp(nx, ny)
+## if moebius:
+## self.moebius()
+## dx = -dx
+ self.move(int(nx), int(ny))
+ dxy[:] = [dx, dy]
+ yield None
+
+ def following(self, other, dx=0, dy=0):
+ while other.alive:
+ self.move(other.x + dx, other.y + dy)
+ yield None
+ self.kill()
+
+ def touchdelay(self, delay):
+ for i in range(delay):
+ yield None
+ self.touchable = 1
+
+ def touching(self, margin=0):
+ return touching(self.x, self.y, self.ico.w, self.ico.h, margin)
+
+ def genangry(self):
+ # do one more step throught all generators of self.gen
+ while 1:
+ glist = self.gen[:]
+ try:
+ for g in glist:
+ if self.alive:
+ g.next()
+ except StopIteration:
+ try:
+ self.gen.remove(g)
+ except ValueError:
+ pass
+ for g in glist[glist.index(g)+1:]:
+ if self.alive:
+ try:
+ g.next()
+ except StopIteration:
+ pass
+ yield None
+
+def touching(x1, y1, w1, h1, margin=0):
+ touch = {}
+ x1 = int(x1)
+ y1 = int(y1)
+ xrange = range(x1>>5, (x1+w1+31)>>5)
+ for y in range(y1>>4, (y1+h1+15)>>4):
+ for x in xrange:
+ touch.update(SpritesByLoc.get((x,y), {}))
+ return [s for s in touch
+ if x1+margin < s.x+s.ico.w and y1+margin < s.y+s.ico.h and
+ s.x+margin < x1+w1 and s.y+margin < y1+h1]
+
+def action(sprlist, len=len):
+ # Main generator dispatch loop
+ for self in sprlist:
+ glist = self.gen + self.angry
+ try:
+ for g in glist:
+ if self.alive:
+ g.next()
+ except StopIteration:
+ try:
+ self.gen.remove(g)
+ except ValueError:
+ pass
+ for g in glist[glist.index(g)+1:]:
+ if self.alive:
+ try:
+ g.next()
+ except StopIteration:
+ pass
+ if self.touchable and self.alive:
+ # record position
+ x = self.x & -8
+ y = self.y & -8
+ if self.touchable != (x, y):
+ self.touchable = x, y
+ for key in self.ranges:
+ del key[self]
+ del self.ranges[:]
+ xrange = range(x>>5, (x+self.ico.w+38)>>5)
+ for y in range(y>>4, (y+self.ico.h+22)>>4):
+ for x in xrange:
+ key = SpritesByLoc.setdefault((x,y), {})
+ key[self] = 1
+ self.ranges.append(key)
+
+def sprget(n, spriconcache={}):
+ try:
+ return spriconcache[n]
+ except KeyError:
+ key = n
+ if isinstance(key, tuple) and key[0] in Transformations:
+ t, n = key
+ transform = Transformations[t]
+ else:
+ transform = transform_noflip
+ filename, rect = sprmap[n]
+ bitmap, rect = transform(filename, rect)
+ if isinstance(n, tuple):
+ n1 = n[0]
+ else:
+ n1 = n
+ if isinstance(n1, int):
+ n1 = n1 % 1000
+ alpha = transparency.get(n1, 255)
+ ico = bitmap.geticon(alpha=alpha, *rect)
+ spriconcache[key] = ico
+ return ico
+
+def transform_noflip(filename, rect):
+ bitmap = gamesrv.getbitmap(filename, KEYCOL)
+ return bitmap, rect
+
+def make_transform(datamap, ptmap):
+ def transform(filename, rect, datamap=datamap, ptmap=ptmap, cache={}):
+ try:
+ bitmap, width, height = cache[filename]
+ except KeyError:
+ f = open(filename, "rb")
+ data = f.read()
+ f.close()
+ width, height, data = pixmap.decodepixmap(data)
+ data = datamap(width, height, data)
+ dummy, dummy, nwidth, nheight = ptmap(0, 0, width, height)
+ data = pixmap.encodepixmap(nwidth, nheight, data)
+ bitmap = gamesrv.newbitmap(data, KEYCOL)
+ cache[filename] = bitmap, width, height
+ #print 'transformed', filename, 'to', nwidth, nheight
+ x, y, w, h = rect
+ x1, y1, dummy, dummy = ptmap(x, y, width, height)
+ x2, y2, dummy, dummy = ptmap(x+w, y+h, width, height)
+ rect = min(x1,x2), min(y1,y2), abs(x2-x1), abs(y2-y1)
+ #print filename, ':', (x,y,w,h), '->', rect
+ return bitmap, rect
+ return transform
+
+Transformations = {
+ '': transform_noflip,
+ 'vflip': make_transform(pixmap.vflip, lambda x,y,w,h: (x,h-y,w,h)),
+ 'hflip': make_transform(pixmap.hflip, lambda x,y,w,h: (w-x,y,w,h)),
+ 'cw': make_transform(pixmap.rotate_cw, lambda x,y,w,h: (h-y,x,h,w)),
+ 'ccw': make_transform(pixmap.rotate_ccw,lambda x,y,w,h: (y,w-x,h,w)),
+ 'rot180': make_transform(pixmap.rotate_180,lambda x,y,w,h: (w-x,h-y,w,h)),
+ }
+
+if 0: # disabled clipping
+ def sprget_subrect(n, subrect):
+ x, y, w, h = subrect
+ filename, (x0, y0, w0, h0) = sprmap[n]
+ key = (n, 'subrect', subrect)
+ sprmap[key] = filename, (x0+x, y0+y, w, h)
+ return sprget(key)
+
+def make_darker(ico, is_dragon, bmpcache={}):
+ bmp, rect = ico.getorigin()
+ try:
+ darkbmp = bmpcache[bmp, is_dragon]
+ except KeyError:
+ image = pixmap.decodepixmap(bmp.read())
+ if is_dragon:
+ translation = pixmap.translation_dragon
+ else:
+ translation = pixmap.translation_darker
+ darkimage = pixmap.make_dark(image, translation)
+ data = pixmap.encodepixmap(*darkimage)
+ darkbmp = gamesrv.newbitmap(data, bmp.colorkey)
+ bmpcache[bmp, is_dragon] = darkbmp
+ return darkbmp.geticon(*rect)
+
+def haspat(n):
+ return n in patmap
+
+def loadpattern(n, keycol=None):
+ if not haspat(n):
+ n = (n[0] % 100,) + n[1:]
+ filename, rect = patmap[n]
+ filename = os.path.join('tmp', filename)
+ bitmap = gamesrv.getbitmap(filename, keycol)
+ return bitmap, rect
+
+def makebkgndpattern(bitmap, (x,y,w,h), darker={}):
+ from boards import CELL
+ try:
+ nbitmap, hscale, vscale = darker[bitmap]
+ except KeyError:
+ data = bitmap.read()
+ width, height, data = pixmap.decodepixmap(data)
+ nwidth, nheight, data = pixmap.makebkgnd(width, height, data)
+ hscale = float(nwidth) / width
+ vscale = float(nheight) / height
+ data = pixmap.encodepixmap(nwidth, nheight, data)
+ nbitmap = gamesrv.newbitmap(data, None)
+ darker[bitmap] = nbitmap, hscale, vscale
+ x = int(x*hscale)
+ y = int(y*vscale)
+ w = int(CELL*hscale)
+ h = int(CELL*vscale)
+ return nbitmap, (x,y,w,h)
+
+def computebiggericon(ico, bigger={}):
+ try:
+ result, computing = bigger[ico]
+ except KeyError:
+ bigger[ico] = None, pixmap.imagezoomer(*ico.getimage())
+ return None
+ if computing is not None:
+ result = computing.next() or computing.next() or computing.next()
+ if not result:
+ return None # still computing
+ w, h, data = result
+ data = pixmap.encodepixmap(w, h, data)
+ result = gamesrv.newbitmap(data, KEYCOL).geticon(0, 0, w, h)
+ bigger[ico] = result, None
+ return result
+
+def biggericon(ico):
+ result = None
+ while result is None:
+ result = computebiggericon(ico)
+ return result
+
+extramap = {
+ 'shield-left': ('extra1.ppm', (0, 0, 32, 32)),
+ 'shield-right': ('extra1.ppm', (0, 32, 32, 32)),
+ 'moebius': ('extra1.ppm', (0, 64, 32, 32)),
+ 'flower': ('extra1.ppm', (0, 96, 32, 32)),
+ 'flower2': ('extra1.ppm', (0, 128, 32, 32)),
+ 'potion4': ('extra1.ppm', (0, 160, 32, 32)),
+ ('glasses', -1):('extra1.ppm', (0, 192, 32, 16)),
+ ('glasses', +1):('extra1.ppm', (0, 208, 32, 16)),
+ 'cactus': ('extra1.ppm', (0, 224, 32, 32)),
+ 'questionmark3':('extra2.ppm', (0, 0, 16, 16)),
+ 'questionmark1':('extra2.ppm', (0, 16, 16, 16)),
+ 'questionmark5':('extra2.ppm', (0, 32, 16, 16)),
+ 'questionmark2':('extra2.ppm', (0, 48, 16, 16)),
+ 'questionmark4':('extra2.ppm', (0, 64, 16, 16)),
+ 'percent': ('extra2.ppm', (0, 80, 16, 16)),
+ 'colon': ('extra2.ppm', (0, 96, 16, 16)),
+ 'gameoverbkgnd':('extra2.ppm', (0, 112, 16, 16)),
+ ('eyes', 0,0): ('extra3.ppm', (0, 0, 32, 32)),
+ ('eyes', 0,-1): ('extra3.ppm', (0, 32, 32, 32)),
+ ('eyes', -1,0): ('extra3.ppm', (0, 64, 32, 32)),
+ ('eyes', -1,-1):('extra3.ppm', (0, 96, 32, 32)),
+ ('eyes', 1,0): ('extra3.ppm', (0, 128, 32, 32)),
+ ('eyes', 1,-1): ('extra3.ppm', (0, 160, 32, 32)),
+ 'eyes-blink': ('extra3.ppm', (0, 192, 32, 32)),
+ ('smstar','blue' ,0): ('extra4.ppm', ( 0, 0, 16, 16)),
+ ('smstar','blue' ,1): ('extra4.ppm', ( 0, 16, 16, 16)),
+ ('smstar','yellow' ,0): ('extra4.ppm', ( 0, 32, 16, 16)),
+ ('smstar','yellow' ,1): ('extra4.ppm', ( 0, 48, 16, 16)),
+ ('smstar','red' ,0): ('extra4.ppm', (16, 0, 16, 16)),
+ ('smstar','red' ,1): ('extra4.ppm', (16, 16, 16, 16)),
+ ('smstar','green' ,0): ('extra4.ppm', (16, 32, 16, 16)),
+ ('smstar','green' ,1): ('extra4.ppm', (16, 48, 16, 16)),
+ ('smstar','magenta',0): ('extra4.ppm', (32, 0, 16, 16)),
+ ('smstar','magenta',1): ('extra4.ppm', (32, 16, 16, 16)),
+ ('smstar','cyan' ,0): ('extra4.ppm', (32, 32, 16, 16)),
+ ('smstar','cyan' ,1): ('extra4.ppm', (32, 48, 16, 16)),
+ ('starbub','blue' ,0): ('extra5.ppm', (0, 0, 32, 32)),
+ ('starbub','blue' ,1): ('extra5.ppm', (0, 32, 32, 32)),
+ ('starbub','blue' ,2): ('extra5.ppm', (0, 64, 32, 32)),
+ ('starbub','yellow' ,0): ('extra5.ppm', (0, 96, 32, 32)),
+ ('starbub','yellow' ,1): ('extra5.ppm', (0,128, 32, 32)),
+ ('starbub','yellow' ,2): ('extra5.ppm', (0,160, 32, 32)),
+ ('starbub','red' ,0): ('extra5.ppm', (0,192, 32, 32)),
+ ('starbub','red' ,1): ('extra5.ppm', (0,224, 32, 32)),
+ ('starbub','red' ,2): ('extra5.ppm', (0,256, 32, 32)),
+ ('starbub','green' ,0): ('extra5.ppm', (0,288, 32, 32)),
+ ('starbub','green' ,1): ('extra5.ppm', (0,320, 32, 32)),
+ ('starbub','green' ,2): ('extra5.ppm', (0,352, 32, 32)),
+ ('starbub','magenta',0): ('extra5.ppm', (0,384, 32, 32)),
+ ('starbub','magenta',1): ('extra5.ppm', (0,416, 32, 32)),
+ ('starbub','magenta',2): ('extra5.ppm', (0,448, 32, 32)),
+ ('starbub','cyan' ,0): ('extra5.ppm', (0,480, 32, 32)),
+ ('starbub','cyan' ,1): ('extra5.ppm', (0,512, 32, 32)),
+ ('starbub','cyan' ,2): ('extra5.ppm', (0,544, 32, 32)),
+ 'sheep-sm': ('extra6.ppm', (0, 0, 32, 32)),
+ 'sheep-big': ('extra6.ppm', (0, 32, 46, 50)),
+ ('emotic', 0): ('extra7.ppm', (0, 0, 8, 8)),
+ ('emotic', 1): ('extra7.ppm', (0, 8, 8, 8)),
+ ('emotic', 2): ('extra7.ppm', (0, 16, 8, 8)),
+ ('emotic', 3): ('extra7.ppm', (0, 24, 8, 8)),
+ ('emotic', 4): ('extra7.ppm', (0, 32, 8, 8)),
+ ('emotic', 5): ('extra7.ppm', (0, 40, 8, 8)),
+ ('emotic', 6): ('extra7.ppm', (0, 48, 8, 8)),
+ ('butterfly', 'jailed', 0): ('butterfly.ppm', (0, 0, 32, 32)),
+ ('butterfly', 'jailed', 1): ('butterfly.ppm', (0, 32, 32, 32)),
+ ('butterfly', 'jailed', 2): ('butterfly.ppm', (0, 64, 32, 32)),
+ ('butterfly', 'dead', 0): ('butterfly.ppm', (0, 96, 32, 32)),
+ ('butterfly', 'dead', 1): ('butterfly.ppm', (0, 128, 32, 32)),
+ ('butterfly', 'dead', 2): ('butterfly.ppm', (0, 160, 32, 32)),
+ ('butterfly', 'dead', 3): ('butterfly.ppm', (0, 192, 32, 32)),
+ ('butterfly', 'fly', 0): ('butterfly.ppm', (0, 224, 32, 32)),
+ ('butterfly', 'fly', 1): ('butterfly.ppm', (0, 256, 32, 32)),
+ 'glue': ('glue.ppm', (0, 0, 32, 32)),
+ 'black': ('black.ppm', (0, 0, 32, 32)),
+ ('sheep',-1, 0): ('sheep.ppm', (0, 0, 32, 32)),
+ ('sheep',-1, 1): ('sheep.ppm', (0, 32, 32, 32)),
+ ('sheep',-1, 2): ('sheep.ppm', (0, 64, 32, 32)),
+ ('sheep',-1, 3): ('sheep.ppm', (0, 96, 32, 32)),
+ ('sheep', 1, 0): ('sheep.ppm', (0, 128, 32, 32)),
+ ('sheep', 1, 1): ('sheep.ppm', (0, 160, 32, 32)),
+ ('sheep', 1, 2): ('sheep.ppm', (0, 192, 32, 32)),
+ ('sheep', 1, 3): ('sheep.ppm', (0, 224, 32, 32)),
+ ('sheep', 'a'): ('sheep.ppm', (2, 263, 7, 8)),
+ ('sheep', 'b'): ('sheep.ppm', (11, 262, 6, 10)),
+ ('sheep', 'c'): ('sheep.ppm', (17, 264, 11, 8)),
+ ('sheep', 'd'): ('sheep.ppm', (18, 272, 11, 7)),
+ ('sheep', 'e'): ('sheep.ppm', (18, 279, 11, 8)),
+ ('sheep', 'f'): ('sheep.ppm', (4, 273, 10, 12)),
+ ('sheep', 'g'): ('sheep.ppm', (19, 257, 11, 8)),
+ }
+hatmap = {
+ ('hat', 0, -1,1):('hat2.ppm',( 0, 0, 32, 48)),
+ ('hat', 0, -1,2):('hat2.ppm',( 32, 0, 32, 48)),
+ ('hat', 0, -1,3):('hat2.ppm',( 64, 0, 32, 48)),
+ ('hat', 0, 1,3):('hat2.ppm',( 96, 0, 32, 48)),
+ ('hat', 0, 1,2):('hat2.ppm',(128, 0, 32, 48)),
+ ('hat', 0, 1,1):('hat2.ppm',(160, 0, 32, 48)),
+ ('hat', 1, -1,1):('hat1.ppm',( 0, 0, 32, 48)),
+ ('hat', 1, -1,2):('hat1.ppm',( 32, 0, 32, 48)),
+ ('hat', 1, -1,3):('hat1.ppm',( 64, 0, 32, 48)),
+ ('hat', 1, 1,3):('hat1.ppm',( 96, 0, 32, 48)),
+ ('hat', 1, 1,2):('hat1.ppm',(128, 0, 32, 48)),
+ ('hat', 1, 1,1):('hat1.ppm',(160, 0, 32, 48)),
+ ('hat', 0) :('hat5.ppm',( 32, 0, 32, 48)),
+ ('hat', 1) :('hat5.ppm',( 0, 0, 32, 48)),
+ }
+
+def generate_sprmap():
+ # check and maybe regenerate the colored image files
+ file = os.path.join('images', 'buildcolors.py')
+ g = {'__name__': '__auto__', '__file__': file}
+ execfile(file, g)
+ # replace the entries 'filename_%d.ppm' by a family of entries,
+ # one for each color
+ sprmap = {}
+ for n, (filename, rect) in (original_sprmap.items() +
+ extramap.items() + hatmap.items()):
+ if filename.find('%d') >= 0:
+ for i in range(MAX):
+ sprmap[n+1000*i] = (os.path.join('images',filename % i), rect)
+ else:
+ sprmap[n] = (os.path.join('images', filename), rect)
+ return sprmap
+sprmap = generate_sprmap()
+
+transparency = {
+ mnstrmap.GreenAndBlue.new_bubbles[0][0]: 0xA0,
+ mnstrmap.GreenAndBlue.new_bubbles[0][1]: 0xB0,
+ mnstrmap.GreenAndBlue.new_bubbles[0][2]: 0xC0,
+ mnstrmap.GreenAndBlue.new_bubbles[0][3]: 0xD0,
+ mnstrmap.GreenAndBlue.normal_bubbles[0][0]: 0xE0,
+ mnstrmap.GreenAndBlue.normal_bubbles[0][1]: 0xE0,
+ mnstrmap.GreenAndBlue.normal_bubbles[0][2]: 0xE0,
+ mnstrmap.DyingBubble.first[0]: 0xD0,
+ mnstrmap.DyingBubble.first[1]: 0xD0,
+ mnstrmap.DyingBubble.first[2]: 0xD0,
+ mnstrmap.DyingBubble.medium[0]: 0xC0,
+ mnstrmap.DyingBubble.medium[1]: 0xC0,
+ mnstrmap.DyingBubble.medium[2]: 0xC0,
+ mnstrmap.DyingBubble.last[0]: 0xB0,
+ mnstrmap.DyingBubble.last[1]: 0xB0,
+ mnstrmap.DyingBubble.last[2]: 0xB0,
+ 'starbub': 0xE0,
+ }
+
+def sprcharacterget(c, filename=os.path.join('images', 'extra8.ppm')):
+ n = ord(c) - 32
+ if 0 <= n < 95:
+ return gamesrv.getbitmap(filename, KEYCOL).geticon(n*8, 0, 8, 15)
+ else:
+ return None
+
+def writestr(x, y, text):
+ result = []
+ for c in text:
+ ico = sprcharacterget(c)
+ if ico is not None:
+ result.append(gamesrv.Sprite(ico, x, y))
+ x += 7
+ return result
+
+def writestrlines(lines):
+ import boards
+ width = boards.bwidth + 9*boards.CELL
+ y = 50
+ for text in lines:
+ if text:
+ writestr((width - 7*len(text)) // 2, y, text)
+ y += 28
+ else:
+ y += 14
+
+
+
+def getsample(fn, freq):
+ return gamesrv.getsample(os.path.join('sounds', fn), freq)
+
+SoundList = ['Pop', 'Jump', 'Die', 'LetsGo', 'Extralife',
+ 'Fruit', 'Extra', 'Yippee', 'Hurry', 'Hell', 'Shh']
+
+class Snd:
+ pass
+
+def loadsounds(freqfactor=1):
+ for key in SoundList:
+ setattr(Snd, key, getsample(key.lower()+'.wav', freqfactor))
+
+loadsounds()
+music_intro = gamesrv.getmusic('music/Snd1-8.wav')
+music_game = gamesrv.getmusic('music/Snd2-8.wav')
+music_potion = gamesrv.getmusic('music/Snd3-8.wav')
+music_modern = gamesrv.getmusic('music/Snd4-8.wav')
+music_old = gamesrv.getmusic('music/Snd5-8.wav')
+music_game2 = gamesrv.getmusic('music/Snd6-8.wav')
+#gamesrv.set_musics([music_intro, music_game], 1)
diff --git a/bubbob/images/.cvsignore b/bubbob/images/.cvsignore
new file mode 100644
index 0000000..7a1612c
--- /dev/null
+++ b/bubbob/images/.cvsignore
@@ -0,0 +1,7 @@
+[1-7]0000_[2-9].ppm
+digits_[2-9].ppm
+dragon_[2-9].ppm
+dragon_bubble_[2-9].ppm
+game_over_[2-9].ppm
+point_[2-9].ppm
+fish_[2-9].ppm
diff --git a/bubbob/images/10000_0.ppm b/bubbob/images/10000_0.ppm
new file mode 100644
index 0000000..2915dc9
--- /dev/null
+++ b/bubbob/images/10000_0.ppm
Binary files differ
diff --git a/bubbob/images/20000_0.ppm b/bubbob/images/20000_0.ppm
new file mode 100644
index 0000000..d1abe4b
--- /dev/null
+++ b/bubbob/images/20000_0.ppm
Binary files differ
diff --git a/bubbob/images/30000_0.ppm b/bubbob/images/30000_0.ppm
new file mode 100644
index 0000000..9869ceb
--- /dev/null
+++ b/bubbob/images/30000_0.ppm
Binary files differ
diff --git a/bubbob/images/40000_0.ppm b/bubbob/images/40000_0.ppm
new file mode 100644
index 0000000..56b3fca
--- /dev/null
+++ b/bubbob/images/40000_0.ppm
Binary files differ
diff --git a/bubbob/images/50000_0.ppm b/bubbob/images/50000_0.ppm
new file mode 100644
index 0000000..765ab0f
--- /dev/null
+++ b/bubbob/images/50000_0.ppm
Binary files differ
diff --git a/bubbob/images/60000_0.ppm b/bubbob/images/60000_0.ppm
new file mode 100644
index 0000000..ed220b8
--- /dev/null
+++ b/bubbob/images/60000_0.ppm
Binary files differ
diff --git a/bubbob/images/70000_0.ppm b/bubbob/images/70000_0.ppm
new file mode 100644
index 0000000..575cd03
--- /dev/null
+++ b/bubbob/images/70000_0.ppm
Binary files differ
diff --git a/bubbob/images/big_bubble.ppm b/bubbob/images/big_bubble.ppm
new file mode 100644
index 0000000..f0e7b7b
--- /dev/null
+++ b/bubbob/images/big_bubble.ppm
@@ -0,0 +1,4 @@
+P6
+64 384
+255
+ÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÿÿÿÿÿÿÿÿÌÿÿÿÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÿÿÿÿÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÌÿÿÌÿÿÿÿÿÌÿÿÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ \ No newline at end of file
diff --git a/bubbob/images/big_bubble_2.ppm b/bubbob/images/big_bubble_2.ppm
new file mode 100644
index 0000000..8436f6e
--- /dev/null
+++ b/bubbob/images/big_bubble_2.ppm
@@ -0,0 +1,4 @@
+P6
+64 320
+255
+ŒŒŒÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ«««ÿÿÿÿÿÿÿÿÿ˜˜˜üüüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿèèèÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÓÓÓÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¸¸¸ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ{{{ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ===ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ===ÿÿÿÿÿÿ===ÿÿÿÿÿÿÄÄÄÿÿÿOOOÿÿÿÄÄÄÿÿÿOOOÿÿÿÄÄÄÿÿÿOOOÿÿÿÄÄÄÿÿÿOOOÿÿÿÄÄÄÿÿÿOOOÿÿÿÄÄÄÿÿÿOOOÿÿÿÄÄÄÿÿÿOOOÿÿÿÄÄÄÿÿÿÿÿÿÿÿÿÄÄÄ===ÿÿÿÿÿÿÄÄÄ===ÿÿÿÿÿÿÿÿÿ===ÿÿÿÿÿÿÿÿÿÚÚÚÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ{{{ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÚÚÚÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿŒŒŒÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¥¥¥ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¸¸¸ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¾¾¾ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿKKKÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÄÄÄÄÄÄÄÄÄÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÄÄÄÄÄÄ{{{{{{{{{{{{iiiÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ \ No newline at end of file
diff --git a/bubbob/images/black.ppm b/bubbob/images/black.ppm
new file mode 100644
index 0000000..c5adbe8
--- /dev/null
+++ b/bubbob/images/black.ppm
@@ -0,0 +1,5 @@
+P6
+# CREATOR: The GIMP's PNM Filter Version 1.0
+32 32
+255
+ \ No newline at end of file
diff --git a/bubbob/images/blitzy.ppm b/bubbob/images/blitzy.ppm
new file mode 100644
index 0000000..b2e0963
--- /dev/null
+++ b/bubbob/images/blitzy.ppm
Binary files differ
diff --git a/bubbob/images/blitzy_angry.ppm b/bubbob/images/blitzy_angry.ppm
new file mode 100644
index 0000000..464ce2b
--- /dev/null
+++ b/bubbob/images/blitzy_angry.ppm
Binary files differ
diff --git a/bubbob/images/blitzy_shot.ppm b/bubbob/images/blitzy_shot.ppm
new file mode 100644
index 0000000..378c956
--- /dev/null
+++ b/bubbob/images/blitzy_shot.ppm
Binary files differ
diff --git a/bubbob/images/bonus_0.ppm b/bubbob/images/bonus_0.ppm
new file mode 100644
index 0000000..4fa6e4c
--- /dev/null
+++ b/bubbob/images/bonus_0.ppm
Binary files differ
diff --git a/bubbob/images/bonus_1.ppm b/bubbob/images/bonus_1.ppm
new file mode 100644
index 0000000..82ddd25
--- /dev/null
+++ b/bubbob/images/bonus_1.ppm
Binary files differ
diff --git a/bubbob/images/bonus_10.ppm b/bubbob/images/bonus_10.ppm
new file mode 100644
index 0000000..9ffd660
--- /dev/null
+++ b/bubbob/images/bonus_10.ppm
Binary files differ
diff --git a/bubbob/images/bonus_11.ppm b/bubbob/images/bonus_11.ppm
new file mode 100644
index 0000000..1a5c509
--- /dev/null
+++ b/bubbob/images/bonus_11.ppm
Binary files differ
diff --git a/bubbob/images/bonus_12.ppm b/bubbob/images/bonus_12.ppm
new file mode 100644
index 0000000..354a1fe
--- /dev/null
+++ b/bubbob/images/bonus_12.ppm
Binary files differ
diff --git a/bubbob/images/bonus_2.ppm b/bubbob/images/bonus_2.ppm
new file mode 100644
index 0000000..61588b0
--- /dev/null
+++ b/bubbob/images/bonus_2.ppm
Binary files differ
diff --git a/bubbob/images/bonus_3.ppm b/bubbob/images/bonus_3.ppm
new file mode 100644
index 0000000..dd1ff6d
--- /dev/null
+++ b/bubbob/images/bonus_3.ppm
Binary files differ
diff --git a/bubbob/images/bonus_4.ppm b/bubbob/images/bonus_4.ppm
new file mode 100644
index 0000000..c85e926
--- /dev/null
+++ b/bubbob/images/bonus_4.ppm
Binary files differ
diff --git a/bubbob/images/bonus_5.ppm b/bubbob/images/bonus_5.ppm
new file mode 100644
index 0000000..57bd455
--- /dev/null
+++ b/bubbob/images/bonus_5.ppm
Binary files differ
diff --git a/bubbob/images/bonus_6.ppm b/bubbob/images/bonus_6.ppm
new file mode 100644
index 0000000..2ac4a64
--- /dev/null
+++ b/bubbob/images/bonus_6.ppm
Binary files differ
diff --git a/bubbob/images/bonus_7.ppm b/bubbob/images/bonus_7.ppm
new file mode 100644
index 0000000..5de3eaa
--- /dev/null
+++ b/bubbob/images/bonus_7.ppm
Binary files differ
diff --git a/bubbob/images/bonus_8.ppm b/bubbob/images/bonus_8.ppm
new file mode 100644
index 0000000..b1c6c2a
--- /dev/null
+++ b/bubbob/images/bonus_8.ppm
Binary files differ
diff --git a/bubbob/images/bonus_9.ppm b/bubbob/images/bonus_9.ppm
new file mode 100644
index 0000000..8d51608
--- /dev/null
+++ b/bubbob/images/bonus_9.ppm
Binary files differ
diff --git a/bubbob/images/bubble.ppm b/bubbob/images/bubble.ppm
new file mode 100644
index 0000000..7c6af5e
--- /dev/null
+++ b/bubbob/images/bubble.ppm
@@ -0,0 +1,5 @@
+P6
+# CREATOR: The GIMP's PNM Filter Version 1.1
+32 448
+255
+ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ™ff¤lh jgœgf]V‡VI‡VI‡VI­qió›xù°ˆý¾“þ×þ–ü½’ø®‡õ¡}ás¯rj[Sásü»ÿؤÿãªÿãªÿߨÿؤÿÌœÿÆ™ü½’ø®‡î–vÃ~n‘_ZÐ…pÿÕ¢ÿ÷¶ÿü¹ÿü¹ÿ÷¶ÿð²ÿà¨ÿÓ ÿÌœÿÇ™ýÁ•ù²Šî–vºxl‘_ZÀ|mÿá©ÿÿãÿÿÿÿÿÿÿÿÖÿû¸ÿí°ÿݧÿÕ¢ÿПÿËœþ×ü»ø®‡ë”u½zl‘_Z jgÿПÿÿñÿÿÿÿÿÿÿÿÿÿÿÈÿ÷¶ÿí°ÿãªÿÙ¤ÿÔ¡ÿÏžÿÇ™ýÁ•û¹÷¨‚ãt¸wk]V[S÷ªƒÿû¸ÿÿÿÿÿÿÿÿÿÿÿÿÿýºÿ÷¶ÿí°ÿå«ÿݧÿÕ¢ÿПÿËœþ×ü½’ù°ˆóœyÞs¯rj‹ZOãtÿð²ÿÿÿÿÿÿÿÿÿÿÿÿÿÿãÿü¹ÿõµÿí°ÿç¬ÿߨÿÕ¢ÿПÿËœÿÆ™ý¾“ú³‹ö¥€ë”uË‚o–db؉qÿá©ÿÿñÿÿÿÿÿÿÿÿÿÿÿãÿÿ»ÿù·ÿô´ÿí°ÿå«ÿߨÿؤÿПÿËœÿÆ™ý¾“ú¶ö¦î–v؉q¯rj‹ZOù°ˆÿýºÿÿÿÿÿÿÿÿÿÿÿñÿÿÈÿü¹ÿõµÿð²ÿé®ÿä«ÿݧÿÕ¢ÿПÿËœÿÆ™ý¾“ú¶÷¨‚ó™wÞsÀ|m’`[Ë‚oÿПÿÿÈÿÿÿÿÿÿÿÿÿÿÿÈÿýºÿù·ÿó³ÿí°ÿç¬ÿá©ÿÛ¥ÿÔ¡ÿÏžÿÉ›þ×ü½’ú³‹÷¨‚ó™wás΄p¤lh‰XLÅnÿÙ¤ÿÿÈÿÿÿÿÿÿÿÿãÿýºÿù·ÿõµÿð²ÿë¯ÿä«ÿߨÿÙ¤ÿÓ ÿÌœÿÇ™þ–û¹ù²Š÷¨‚ó›xãtÐ…p¸wk‰XLÀ|mÿߨÿÿ»ÿÿÿÿÿÿÿÿ»ÿû¸ÿ÷¶ÿñ³ÿí°ÿç¬ÿà¨ÿÛ¥ÿÕ¢ÿÏžÿÉ›þ×ý¾“û·ù°ˆ÷¨‚óœyæ‘tÓ‡qÀ|m‹ZO¸wkÿݧÿü¹ÿÿãÿýºÿû¸ÿ÷¶ÿñ³ÿí°ÿè­ÿãªÿݧÿؤÿÓ ÿÌœÿÇ™þ–ü½’ú¶ø®‡ö¦óœyæ‘tÖˆqÈ€n[S²tjÿÓ ÿ÷¶ÿýºÿø·ÿõµÿñ³ÿí°ÿè­ÿä«ÿà¨ÿÙ¤ÿÔ¡ÿÏžÿËœÿÆ™ýÁ•ü»ú³‹ø¬…ö¥€ó›xæ‘t؉q΄p]V¯rjÿÉ›ÿñ³ÿ÷¶ÿó³ÿð²ÿì¯ÿç¬ÿãªÿà¨ÿÛ¥ÿÕ¢ÿПÿÌœÿÇ™þ×ý¾“û¹ù°ˆ÷¨‚õ¡}ó™wæ‘tãtÖˆq–dbªoiü½’ÿç¬ÿï±ÿè­ÿç¬ÿãªÿà¨ÿݧÿÙ¤ÿÕ¢ÿПÿËœÿÇ™þ×ýÁ•ü»ú³‹÷ªƒõ¢~ó›xë”uãtë”u؉q™ff¤lhø®‡ÿÓ ÿݧÿÛ¥ÿݧÿÛ¥ÿؤÿÔ¡ÿÓ ÿÏžÿÉ›ÿÆ™þ×ýÁ•ü»ú¶ø¬…ö¥€óœyî–vÞsé“uó™wÐ…p]Vœgfõ¡}ý¾“ÿÉ›ÿÏžÿÓ ÿÔ¡ÿÓ ÿÏžÿËœÿÇ™þ×ýÁ•ý¾“ü»û·ù°ˆ÷¨‚ôŸ{ï—vãtãtõ¡}ôŸ{Ã~n‰XL–dbæ‘tú³‹ü½’þ–ÿÇ™ÿËœÿÌœÿÉ›ÿÆ™þ–ü½’ü»û¹û·ù²Š÷ªƒõ¢~ó™wæ‘té“uö¥€ù²Šóœyµvk‰XLÅnö¦ú³‹û¹ý¾“þ–þ×ýÁ•ü½’û¹ú¶ú³‹ù²Šù°ˆ÷ªƒö¥€óœyë”uë”uö¥€ü»ú¶ás–db­qië”u÷¨‚ø®‡ú³‹û·û¹û·ú¶ù²Šø®‡ø¬…÷ªƒ÷¨‚õ¢~ó›xë”uë”uõ¢~û¹ÿÆ™ø®‡Ã~n‹ZOÀ|mï—vö¥€÷ªƒø¬…ø®‡ø®‡ø®‡ø¬…÷¨‚ö¥€õ¢~ôŸ{ó™wé“uæ‘tôŸ{û·þ×ü»ë”uŸig’`[È€nï—vôŸ{õ¡}õ¢~õ¢~ö¥€ö¥€õ¡}ôŸ{ó™wï—vé“ué“uóœyù²ŠýÁ•û¹óœyºxl‹ZO–dbË‚oé“uë”uî–vï—vó™wó™wï—vî–vé“uæ‘të”uóœyø¬…û¹û·ôŸ{Ån‹ZOœgfÅnÖˆqÛ‹rásé“ué“uæ‘tæ‘té“uë”uóœy÷¨‚ù²Šø®‡ó›xË‚o’`[™ffºxlË‚o؉qæ‘tæ‘tæ‘té“uï—vó›xõ¡}ö¥€õ¢~ãtºxl’`[”b_¯rjÀ|mÐ…pÞsãtæ‘të”ué“uæ‘tÞsË‚oªoi‰XL‰XL”b_µvkË‚oÓ‡qÓ‡qÓ‡qË‚o½zl jg‘_Z‡VI‡VI‡VI‡VI‡VI‡VI‡VI‡VIvF,‡VI‡VI‡VI„SC‡VI‡VI‡VI‡VIœgfÐ…p÷ªƒü½’ÿÆ™ÿÇ™ÿÆ™ü»÷ªƒó›xÀ|m]Vœgf؉qü½’ÿߨÿÛ¥ÿݧÿÕ¢ÿÔ¡ÿПÿËœÿÇ™ýÁ•ù°ˆë”uŸig‡VI‡VIø®‡ÿù·ÿÿ»ÿõµÿó³ÿï±ÿãªÿÛ¥ÿÕ¢ÿПÿÌœÿÇ™ýÁ•ú³‹æ‘t½zl‡VI‡VIü½’ÿÿÖÿÿÿÿÿÿÿÿãÿü¹ÿô´ÿë¯ÿá©ÿÙ¤ÿÔ¡ÿПÿËœþ–ü»ú³‹ë”uÅn‡VI”b_÷¨‚ÿÿñÿÿÿÿÿÿÿÿÿÿÿÿÿýºÿø·ÿï±ÿå«ÿߨÿؤÿÓ ÿÏžÿÇ™ýÁ•ü½’÷¨‚é“uÓ‡q‡VI…TEï—vÿù·ÿÿãÿÿÿÿÿÿÿÿÿÿÿãÿü¹ÿ÷¶ÿð²ÿé®ÿà¨ÿÙ¤ÿÓ ÿÏžÿÉ›þ×ý¾“ø®‡ôŸ{é“uÀ|m‡VIásÿé®ÿÿÿÿÿÿÿÿÿÿÿÿÿÿñÿÿ»ÿû¸ÿõµÿï±ÿè­ÿá©ÿÙ¤ÿÔ¡ÿÏžÿÉ›þ×ý¾“ú³‹ö¥€ë”u؉q¤lh¤lhÿð²ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÈÿýºÿõµÿó³ÿí°ÿç¬ÿá©ÿÙ¤ÿÔ¡ÿÏžÿÉ›þ×ü½’ú³‹ö¦ï—vásÀ|m‡VIù°ˆÿø·ÿÿÿÿÿÿÿÿÿÿÿãÿÿÈÿýºÿù·ÿô´ÿï±ÿé®ÿä«ÿߨÿؤÿÓ ÿÌœÿÉ›þ×ü½’ú³‹ö¦ï—vãtÓ‡q¤lh‡VIÿÓ ÿø·ÿÿÿÿÿÿÿÿÿÿÿÈÿýºÿû¸ÿõµÿñ³ÿì¯ÿç¬ÿá©ÿݧÿÕ¢ÿПÿËœÿÇ™þ–û¹ù²Šö¦ó›xãtÓ‡q½zl‡VI‡VIÿߨÿ÷¶ÿÿÿÿÿÿÿÿñÿýºÿû¸ÿõµÿó³ÿí°ÿè­ÿãªÿߨÿÙ¤ÿÔ¡ÿÏžÿÉ›þ×ý¾“û·ù°ˆ÷¨‚ó›xæ‘tÓ‡q΄p‡VI‡VIÿߨÿô´ÿÿãÿÿ»ÿü¹ÿû¸ÿ÷¶ÿñ³ÿí°ÿé®ÿå«ÿà¨ÿÛ¥ÿÕ¢ÿПÿËœÿÆ™þ–ü½’ú¶ø®‡÷¨‚ó›xé“uÓ‡q؉q‡VI‡VIÿÕ¢ÿí°ÿ÷¶ÿù·ÿ÷¶ÿõµÿð²ÿí°ÿé®ÿå«ÿá©ÿݧÿؤÿÓ ÿÏžÿÉ›þ×ýÁ•ü»ú³‹ø¬…ö¦ó›xë”uÓ‡qÞs‡VI‡VIÿÌœÿå«ÿð²ÿô´ÿó³ÿï±ÿë¯ÿç¬ÿãªÿà¨ÿݧÿؤÿÔ¡ÿПÿËœÿÆ™þ–ü½’û·ø®‡÷¨‚õ¢~ï—vé“uæ‘tæ‘t™ff‡VIü»ÿÙ¤ÿä«ÿè­ÿé®ÿç¬ÿãªÿà¨ÿݧÿÙ¤ÿÕ¢ÿÓ ÿÏžÿËœÿÇ™þ×ý¾“û¹ù°ˆ÷¨‚õ¢~óœyë”uÞsó›xé“u–db‡VI÷¨‚ÿÌœÿÕ¢ÿÙ¤ÿݧÿݧÿÛ¥ÿؤÿÕ¢ÿÓ ÿÏžÿËœÿÉ›ÿÆ™þ–ý¾“û¹ù²Š÷ªƒõ¢~óœyî–vÓ‡qó™wõ¡}Û‹r‡VI‡VIó›xû¹þ×ÿËœÿÏžÿÓ ÿÕ¢ÿÓ ÿÏžÿËœÿÇ™þ×þ–ýÁ•ü½’ü»ú³‹ø¬…ö¥€ó›xï—vÓ‡qî–vù°ˆ÷¨‚Ån‡VIÖˆqù²Šü»ý¾“þ×ÿÇ™ÿÌœÿÏžÿÇ™þ×ýÁ•ü½’ü»ü»û¹ú³‹ø®‡ö¦ôŸ{ï—v؉qôŸ{û·ý¾“ôŸ{¯rj‘_Zõ¢~ù°ˆú³‹û¹ü½’ýÁ•þ–ý¾“ü½’û·ú¶ú³‹ù²Šù°ˆø¬…ö¦õ¢~ï—væ‘tóœyû¹ÿÌœû·Ó‡q‡VIÓ‡qõ¢~÷ªƒø®‡ù²Šú³‹ú¶ú¶ú³‹ù°ˆø®‡÷ªƒ÷¨‚ö¦õ¢~óœyæ‘tÞsõ¡}ü»ÿÌœÿÌœö¥€¯rj‡VIÖˆqôŸ{ö¥€÷¨‚÷¨‚÷¨‚÷ªƒ÷ªƒ÷ªƒ÷¨‚ö¥€õ¡}ôŸ{ó™wæ‘tæ‘tóœyû¹ÿÌœÿÇ™÷¨‚Ë‚o‡VI‡VI؉qó™wó™wó›xó›xõ¡}õ¡}õ¡}óœyóœyï—vé“uæ‘të”uõ¡}û·ÿÆ™ýÁ•ôŸ{Û‹r‡VI‡VIÓ‡qÞsÞsãtãtë”uë”ué“ué“uæ‘tæ‘tó›xõ¢~ú³‹ü½’ú¶õ¢~Þs‡VI‡VIºxlÓ‡qÓ‡qæ‘tó›xé“uæ‘té“uó™wóœy÷¨‚ø¬…ù²Š÷¨‚ë”u½zl‡VI‡VIªoiºxlÓ‡qÛ‹rë”uë”uï—vó›xó™wó™wó›xæ‘tÓ‡q‡VI‡VI‡VI™ff²tjË‚oÛ‹rÓ‡qÓ‡qÓ‡qºxlªoi‡VI‡VI‡VI‡VI‡VI‡VI‡VI‡VI‡VI‡VI‡VI‡VI‡VI‡VI‡VI‡VI‡VI‡VIºxlÖˆqõ¢~û¹ýÁ•ÿÇ™ÿÆ™þ×û¹ø¬…õ¡}Þsµvk’`[À|mó™wÿÇ™ÿð²ÿõµÿ÷¶ÿó³ÿë¯ÿãªÿݧÿؤÿÏžÿÇ™ü»ôŸ{΄pŸig[Sªoiû·ÿø·ÿÿÿÿÿÿÿÿÿÿÿãÿû¸ÿó³ÿé®ÿãªÿߨÿؤÿÓ ÿËœü»ö¥€Þs²tj‘_Z²tjÿËœÿÿÈÿÿÿÿÿÿÿÿÿÿÿÿÿÿÈÿø·ÿí°ÿå«ÿߨÿÕ¢ÿÏžÿÇ™þ×ý¾“û·ö¦ãt½zl–dbµvkýÁ•ÿü¹ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýºÿõµÿí°ÿè­ÿà¨ÿؤÿПÿÌœÿÇ™þ–ü½’ù²Šõ¡}é“uÅn–db²tjû·ÿü¹ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ»ÿù·ÿô´ÿí°ÿç¬ÿà¨ÿÙ¤ÿÓ ÿÏžÿÉ›þ×ý¾“ú¶÷¨‚ó™wÞsºxl‘_Z²tjÿÉ›ÿü¹ÿÿÿÿÿÿÿÿÿÿÿÿÿÿñÿÿÈÿü¹ÿõµÿñ³ÿì¯ÿå«ÿà¨ÿؤÿÓ ÿÏžÿÉ›þ×ý¾“û·ø¬…óœyæ‘tÐ…p­qi‰XLé“uÿí°ÿÿÿÿÿÿÿÿÿÿÿÿÿÿãÿÿÈÿü¹ÿø·ÿó³ÿí°ÿè­ÿãªÿߨÿÕ¢ÿПÿÌœÿÇ™þ×ü½’ú¶ø¬…ôŸ{ë”uÛ‹rÀ|m–db–dbü»ÿõµÿÿÿÿÿÿÿÿÿÿÿÿÿÿ»ÿü¹ÿù·ÿô´ÿð²ÿë¯ÿä«ÿà¨ÿÛ¥ÿÔ¡ÿÏžÿËœÿÆ™þ–ü»ú³‹÷ªƒõ¡}ë”uÞsË‚o¯rj[S–dbþ×ÿù·ÿÿÿÿÿÿÿÿÿÿÿÈÿü¹ÿø·ÿô´ÿð²ÿì¯ÿç¬ÿá©ÿݧÿؤÿÓ ÿÌœÿÉ›þ×ý¾“û¹ù²Š÷ªƒõ¡}î–vÞsÐ…pºxl[S–dbþ×ÿÿ»ÿÿÿÿÿÿÿýºÿû¸ÿø·ÿó³ÿï±ÿë¯ÿç¬ÿãªÿݧÿؤÿÔ¡ÿÏžÿÉ›þ×ýÁ•ü½’ú¶ù°ˆ÷¨‚õ¡}ï—vásÓ‡qÃ~nœgf”b_ü»ÿü¹ÿÿÿÿÿ»ÿ÷¶ÿô´ÿñ³ÿí°ÿé®ÿå«ÿá©ÿߨÿÙ¤ÿÔ¡ÿПÿËœÿÇ™þ–ý¾“û¹ú³‹ø¬…ö¦ôŸ{î–vás؉qÈ€nœgf”b_ù²Šÿ÷¶ÿÿ»ÿø·ÿð²ÿí°ÿé®ÿç¬ÿãªÿà¨ÿݧÿÙ¤ÿÔ¡ÿПÿÌœÿÇ™þ×ýÁ•ü»ú¶ø®‡÷¨‚õ¢~ó›xé“uãtæ‘tÓ‡qœgf‘_Zõ¡}ÿç¬ÿù·ÿë¯ÿä«ÿãªÿá©ÿߨÿÛ¥ÿؤÿÔ¡ÿПÿÌœÿËœÿÇ™þ×ýÁ•ü½’ú¶ù°ˆ÷¨‚õ¡}ó›xë”uásé“uî–vÐ…pœgf[Së”uÿÏžÿè­ÿПÿÔ¡ÿؤÿؤÿÕ¢ÿÓ ÿПÿÌœÿÉ›ÿÆ™þ×þ–ý¾“ü»ú¶ù°ˆ÷¨‚õ¡}ó›xë”uÞsæ‘tôŸ{ó™wÅn]V–dbÛ‹rû·ÿÇ™þ–ÿÆ™ÿËœÿÌœÿÏžÿÌœÿÉ›ÿÆ™þ–ý¾“ü½’ü½’û¹ú¶ù°ˆ÷ªƒõ¢~ó›xë”uãtî–vö¦ø®‡ï—vºxl]V­qiôŸ{ú³‹ú¶û¹ý¾“þ–þ×þ×ýÁ•ü½’û¹ú¶ú¶ú³‹ù²Šø®‡÷¨‚õ¢~ó›xë”ué“uó›xù²Šü½’ø¬…Öˆqœgf]V؉qóœy÷ªƒø®‡ú³‹ú¶û·û·û·ú³‹ù°ˆø®‡ø¬…÷ªƒ÷¨‚ö¥€ôŸ{ó™wé“uë”uõ¡}ú¶þ×ý¾“ó›x½zl‰XL™ff΄pó™wõ¢~÷¨‚÷ªƒ÷ªƒ÷ªƒø¬…÷ªƒ÷¨‚ö¦õ¢~õ¡}ôŸ{ó™wë”uæ‘të”uõ¢~û·þ×ü½’ö¥€È€n’`[”b_Ã~né“uó™wó›xó›xóœyõ¡}õ¡}ôŸ{óœyó›xï—vë”ué“ué“uï—vö¥€û·ý¾“ú¶ôŸ{È€n”b_‘_ZºxlÖˆqÞsásãté“uî–vë”ué“ué“uæ‘tæ‘tî–vóœyö¦ù²Šú¶ø¬…ï—vÀ|m’`[[S­qiÀ|m΄pÖˆqãtë”uæ‘tæ‘té“uë”uï—vôŸ{ö¥€÷¨‚÷¨‚ó™wÓ‡q­qi]V‰XL”b_ªoiºxlË‚o؉qásæ‘té“uë”uë”uë”ué“uÞsÐ…pªoi[S‡VI‰XL]V jg½zlÐ…pÖˆqÖˆqÖˆqÐ…pÃ~nµvk’`[‹ZO‡VI‡VI‡VI–dbŸig–db]V‡VI‡VIY33_66^55[44R..L++L++L++d99ŒPPŸXX±^^¸``µ__¯]]XX‘RR€IIe99Q..€II¬\\ÕffõffõffçffÕffÃccº``¯]]XXˆMMp??S//wDDÐffÿ~~ÿÿŽŽÿ~~ÿqqìffÉeeÃcc¼aa³^^¡YYˆMMk==S//n??ñffÿ­­ÿÆÆÿ½½ÿ©©ÿ‰‰ÿmmãffÐffÇddÀbb¸``¬\\XX†LLm>>S//^55Çddÿ²²ÿææÿëëÿÏÏÿ££ÿ||ÿmmõffÙffÌffÅdd¼aa³^^ª\\—VV‚JJj<<R..Q..™WWÿ‡‡ÿÍÍÿððÿééÿÀÀÿ––ÿ~~ÿnnÿffãffÐffÇddÀbb¸``¯]]ŸXXŽQQHHe99O--‚JJÿqqÿ¶¶ÿÍÍÿ××ÿÌÌÿªªÿŽŽÿ{{ÿnnÿggçffÐffÇddÀbbº``±^^¤ZZ”TT†LLtBBX22|FFñffÿ±±ÿååÿÖÖÿ¿¿ÿ®®ÿ™™ÿ„„ÿwwÿmmÿffçffÕffÇddÀbbº``±^^¦ZZ•UUˆMM|FFe99O--ŸXXÿ‘‘ÿßßÿööÿÔÔÿ²²ÿ  ÿÿ{{ÿqqÿiiúffãffÐffÇddÀbbº``±^^¦ZZ—VV‹OOHHn??U00tBBÇddÿ¡¡ÿêêÿêêÿÁÁÿ¡¡ÿ’’ÿƒƒÿvvÿmmÿggñffÞffÌffÅdd¾bb¸``¯]]¤ZZ—VV‹OO€IIvCC_66N,,q@@Ùffÿ¡¡ÿääÿØØÿ®®ÿ••ÿ……ÿ{{ÿqqÿkkúffçffÙffÉeeÃcc¼aaµ__ª\\¡YY—VVŒPP‚JJwDDj<<N,,n??çffÿ™™ÿÌÌÿ½½ÿœœÿˆˆÿ||ÿttÿmmÿggìffÞffÐffÅdd¾bb¸``±^^¨[[ŸXX—VVŽQQƒKKyEEn??O--j<<ãffÿŒŒÿ¬¬ÿ““ÿ‡‡ÿ~~ÿttÿmmÿhhõffãffÕffÉeeÃcc¼aaµ__¯]]¦ZZXX•UUŽQQƒKKzEEsAAQ..g::Éeeÿ||ÿ’’ÿÿzzÿttÿmmÿhhúffìffÙffÌffÅddÀbbº``³^^¬\\¤ZZ›WW”TTŒPPƒKK|FFvCCR..e99¾bbÿttÿ||ÿuuÿqqÿllÿggõffìffÞffÐffÇddÃcc¼aa¸``±^^ª\\ŸXX—VV‘RR‹OOƒKK‚JJzEEX22b88¯]]ÿggÿppÿhhÿggõffìffãffÙffÐffÇddÀbb¼aa¸``³^^¬\\¤ZZ™WW’SSŒPP†LL‚JJ†LL|FFY33_66XXÉeeãffÞffãffÞffÕffÌffÉeeÅdd¾bbº``¸``³^^¬\\¦ZZ›WW”TTŽQQˆMMHH…KK‹OOwDDR..[44‘RR±^^¾bbÅddÉeeÌffÉeeÅddÀbb¼aa¸``³^^±^^¬\\¨[[ŸXX—VVQQ‰NN‚JJ‚JJ‘RRQQp??N,,X22ƒKK¤ZZ¯]]µ__¼aaÀbbÃcc¾bbº``µ__¯]]¬\\ª\\¨[[¡YY™WW’SS‹OOƒKK…KK”TT¡YYŽQQh;;N,,q@@•UU¤ZZª\\±^^µ__¸``³^^¯]]ª\\¦ZZ¤ZZ¡YYŸXX™WW”TTŽQQ†LL†LL”TT¬\\¦ZZ€IIX22d99†LL—VVXX¤ZZ¨[[ª\\¨[[¦ZZ¡YYXX›WW™WW—VV’SSŒPP†LL†LL’SSª\\º``XXp??O--n??‰NN”TT™WW›WWXXXXXX›WW—VV”TT’SSQQ‹OO…KKƒKKQQ¨[[¸``¬\\†LL\44U00sAA‰NNQQ‘RR’SS’SS”TT”TT‘RRQQ‹OO‰NN…KK…KKŽQQ¡YY³^^ª\\ŽQQk==O--X22tBB…KK†LLˆMM‰NN‹OO‹OO‰NNˆMM…KKƒKK†LLŽQQ›WWª\\¨[[QQq@@O--[44q@@zEE}GG€II…KK…KKƒKKƒKK…KK†LLŽQQ—VV¡YYXXŒPPtBBU00Y33k==tBB|FFƒKKƒKKƒKK…KK‰NNŒPP‘RR”TT’SS‚JJk==U00V11e99n??wDDHH‚JJƒKK†LL…KKƒKKHHtBBb88N,,N,,V11h;;tBByEEyEEyEEtBBm>>^55S//L++L++L++L++L++L++L++L++?##L++L++L++I))L++L++L++L++[44wDD™WW¯]]º``¼aaº``¬\\™WWŒPPn??R..[44|FF¯]]çffÞffãffÐffÌffÇddÀbb¼aa³^^ŸXX†LL\44L++L++XXÿ„„ÿ——ÿzzÿuuÿppõffÞffÐffÇddÃcc¼aa³^^¤ZZƒKKm>>L++L++¯]]ÿ¥¥ÿääÿääÿªªÿŒŒÿyyÿkkñffÙffÌffÇddÀbbµ__¬\\¤ZZ†LLq@@L++V11—VVÿ³³ÿòòÿÿÿÿôôÿÅÅÿ‘‘ÿÿppÿffçffÕffÉeeÅdd¼aa³^^¯]]—VV…KKyEEL++K**‰NNÿ„„ÿªªÿääÿùùÿÝÝÿ®®ÿÿ~~ÿqqÿiiìffÙffÉeeÅdd¾bb¸``±^^XXQQ…KKn??L++€IIÿiiÿÉÉÿääÿÃÃÿÈÈÿ±±ÿ››ÿ‡‡ÿzzÿppÿhhñffÙffÌffÅdd¾bb¸``±^^¤ZZ”TT†LL|FF_66_66ÿrrÿÁÁÿÿÿÿÿÿÿÃÃÿ··ÿŸŸÿ‘‘ÿ{{ÿuuÿmmÿggñffÙffÌffÅdd¾bb¸``¯]]¤ZZ•UU‰NN€IIn??L++ŸXXÿÿææÿýýÿèèÿ¯¯ÿ¤¤ÿ““ÿƒƒÿwwÿppÿiiúffçffÕffÉeeÃcc¾bb¸``¯]]¤ZZ•UU‰NN‚JJyEE_66L++ÉeeÿÿííÿôôÿÈÈÿ¤¤ÿ““ÿˆˆÿ{{ÿttÿllÿggñffãffÐffÇddÀbb¼aaµ__ª\\¡YY•UUŒPP‚JJyEEm>>L++L++çffÿ~~ÿ××ÿââÿµµÿ““ÿˆˆÿzzÿuuÿnnÿhhõffçffÙffÌffÅdd¾bb¸``±^^¨[[ŸXX—VVŒPPƒKKyEEvCCL++L++çffÿyyÿ®®ÿ››ÿŽŽÿ‡‡ÿ||ÿttÿnnÿiiÿffìffÞffÐffÇddÀbbº``µ__¯]]¦ZZXX—VVŒPP…KKyEE|FFL++L++Ðffÿmmÿ||ÿƒƒÿ~~ÿzzÿrrÿmmÿiiÿffñffãffÕffÉeeÅdd¾bb¸``³^^¬\\¤ZZ›WW•UUŒPP†LLyEEHHL++L++ÃccÿffÿqqÿyyÿuuÿppÿkkÿggõffìffãffÕffÌffÇddÀbbº``µ__¯]]¨[[XX—VV’SS‰NN…KKƒKKƒKKY33L++¬\\ÙffúffÿhhÿiiÿggõffìffãffÙffÐffÉeeÅddÀbb¼aa¸``±^^ª\\ŸXX—VV’SSŽQQ†LLHHŒPP…KKX22L++—VVÃccÐffÙffãffãffÞffÕffÐffÉeeÅddÀbb¾bbº``µ__±^^ª\\¡YY™WW’SSŽQQˆMMyEE‹OO‘RR}GGL++L++ŒPPª\\¸``ÀbbÅddÉeeÐffÉeeÅddÀbb¼aa¸``µ__³^^¯]]¬\\¤ZZ›WW”TTŒPP‰NNyEEˆMMŸXX—VVq@@L++zEE¡YY¬\\±^^¸``¼aaÃccÅdd¼aa¸``³^^¯]]¬\\¬\\ª\\¤ZZXX•UUQQ‰NN|FFQQ¨[[±^^QQe99S//’SSŸXX¤ZZª\\¯]]³^^µ__±^^¯]]¨[[¦ZZ¤ZZ¡YYŸXX›WW•UU’SS‰NNƒKKŽQQª\\Ãcc¨[[yEEL++yEE’SS™WWXX¡YY¤ZZ¦ZZ¦ZZ¤ZZŸXXXX™WW—VV•UU’SSŽQQƒKKHH‘RR¬\\ÃccÃcc”TTe99L++zEEQQ”TT—VV—VV—VV™WW™WW™WW—VV”TT‘RRQQ‹OOƒKKƒKKŽQQª\\Ãcc¼aa—VVtBBL++L++|FF‹OO‹OOŒPPŒPP‘RR‘RR‘RRŽQQŽQQ‰NN…KKƒKK†LL‘RR¨[[º``³^^QQ}GGL++L++yEEHHHH‚JJ‚JJ†LL†LL…KK…KKƒKKƒKKŒPP’SS¤ZZ¯]]¦ZZ’SSHHL++L++k==yEEyEEƒKKŒPP…KKƒKK…KK‹OOŽQQ—VV›WW¡YY—VV†LLm>>L++L++b88k==yEE}GG†LL†LL‰NNŒPP‹OO‹OOŒPPƒKKyEEL++L++L++Y33g::tBB}GGyEEyEEyEEk==b88L++L++L++L++L++L++L++L++L++L++L++L++L++L++L++L++L++L++k==zEE’SSª\\³^^¼aaº``¸``ª\\›WW‘RRHHh;;U00n??‹OO¼aaÿrrÿzzÿ||ÿvvÿkkõffãffÕffÅdd¼aa¬\\QQvCC\44Q..b88¨[[ÿ‚‚ÿ¸¸ÿÁÁÿ¼¼ÿªªÿŠŠÿuuÿiiõffçffÕffÉeeÀbb¬\\”TTHHg::S//g::Àbbÿ¡¡ÿ××ÿêêÿÝÝÿÊÊÿ¡¡ÿ€€ÿnnÿffçffÐffÅdd¼aa¸``±^^¨[[•UU‚JJm>>X22h;;³^^ÿÿÎÎÿððÿððÿÔÔÿ··ÿ••ÿzzÿnnÿhhìffÕffÇddÃcc¼aaµ__¯]]¡YY‘RR…KKq@@X22g::¨[[ÿÿ½½ÿÍÍÿÔÔÿÏÏÿ¶¶ÿššÿ……ÿyyÿnnÿggìffÙffÉeeÅdd¾bb¸``±^^¦ZZ—VV‹OOHHk==S//g::¾bbÿŽŽÿÑÑÿííÿÖÖÿ½½ÿ±±ÿžžÿŒŒÿ{{ÿttÿllÿffìffÕffÉeeÅdd¾bb¸``±^^¨[[›WWŽQQƒKKwDDd99N,,…KKÿnnÿ¿¿ÿïïÿððÿÌÌÿ­­ÿŸŸÿÿ€€ÿuuÿnnÿhhõffçffÐffÇddÃcc¼aa¸``¯]]¦ZZ›WWQQ†LL}GGn??X22X22¬\\ÿ{{ÿááÿââÿááÿ¶¶ÿœœÿÿƒƒÿyyÿqqÿkkúffìffÞffÌffÅddÀbbº``µ__¬\\¤ZZ™WW‘RR†LLHHtBBe99Q..X22¸``ÿƒƒÿÛÛÿÓÓÿÈÈÿ  ÿÿ‚‚ÿwwÿrrÿllÿggñffãffÕffÉeeÃcc¾bb¸``±^^ª\\¡YY™WW‘RRˆMMHHwDDk==Q..X22¸``ÿ——ÿÎÎÿÈÈÿ••ÿ‰‰ÿÿvvÿppÿkkÿggõffãffÕffÌffÅdd¾bb¸``³^^¯]]¦ZZŸXX—VV‘RR‰NN€IIyEEp??[44V11¬\\ÿŽŽÿ¼¼ÿœœÿ~~ÿyyÿttÿnnÿiiÿffñffçffÙffÌffÇddÀbb¼aaµ__±^^ª\\¤ZZ›WW•UUQQˆMM€II|FFsAA[44V11¡YYÿ||ÿššÿ€€ÿrrÿnnÿiiÿggõffìffãffÙffÌffÇddÃcc¼aa¸``³^^¬\\¦ZZXX—VV’SSŒPP…KK‚JJƒKKyEE[44S//‘RRÿggÿƒƒÿkkúffõffñffçffÞffÕffÌffÇddÃccÀbb¼aa¸``³^^¯]]¦ZZŸXX—VV‘RRŒPP†LL€II…KKˆMMwDD[44Q..†LLÅddÿhhÇddÌffÕffÕffÐffÉeeÇddÃcc¾bbº``¸``µ__±^^¬\\¦ZZŸXX—VV‘RRŒPP†LLHHƒKKQQ‹OOq@@R..X22}GG¨[[¼aaµ__º``ÀbbÃccÅddÃcc¾bbº``µ__±^^¯]]¯]]ª\\¦ZZŸXX™WW’SSŒPP†LL‚JJˆMM•UUXX‰NNk==R..d99QQ¤ZZ¦ZZª\\±^^µ__¸``¸``³^^¯]]ª\\¦ZZ¦ZZ¤ZZ¡YYXX—VV’SSŒPP†LL…KKŒPP¡YY¯]]›WWzEE[44R..|FFŽQQ™WWXX¤ZZ¦ZZ¨[[¨[[¨[[¤ZZŸXXXX›WW™WW—VV”TTQQ‹OO…KK†LL‘RR¦ZZ¸``±^^ŒPPm>>N,,Y33vCC‹OO’SS—VV™WW™WW™WW›WW™WW—VV•UU’SS‘RRQQ‹OO†LLƒKK†LL’SS¨[[¸``¯]]”TTsAAU00V11p??…KK‹OOŒPPŒPPŽQQ‘RR‘RRQQŽQQŒPP‰NN†LL…KK…KK‰NN”TT¨[[±^^¦ZZQQsAAV11S//k==zEEHH€II‚JJ…KKˆMM†LL…KK…KKƒKKƒKKˆMMŽQQ•UU¡YY¦ZZ›WW‰NNn??U00Q..d99n??vCCzEE‚JJ†LLƒKKƒKK…KK†LL‰NNQQ”TT—VV—VV‹OOyEEd99R..N,,V11b88k==tBB|FF€IIƒKK…KK†LL†LL†LL…KKHHwDDb88Q..L++N,,R..^55m>>wDDzEEzEEzEEwDDp??h;;U00O--L++L++L++X22\44X22R..L++L++?D""B!!@ 8333G##}33°33Ö33ê33å33Ñ33«33‹33a00H$$7a00Ì33ÿ;;ÿFFÿFFÿBBÿ;;ÿ44ï33Ñ33«33o33R)):Y,,ÿ::ÿddÿxxÿwwÿddÿUUÿCCÿ88ÿ44ô33Û33´33o33M&&:P((ÿDDÿššÿººÿ¯¯ÿ——ÿqqÿQQÿ@@ÿ::ÿ66ÿ33ê33Ì33«33j33O'':B!!ÿ66ÿ¡¡ÿààÿææÿÄÄÿÿccÿQQÿFFÿ==ÿ99ÿ55ô33Û33Ç3333b11L&&87¢33ÿnnÿÂÂÿííÿääÿ±±ÿ€€ÿddÿRRÿHHÿ@@ÿ::ÿ66ÿ33ê33Ñ33°3333a00H$$7b11ÿUUÿ¦¦ÿÂÂÿÎÎÿ¿¿ÿ˜˜ÿwwÿ``ÿRRÿIIÿBBÿ::ÿ66ÿ33ï33Ö33¹33”33j33U**=]..ÿDDÿŸŸÿßßÿÌÌÿ°°ÿœœÿ‚‚ÿjjÿ\\ÿQQÿHHÿBBÿ;;ÿ66ÿ33ï33Ö33¾33™33o33]..H$$7°33ÿyyÿ××ÿóóÿËËÿ¡¡ÿ‹‹ÿvvÿ``ÿUUÿMMÿGGÿ@@ÿ::ÿ66ÿ33ï33Ö33¾3333x33a00P((;U**ÿ66ÿŒŒÿååÿååÿ´´ÿŒŒÿ{{ÿiiÿ[[ÿQQÿIIÿDDÿ??ÿ99ÿ55ù33ê33Ñ33¹3333x33a00W++D""5T**ÿ==ÿŒŒÿÞÞÿÐÐÿœœÿÿmmÿ``ÿUUÿNNÿGGÿBBÿ==ÿ88ÿ44ô33å33Ç33´3333}33b11Y,,L&&5P((ÿBBÿ‚‚ÿ¿¿ÿ¯¯ÿ‡‡ÿooÿccÿYYÿQQÿIIÿCCÿ??ÿ::ÿ55ù33ê33Ö33Â33°333333d22Z--P((7L&&ÿ@@ÿssÿ™™ÿ||ÿnnÿddÿYYÿQQÿKKÿFFÿ@@ÿ;;ÿ88ÿ44ô33å33Ñ33¾33«33™3333d22\..T**7J%%ÿ88ÿccÿ{{ÿeeÿ__ÿYYÿQQÿKKÿGGÿCCÿ==ÿ99ÿ55ÿ33ï33Û33Ì33¹33¦33”33}33d22]..W++8H$$ù33ÿYYÿccÿZZÿUUÿPPÿIIÿFFÿCCÿ??ÿ::ÿ66ÿ44ô33ê33Ö33Ç33°3333‹33x33d22b11\..=E""Ñ33ÿIIÿSSÿKKÿIIÿFFÿCCÿ@@ÿ==ÿ::ÿ66ÿ33ô33ê33Û33Ì33¹33¢3333}33j33b11j33]..?D""«33ÿ88ÿ@@ÿ??ÿ@@ÿ??ÿ;;ÿ99ÿ88ÿ55ù33ï33ê33Û33Ì33¾33¦33”3333o33a00f33x33Y,,8@ ‹33Ö33ù33ÿ55ÿ88ÿ99ÿ88ÿ55ÿ33ô33ê33Û33Ö33Ì33Â33°3333†33s33b11b11‹33†33R))5=d22¹33Ñ33å33ô33ÿ33ÿ44ù33ï33å33Ñ33Ì33Ç33Â33´33¢3333x33d22f33”33´3333L&&5T**™33¹33Ç33Ö33å33ê33Û33Ñ33Ç33¾33¹33´33°33¢33”3333j33j33”33Ì33¾33a00=G##j3333«33¹33Â33Ç33Â33¾33´33«33¦33¢333333}33j33j3333Ç33ï33«33R))7P((s33”33¢33¦33«33«33«33¦3333”3333†33x33f33d22†33Â33ê33Ì33j33@ ;T**s33†33‹333333”33”33‹33†33x33s33f33f3333´33Û33Ç3333M&&7=U**f33j33o33s33x33x33s33o33f33d22j3333¦33Ç33Â33†33T**7@ T**\.._//a00f33f33d22d22f33j333333´33«33}33U**;?M&&U**]..d22d22d22f33s33}33‹33”3333b11M&&;;H$$P((Y,,a00b11d22j33f33d22a00U**E""55;L&&U**Z--Z--Z--U**O''B!!:33333333*33323333@ Y,,¢33Ñ33ï33ô33ï33Ì33¢33}33P((8@ ]..Ñ33ÿBBÿ??ÿ@@ÿ::ÿ99ÿ66ÿ33ô33Û33°33j33@ 33«33ÿjjÿÿ__ÿZZÿSSÿFFÿ??ÿ::ÿ66ÿ44ô33Û33¹33d22O''33Ñ33ÿ’’ÿÞÞÿÞÞÿ˜˜ÿssÿ^^ÿNNÿDDÿ==ÿ99ÿ66ÿ33å33Ì33¹33j33T**3;33ÿ¢¢ÿïïÿÿÿÿòòÿ¸¸ÿyyÿeeÿSSÿHHÿBBÿ;;ÿ88ÿ55ô33Û33Ñ3333f33Z--33s33ÿjjÿ˜˜ÿÞÞÿøøÿÖÖÿœœÿxxÿddÿUUÿMMÿCCÿ==ÿ88ÿ55ù33ê33Ö33«33†33f33P((3a00ÿMMÿ½½ÿÞÞÿµµÿ»»ÿŸŸÿ……ÿnnÿ__ÿSSÿKKÿDDÿ==ÿ99ÿ55ù33ê33Ö33¹33”33j33]..D""D""ÿVVÿ´´ÿÿÿÿÿÿÿµµÿ§§ÿŠŠÿyyÿ``ÿZZÿQQÿIIÿDDÿ==ÿ99ÿ55ù33ê33Ñ33¹33™33s33a00P((3°33ÿeeÿààÿýýÿããÿžžÿÿ||ÿiiÿ\\ÿSSÿMMÿGGÿBBÿ;;ÿ88ÿ44ù33ê33Ñ33¹33™33s33b11Z--D""3ÿ88ÿeeÿééÿòòÿ»»ÿÿ||ÿooÿ``ÿYYÿPPÿIIÿDDÿ@@ÿ::ÿ66ÿ33ô33å33Ç33´33™33}33b11Z--O''33ÿBBÿddÿÎÎÿÜÜÿ££ÿ||ÿooÿ__ÿZZÿRRÿKKÿFFÿBBÿ==ÿ99ÿ55ù33ê33Ö33Â33°3333}33d22Z--W++33ÿBBÿ^^ÿœœÿ……ÿwwÿnnÿccÿYYÿRRÿMMÿHHÿCCÿ??ÿ::ÿ66ÿ33ï33å33Ñ33¾33«3333}33f33Z--]..33ÿ::ÿQQÿccÿiiÿddÿ__ÿVVÿQQÿMMÿHHÿDDÿ@@ÿ;;ÿ88ÿ55ù33ê33Û33Ì33¹33¦33™33}33j33Z--a0033ÿ44ÿHHÿUUÿ^^ÿZZÿSSÿNNÿIIÿFFÿCCÿ@@ÿ;;ÿ99ÿ66ÿ33ï33å33Ñ33Â33«333333s33f33d22d22?3Ì33ÿ==ÿGGÿKKÿMMÿIIÿFFÿCCÿ@@ÿ==ÿ::ÿ88ÿ55ÿ33ô33ê33Ö33Ç33°33333333j33a00}33f33=333ÿ44ÿ::ÿ==ÿ@@ÿ@@ÿ??ÿ;;ÿ::ÿ88ÿ55ÿ33ù33ï33å33Ö33Ç33´33¢333333o33Z--x33‹33_//33}33Ç33ê33ÿ33ÿ55ÿ88ÿ::ÿ88ÿ55ÿ33ô33ê33å33Û33Ñ33Ì33¹33¦33”33}33s33Z--o33°3333T**3\..´33Ì33Ö33ê33ô33ÿ44ÿ55ô33ê33Û33Ñ33Ì33Ì33Ç33¹33«33™33†33s33]..†33Â33Ö33†33H$$:33°33¹33Ç33Ñ33Û33å33Ö33Ñ33Â33¾33¹33´33°33¦33™3333s33d2233Ç33ÿ44Â33Z--3Z--33¢33«33´33¹33¾33¾33¹33°33«33¢3333™333333d22a00‹33Ì33ÿ44ÿ44”33H$$3\..†33”33333333¢33¢33¢3333”33‹33†33x33d22d2233Ç33ÿ44ô3333U**33]..x33x33}33}33‹33‹33‹333333s33f33d22j33‹33Â33ï33Û33†33_//33Z--a00a00b11b11j33j33f33f33d22d22}3333¹33Ñ33¾3333a0033M&&Z--Z--d22}33f33d22f33x333333¦33´3333j33O''33E""M&&Z--_//j33j33s33}33x33x33}33d22Z--333?J%%U**_//Z--Z--Z--M&&E""333333333333333333M&&\..33Ç33Û33ô33ï33ê33Ç33¦33‹33a00L&&;P((x33ô33ÿVVÿ__ÿccÿ[[ÿNNÿFFÿ@@ÿ;;ÿ55ô33Ì33†33W++@ 7E""Â33ÿhhÿ¨¨ÿ´´ÿ­­ÿ˜˜ÿrrÿZZÿMMÿFFÿBBÿ;;ÿ88ÿ33Ì33”33a00J%%:J%%ÿ33ÿŒŒÿÎÎÿååÿÖÖÿ¾¾ÿŒŒÿffÿRRÿHHÿBBÿ::ÿ55ô33ê33Ö33Â33™33b11O''=L&&Û33ÿxxÿÃÃÿííÿííÿËËÿ§§ÿÿ__ÿRRÿKKÿCCÿ;;ÿ66ÿ44ô33å33Ñ33´33‹33f33T**=J%%Â33ÿvvÿ¯¯ÿÂÂÿËËÿÄÄÿ¦¦ÿ„„ÿmmÿ^^ÿRRÿIIÿCCÿ==ÿ88ÿ55ù33ê33Ö33¾3333x33a00M&&:J%%ù33ÿwwÿÅÅÿééÿÌÌÿ¯¯ÿŸŸÿ‰‰ÿssÿ``ÿYYÿPPÿHHÿCCÿ;;ÿ88ÿ55ù33ê33Ö33Â33¦3333d22Y,,G##5f33ÿRRÿ°°ÿëëÿííÿ¿¿ÿššÿŠŠÿxxÿffÿZZÿRRÿKKÿFFÿBBÿ::ÿ66ÿ44ô33ê33Ñ33¾33¦33†33j33_//P((==Ì33ÿ``ÿÚÚÿÜÜÿÚÚÿ¦¦ÿ‡‡ÿxxÿiiÿ^^ÿUUÿNNÿGGÿCCÿ??ÿ99ÿ55ÿ33ï33å33Ì33¹33¢33‹33j33a00U**H$$7=ê33ÿiiÿÒÒÿÉÉÿ»»ÿ‹‹ÿvvÿhhÿ\\ÿVVÿPPÿIIÿDDÿ@@ÿ;;ÿ88ÿ44ù33ê33Ö33Ç33´33¢33‹33o33a00Y,,M&&7=ê33ÿÿÃÃÿ»»ÿÿqqÿeeÿ[[ÿSSÿNNÿIIÿFFÿ@@ÿ;;ÿ99ÿ55ù33ê33Û33Ñ33¾33°3333‹33s33a00Z--R))@ ;Ì33ÿwwÿ­­ÿ‡‡ÿddÿ^^ÿYYÿRRÿMMÿHHÿDDÿBBÿ==ÿ99ÿ66ÿ33ô33å33Ö33Ç33¹33¦33™33†33o33a00]..T**@ ;´33ÿccÿ„„ÿffÿVVÿRRÿMMÿIIÿFFÿCCÿ@@ÿ==ÿ99ÿ66ÿ44ô33ê33Û33Ì33¾33«333333}33f33b11d22Z--@ :‹33ÿIIÿiiÿNNÿGGÿFFÿDDÿBBÿ??ÿ;;ÿ99ÿ66ÿ44ÿ33ô33ê33Û33Ñ33¾33°3333‹33}33j33a00f33o33Y,,@ 7j33ÿ55ÿKKÿ66ÿ99ÿ;;ÿ;;ÿ::ÿ88ÿ66ÿ44ù33ï33ê33å33Ö33Ì33¾33°3333‹33}33j33a00d22†33x33T**8=_//Â33ô33å33ï33ÿ33ÿ44ÿ55ÿ44ù33ï33å33Ö33Ñ33Ñ33Ç33¾33°33¢3333}33j33b11o33™33«33s33M&&8G##†33¹33¾33Ç33Ö33å33ê33ê33Û33Ñ33Ç33¾33¾33¹33´33«333333}33j33f33}33´33Ñ33¦33\..@ 8]..33¢33«33¹33¾33Â33Â33Â33¹33°33«33¦33¢3333”33†33x33f33j33‹33¾33ê33Ö33}33O''5?W++x333333¢33¢33¢33¦33¢3333™3333‹33†33x33j33d22j3333Â33ê33Ñ33”33T**;;R))f33x33}33}3333‹33‹33†3333}33s33j33f33f33s33”33Â33Ö33¾33†33T**;:M&&\..a00a00b11f33o33j33f33f33d22d22o3333™33´33¾33¦33s33P((;7G##P((W++\..b11j33d22d22f33j33s33†33”333333x33Z--G##85;E""M&&U**]..a00d22f33j33j33j33f33a00Y,,E""7358B!!O''Y,,\..\..\..Y,,R))L&&;7333=@ =833 \ No newline at end of file
diff --git a/bubbob/images/buildcolors.py b/bubbob/images/buildcolors.py
new file mode 100644
index 0000000..0f7ea01
--- /dev/null
+++ b/bubbob/images/buildcolors.py
@@ -0,0 +1,324 @@
+#! /usr/bin/env python
+import sys, os
+
+if __name__ == '__main__':
+ ThisDir = sys.argv[0]
+else:
+ ThisDir = __file__
+ThisDir = os.path.dirname(os.path.abspath(ThisDir))
+
+### rotate colors
+import colorsys
+COLORS = [#(0, 0.0, 1.0, 1, 1), # vert
+ #(1, 0.0, 1.0, 1, 1), # bleu
+ (1, -0.7, 1.0, 1, 1), # rose
+ (0, -0.2, 1.0, 1, 1), # brun
+ (1, 0.72,1.0,-1, 1), # jaune
+ (0, -0.35,0.85,1, 1), # rouge
+ (0, 0, 0.0, 1, 1), # gris
+ (0, -0.85, 0.9, 1, 1), # cyan (was mauve)
+ #(0, 0.2, 1.0, 1, 1), # turquoise
+ (0, 0.925, 0.95,-1, 1), # bleu fonce
+ #(0, 0.45, 0.5, -0.5, 0.75), # hum
+ (1, 'specialpixelmap'), # vert fonce
+ ]
+MAX = 2 + len (COLORS)
+
+## By ION:
+#
+# Here's the new palette-based method.
+#
+# It's an array [N][320] of 24bit unsigned integers
+# (where N is the total number of color sets including the original one.)
+#
+# That is, you access it like
+#
+# Palettes[(PALETTESIZE * palettenumber)+paletteindex]
+#
+# Activate it by passing a palette file as a cmdline argument.
+#
+# The color mapping could be further sped up
+# by making Palettes an array of bytes rather than ints,
+# at the cost of increased complexity (Palettes [ (PALETTESIZE * 3 * palettenumber) + paletteindex + component])
+#
+
+Palettes = None # currently there is no 'internal' palette since this is experimental.
+
+PALETTESIZE = 960
+
+PaletteIndex = None
+# generate the string:paletteindex lookup table
+def initpalettelut ():
+ global PaletteIndex
+ global COLORS, COLORMAPS
+ # palette 0 is the base palette (green dragon, blue tiger)
+ #
+ # Palette 0 must contain NO duplicate colors.
+ PaletteIndex = {}
+ for i in range (PALETTESIZE):
+ v = Palettes[i]
+ #if v & 0xff == 0 and (v >> 8) & 0xff == 0x87 and (v >> 16) & 0xff == 0:
+ # print 'FOUND'
+ s = "".join ([chr ((v >> shift) & 0xff) for shift in (0,8,16)])
+ PaletteIndex[s] = i
+ # invalidate COLORS, but match the length to the number of alt palettes.
+ COLORS = range ((len (Palettes) / PALETTESIZE) - 1)
+ #print 'COLORS',COLORS
+ COLORMAPS = [{} for n in COLORS]
+ #print 'COLORMAPS',COLORMAPS
+
+def loadpalettesets (filename):
+ global Palettes
+ #import array
+ #Palettes = array.array ('I')
+ Palettes = []
+ assert ((os.path.getsize (filename) % (PALETTESIZE * 3)) == 0)
+ #print os.path.getsize (filename)
+ f = open (filename, 'rb')
+ for i in range (os.path.getsize(filename) / (PALETTESIZE * 3)):
+ for j in range (PALETTESIZE):
+ tmp = f.read (3)
+ val = ord (tmp[0]) | (ord (tmp[1]) << 8) | (ord (tmp[2]) << 16)
+ Palettes.append (val)
+ #debuggest
+ #print len(Palettes)
+ #print len(Palettes) % PALETTESIZE
+ assert (len (Palettes) % PALETTESIZE) == 0
+ #print "Palettes len:",len (Palettes)
+
+def inputfiles ():
+ InputFiles = {
+ os.path.join (ThisDir, os.pardir, 'ext1', 'image1-%d.ppm'): 1,
+ os.path.join (ThisDir, os.pardir, 'ext3', 'image1-%d.ppm'): 1,
+ os.path.join (ThisDir, os.pardir, 'ext4', 'image1-%d.ppm'): 1,
+ os.path.join (ThisDir, os.pardir, 'ext6', 'image1-%d.ppm'): 1,
+ os.path.join (ThisDir, os.pardir, 'ext7', 'image1-%d.ppm'): 1,
+ }
+ d = {}
+ execfile (os.path.join(ThisDir, os.pardir, 'sprmap.py'), d)
+ sprmap = d['sprmap']
+ for key, (filename, rect) in sprmap.items ():
+ if filename.find('%d') >= 0:
+ InputFiles[os.path.join (ThisDir, filename)] = 1
+ return InputFiles.keys ()
+
+# ____________________________________________________________
+
+def pixelmap (r, g, b):
+ r /= 255.0
+ g /= 255.0
+ b /= 255.0
+ h, s, v = colorsys.rgb_to_hsv(r, g, b)
+ h = (h*sign + delta) % 1.0
+ s *= sat
+ v *= lumen
+ r, g, b = colorsys.hsv_to_rgb(h, s, v)
+ return r*255.1, g*255.1, b*255.1
+
+def specialpixelmap (r, g, b):
+ return r * 0.1, g * 0.7, r * 0.5
+
+usingpalette = 0
+
+def palettepixelmap (r, g, b):
+# print max(r,g,b)
+ packed = chr(r) + chr(g) + chr(b)
+ try:
+ index = PaletteIndex[packed]
+ #print 'index %r' % index
+# print 'USING', usingpalette
+ v = thispalette[index] #Palettes[(PALETTESIZE * (usingpalette + 1)) + index]
+# print 'hit! %r' % packed
+# print '-> %r' % (chr(v & 0xff) + chr ((v >> 8) & 0xff) + chr((v >> 16) & 0xff))
+# print '%r : %r' % (Palettes[index], Palettes[PALETTESIZE + index])
+ return v & 0xff, (v >> 8) & 0xff, (v >> 16) & 0xff
+ except KeyError:
+ return r,g,b
+
+def ppmbreak (f):
+ sig = f.readline ().strip ()
+ assert sig == "P6"
+ while 1:
+ line = f.readline ().strip ()
+ if not line.startswith('#'):
+ break
+ wh = line.split ()
+ w, h = map (int, wh)
+ sig = f.readline ().strip()
+ assert sig == "255"
+ data = f.read ()
+ return w, h, data
+
+COLORMAPS = [{} for n in COLORS]
+del n
+
+def paletterotate (imglist, chr=chr, int=int, ord=ord):
+ global thispalette
+ gw, gh, green = imglist[0]
+# assert bw == gw and bh == gh
+ n = 0
+ (_, _, fromimage) = imglist[0]
+ for reserved in COLORS:
+ # is not being entered, the fool.
+# lut = {}
+# for
+ thispalette = Palettes[(PALETTESIZE * (reserved + 1)):(PALETTESIZE * (reserved + 2))]
+ # wot is this? _ means unused?
+# (_, _, otherimage) = imglist[1-n]
+ image = []
+ colormap = COLORMAPS[reserved]
+ append = image.append
+
+ for i in range (0, len(fromimage), 3):
+ rgb1 = fromimage[i:i+3]
+# rgb2 = otherimage[i:i+3]
+# if rgb1 == rgb2:
+# append (rgb1)
+ if rgb1 in colormap:
+ append (colormap[rgb1])
+ else:
+# print 'HI!'
+ r, g, b = ord(rgb1[0]), ord(rgb1[1]), ord(rgb1[2])
+# print '%d,%d,%d ->' % (r,g,b)
+ r, g, b = palettepixelmap (r, g, b)
+# print '%d,%d,%d.' % (r,g,b)
+ newrgb = chr (int (r))+chr (int (g))+chr (int (b))
+ append (newrgb)
+ colormap[rgb1] = newrgb
+ imglist.append((gw, gh, ''.join (image)))
+
+
+def rotate (imglist, chr=chr, int=int, ord=ord):
+ global delta, sat, sign, lumen
+ (bw, bh, blue), (gw, gh, green) = imglist
+ assert bw == gw and bh == gh
+ for reserved in range (len (COLORS)):
+ if len (COLORS[reserved]) == 2:
+ n, fn = COLORS[reserved]
+ fn = globals ()[fn]
+ else:
+ n, delta, sat, sign, lumen = COLORS[reserved]
+ fn = pixelmap
+ (_, _, fromimage) = imglist[n]
+ (_, _, otherimage) = imglist[1-n]
+ image = []
+ colormap = COLORMAPS[reserved]
+ append = image.append
+ for i in range (0, len(fromimage), 3):
+ rgb1 = fromimage[i:i+3]
+ rgb2 = otherimage[i:i+3]
+ if rgb1 == rgb2:
+ append (rgb1)
+ elif rgb1 in colormap:
+ append (colormap[rgb1])
+ else:
+ r, g, b = fn(ord(rgb1[0]), ord(rgb1[1]), ord(rgb1[2]))
+ newrgb = chr(int(r))+chr(int(g))+chr(int(b))
+ append(newrgb)
+ colormap[rgb1] = newrgb
+ imglist.append((bw, bh, ''.join(image)))
+
+def writeout (imglist, namepattern, paletted = False):
+ start = 2
+ if paletted:
+ start = 1
+ for i in range (start, len (imglist)):
+ w, h, data = imglist[i]
+ fn = namepattern % i
+ f = open (fn, 'wb')
+ print >> f, 'P6'
+ print >> f, w, h
+ print >> f, 255
+ f.write (data)
+ f.close ()
+
+
+def convert (name):
+ print >> sys.stderr, 'generating colors for %s...' % name
+ imglist = [ppmbreak (open (name % 0, 'rb'))]
+ paletted = False
+ if Palettes:
+ paletterotate (imglist)
+ paletted = True
+ else:
+ imglist.append(ppmbreak (open (name % 1, 'rb')))
+ rotate (imglist)
+ writeout (imglist, name, paletted)
+
+def updatecheck ():
+ myself = os.path.join (ThisDir, 'buildcolors.py')
+
+ def older (list1, list2):
+ def mtime (name):
+ try:
+ st = os.stat (name)
+ except OSError:
+ return None
+ else:
+ return st.st_mtime
+ list2 = [mtime (name) for name in list2]
+ if None in list2:
+ return 0
+ else:
+ list1 = [mtime(name) for name in list1]
+ list1 = [t for t in list1 if t is not None]
+ return list1 and list2 and max (list1) < min (list2)
+
+ rebuild = {}
+ for filename in inputfiles ():
+ distfiles = [myself, filename % 0]
+ genfiles = [filename % n for n in range (1, MAX)]
+ rebuild[filename] = not older (distfiles, genfiles)
+ return rebuild
+
+
+#try to load palettes first
+tmp = os.path.join (ThisDir, os.pardir, 'images', 'palettes.dat')
+if os.path.exists (tmp):
+ #print 'loading palettes'
+ loadpalettesets (tmp)
+ initpalettelut ()
+else:
+ # from now on we should always use the palette approach;
+ # comment out the following line to restore the old color-rotation code.
+ raise IOError("cannot find the palette file %r" % (tmp,))
+
+
+if __name__ == '__auto__': # when execfile'd from images.py
+ rebuild = updatecheck ().items ()
+ rebuild.sort ()
+ for fn, r in rebuild:
+ if r:
+ convert(fn)
+
+#try:
+# import psyco
+# psyco.bind(rotate)
+#except:
+# pass
+
+if __name__ == '__main__':
+ if sys.argv[1:2] == ['-f']:
+ files = inputfiles ()
+ elif sys.argv[1:2] == ['-c']:
+ for filename in inputfiles ():
+ for n in range (1, MAX):
+ try:
+ os.unlink (filename % n)
+ except OSError:
+ pass
+ else:
+ print 'rm', filename % n
+ sys.exit()
+ else:
+ rebuild = updatecheck ()
+ if 0 in rebuild.values ():
+ print >> sys.stderr, ('%d images up-to-date. '
+ 'Use -f to force a rebuild or -c to clean.' %
+ rebuild.values ().count(0))
+ files = [fn for fn, r in rebuild.items () if r]
+
+ files.sort ()
+ for filename in files:
+ convert (filename)
+
diff --git a/bubbob/images/butterfly.ppm b/bubbob/images/butterfly.ppm
new file mode 100644
index 0000000..a7af2c5
--- /dev/null
+++ b/bubbob/images/butterfly.ppm
Binary files differ
diff --git a/bubbob/images/cream_pie_big.ppm b/bubbob/images/cream_pie_big.ppm
new file mode 100644
index 0000000..827a2e0
--- /dev/null
+++ b/bubbob/images/cream_pie_big.ppm
Binary files differ
diff --git a/bubbob/images/diamond_big_blue.ppm b/bubbob/images/diamond_big_blue.ppm
new file mode 100644
index 0000000..78f7a8c
--- /dev/null
+++ b/bubbob/images/diamond_big_blue.ppm
Binary files differ
diff --git a/bubbob/images/diamond_big_purple.ppm b/bubbob/images/diamond_big_purple.ppm
new file mode 100644
index 0000000..2b0938e
--- /dev/null
+++ b/bubbob/images/diamond_big_purple.ppm
@@ -0,0 +1,5 @@
+P6
+# CREATOR: GIMP PNM Filter Version 1.1
+90 90
+255
+ˆˆˆˆ˜!˜ $  $ §&§§&§¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯§&§§&§ $  $ ˜!˜ˆˆˆˆÀ/ÀÜ3Ìÿ9Ìÿ9ÌÿGÌÿGÌÿQÌÿQÌÿ[Ìÿ[Ìÿ[Ìÿ[ÌÿfÌÿ_Ìÿ_Ìÿ[ÌÿQÌÿGÌÿ9Ìÿ3Ìõ3Ìõ3Ìñ3Ìñ3Ìê3Ìç3Ìà3ÌÜ3ÌÕ3ÌÐ3ÌÎ3ÌÉ2ÉÄ0ÄÀ/À»-»¸,¸¶+¶´+´²*²¯)¯¯)¯¯)¯¯)¯¯)¯• •• •• •À/ÀÜ3Ìÿ9ÌÿXÌÿgÌÿmÏÿoÐÿoÐÿoÐÿpÑÿpÑÿrÒÿtÓÿtÓÿtÓÿtÓÿtÓÿrÒÿpÑÿkÎÿbÌÿQÌÿGÌÿ9Ìÿ6Ìÿ3Ìü3Ìú3Ìõ3Ìñ3Ìñ3Ìç3Ìà3ÌÜ3ÌÐ3ÌÉ2ÉÄ0ÄÀ/À»-»¶+¶´+´²*²¯)¯¯)¯¯)¯¯)¯¡$¡¡$¡• •• •À/ÀÜ3Ìÿ9ÌÿXÌÿmÏÿoÐÿoÐÿpÑÿpÑÿrÒÿrÒÿrÒÿrÒÿrÒÿrÒÿrÒÿpÑÿoÐÿkÎÿfÌÿXÌÿNÌÿ@Ìú3Ìú3Ìú3Ìõ3Ìó3Ìì3Ìç3Ìà3ÌÞ3ÌÜ3ÌÕ3ÌÎ3ÌÉ2ÉÃ0ø,¸¶+¶´+´²*²¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¡$¡¡$¡• •ŠŠ• •À/ÀÜ3Ìÿ9ÌÿXÌÿmÏÿtÓÿtÓÿzÖÿzÖÿzÖÿzÖÿzÖÿxÕÿtÓÿpÑÿmÏÿiÍÿfÌÿ_ÌÿXÌÿNÌü3Ìñ3Ìñ3Ìñ3Ìê3ÌÜ3ÌÎ3ÌÎ3ÌÉ2ÉÄ0ÄÀ/ÀÀ/À¿.¿»-»»-»¶+¶¶+¶¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯§&§¡$¡• •ŠŠ• •À/ÀÜ3Ìÿ9ÌÿXÌÿmÏÿtÓÿzÖÿØÿØÿØÿØÿØÿØÿ{ÖÿxÕÿtÓÿpÑÿmÏÿiÍÿfÌÿ_ÌÿXÌÿJÌü3ÌÜ3ÌÜ3ÌÜ3ÌÎ3ÌÉ2ÉÄ0ÄÄ0ÄÀ/À¿.¿¿.¿»-»»-»¶+¶¶+¶¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯¯)¯ª'ª¡$¡• •ŠŠ• •À/ÀÜ3Ìÿ9ÌÿXÌÿmÏÿtÓÿzÖÿ„ÛÿŠÞÿŒßÿ‘áÿ‘áÿŠÞÿ„ÛÿØÿ{ÖÿxÕÿtÓÿpÑÿmÏÿiÍÿfÌÿ_ÌÿXÌÿJÌü3Ìç3ÌÞ3ÌÞ3ÌÞ3ÌÜ3Ì×3ÌÕ3ÌÐ3ÌÎ3ÌÉ2ÉÄ0ÄÄ0ÄÄ0ÄÃ0ÃÀ/ÀÀ/À¿.¿¼-¼»-»¸,¸¶+¶¶+¶¶+¶´+´²*²¯)¯¯)¯¯)¯ª'ª¡$¡• •ŽŽ• •À/ÀÜ3Ìÿ9ÌÿXÌÿmÏÿtÓÿzÖÿ„Ûÿ˜åÿ¥ëÿ²òÿ¥ëÿ˜åÿ‘áÿŠÞÿ„ÛÿØÿ{ÖÿxÕÿtÓÿpÑÿmÏÿiÍÿfÌÿ_ÌÿXÌÿQÌÿ@Ìø3Ìñ3Ìñ3Ìñ3Ìñ3Ìì3Ìê3Ìç3Ìå3Ìà3ÌÞ3ÌÜ3ÌÜ3Ì×3ÌÐ3ÌÎ3ÌÎ3ÌÉ2ÉÇ1ÇÃ0ÿ.¿¼-¼¼-¼¼-¼¸,¸¶+¶´+´¯)¯ª'ª¡$¡• •ŽŽ• •À/ÀÜ3Ìÿ9ÌÿXÌÿmÏÿtÓÿzÖÿ„Ûÿ˜åÿ²òÿ¿øÿÄûÿ¿øÿ·ôÿ¥ëÿ˜åÿŒßÿ„ÛÿØÿ{ÖÿxÕÿtÓÿpÑÿmÏÿiÍÿfÌÿ_ÌÿXÌÿQÌÿJÌÿ3Ìø3Ìõ3Ìõ3Ìñ3Ìì3Ìê3Ìç3Ìå3Ìà3ÌÞ3ÌÜ3ÌÜ3Ì×3ÌÐ3ÌÎ3ÌÎ3ÌÌ3ÌÉ2ÉÇ1ÇÃ0ÿ.¿¼-¼¼-¼¸,¸¶+¶´+´¯)¯ª'ª¡$¡• •ŽŽ• •À/ÀÜ3Ìÿ9ÌÿXÌÿmÏÿtÓÿzÖÿ„Ûÿ˜åÿ²òÿÌÿÿÌÿÿÕÿÿÕÿÿÌÿÿ·ôÿ¥ëÿ˜åÿŒßÿ„ÛÿØÿ{ÖÿxÕÿtÓÿpÑÿmÏÿiÍÿfÌÿ_ÌÿXÌÿQÌÿJÌÿ@Ìÿ3Ìü3Ìø3Ìõ3Ìõ3Ìó3Ìñ3Ìê3Ìê3Ìç3Ìç3Ìç3Ìå3Ìà3ÌÞ3ÌÞ3Ì×3ÌÐ3ÌÇ1ÇÃ0ÿ.¿¼-¼¼-¼¸,¸¶+¶´+´¯)¯ª'ª¤%¤• •ŽŽ• •À/ÀÜ3Ìÿ9ÌÿXÌÿmÏÿtÓÿzÖÿ„Ûÿ˜åÿ²òÿÌÿÿØÿÿàÿÿàÿÿàÿÿØÿÿÌÿÿ·ôÿ¥ëÿ˜åÿŒßÿ„ÛÿØÿ{ÖÿxÕÿtÓÿpÑÿmÏÿiÍÿfÌÿ_ÌÿXÌÿQÌÿGÌÿ9Ìÿ6Ìÿ3Ìÿ3Ìÿ3Ìü3Ìú3Ìõ3Ìõ3Ìõ3Ìõ3Ìõ3Ìó3Ìó3Ìó3Ìñ3Ìê3ÌÞ3Ì×3ÌÎ3ÌÇ1ÇÃ0ÿ.¿¼-¼¸,¸¶+¶´+´¯)¯ª'ª¤%¤• •ŽŽ• •À/ÀÜ3Ìÿ9ÌÿXÌÿmÏÿtÓÿzÖÿ„Ûÿ˜åÿ²òÿÌÿÿØÿÿàÿÿçÿÿçÿÿçÿÿàÿÿØÿÿÌÿÿ·ôÿ¥ëÿ˜åÿŒßÿ„ÛÿØÿ{ÖÿxÕÿtÓÿpÑÿmÏÿiÍÿfÌÿ_ÌÿXÌÿQÌÿGÌÿ=Ìÿ9Ìÿ6Ìÿ3Ìÿ3Ìÿ3Ìü3Ìú3Ìõ3Ìõ3Ìõ3Ìõ3Ìó3Ìó3Ìñ3Ìñ3Ìñ3ÌÞ3Ì×3ÌÎ3ÌÇ1ÇÃ0ÿ.¿¼-¼¸,¸¶+¶´+´¯)¯ª'ª¤%¤• •ŽŽ• •À/ÀÜ3Ìÿ9ÌÿXÌÿmÏÿtÓÿzÖÿ„Ûÿ˜åÿ²òÿÌÿÿØÿÿàÿÿçÿÿòÿÿúÿÿòÿÿçÿÿàÿÿØÿÿÌÿÿ·ôÿ¥ëÿ˜åÿŒßÿ„ÛÿØÿ{ÖÿxÕÿtÓÿpÑÿmÏÿiÍÿfÌÿ_ÌÿXÌÿQÌÿGÌÿ=Ìÿ=Ìÿ9Ìÿ9Ìÿ6Ìÿ3Ìü3Ìü3Ìú3Ìø3Ìø3Ìø3Ìõ3Ìó3Ìó3Ìñ3Ìñ3Ìå3Ì×3ÌÌ3ÌÇ1ÇÃ0ÿ.¿¼-¼¸,¸¶+¶´+´¯)¯ª'ª¤%¤• •ŽŽ• •À/ÀÜ3Ìÿ9ÌÿXÌÿmÏÿtÓÿzÖÿ„Ûÿ˜åÿ²òÿÌÿÿØÿÿàÿÿçÿÿòÿÿúÿÿÿÿÿúÿÿòÿÿçÿÿàÿÿØÿÿÌÿÿ·ôÿ¥ëÿ˜åÿŒßÿ„ÛÿØÿ{ÖÿxÕÿtÓÿpÑÿmÏÿiÍÿfÌÿ_ÌÿXÌÿQÌÿGÌÿ@Ìÿ@Ìÿ=Ìÿ9Ìÿ9Ìÿ3Ìÿ3Ìü3Ìü3Ìú3Ìø3Ìø3Ìõ3Ìó3Ìó3Ìñ3Ìñ3Ìå3Ì×3ÌÎ3ÌÇ1ÇÃ0ÿ.¿¼-¼¸,¸¶+¶´+´¯)¯ª'ª¡$¡• •ŽŽ• •À/ÀÜ3Ìÿ9ÌÿXÌÿmÏÿtÓÿzÖÿ„Ûÿ˜åÿ²òÿÌÿÿØÿÿàÿÿçÿÿòÿÿúÿÿÿÿÿÿÿÿÿÿÿúÿÿòÿÿçÿÿàÿÿØÿÿÌÿÿ·ôÿ¥ëÿ˜åÿŒßÿ„ÛÿØÿ{ÖÿxÕÿtÓÿpÑÿmÏÿiÍÿfÌÿ_ÌÿXÌÿQÌÿNÌÿGÌÿ@Ìÿ=Ìÿ=Ìÿ9Ìÿ9Ìÿ3Ìÿ3Ìü3Ìü3Ìú3Ìø3Ìõ3Ìó3Ìó3Ìñ3Ìñ3Ìç3Ì×3ÌÎ3ÌÇ1ÇÃ0ÿ.¿¼-¼¸,¸¶+¶´+´¯)¯«(«¡$¡• •ŽŽ• •À/ÀÜ3Ìÿ9ÌÿXÌÿmÏÿtÓÿzÖÿ„Ûÿ˜åÿ²òÿÌÿÿØÿÿàÿÿçÿÿòÿÿúÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿúÿÿòÿÿçÿÿàÿÿØÿÿÌÿÿ·ôÿ¥ëÿ˜åÿŒßÿ„ÛÿØÿ{ÖÿxÕÿtÓÿpÑÿmÏÿiÍÿfÌÿ_ÌÿXÌÿQÌÿNÌÿGÌÿ@Ìÿ@Ìÿ=Ìÿ9Ìÿ9Ìÿ6Ìÿ3Ìÿ3Ìü3Ìú3Ìú3Ìõ3Ìõ3Ìó3Ìñ3Ìñ3Ìç3Ì×3ÌÎ3ÌÇ1ÇÃ0ÿ.¿¼-¼¸,¸¶+¶´+´¯)¯«(«¡$¡• •ŽŽ• •À/ÀÜ3Ìÿ9ÌÿXÌÿmÏÿtÓÿzÖÿ„Ûÿ˜åÿ²òÿÌÿÿØÿÿàÿÿçÿÿòÿÿúÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿúÿÿòÿÿçÿÿàÿÿØÿÿÌÿÿ·ôÿ¥ëÿ˜åÿŒßÿ„ÛÿØÿ{ÖÿxÕÿtÓÿpÑÿmÏÿiÍÿfÌÿ_ÌÿXÌÿQÌÿNÌÿGÌÿ@Ìÿ@Ìÿ=Ìÿ=Ìÿ9Ìÿ9Ìÿ6Ìÿ3Ìÿ3Ìü3Ìú3Ìø3Ìõ3Ìó3Ìñ3Ìñ3Ìç3Ì×3ÌÎ3ÌÇ1ÇÃ0ÿ.¿¼-¼¸,¸¶+¶´+´¯)¯«(«¡$¡• •ŽŽ• •À/ÀÜ3Ìÿ9ÌÿXÌÿmÏÿtÓÿzÖÿ„Ûÿ˜åÿ²òÿÌÿÿØÿÿàÿÿçÿÿòÿÿúÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿúÿÿòÿÿçÿÿàÿÿØÿÿÌÿÿ·ôÿ¥ëÿ˜åÿŒßÿ„ÛÿØÿ{ÖÿxÕÿtÓÿpÑÿmÏÿiÍÿfÌÿ_ÌÿXÌÿQÌÿNÌÿGÌÿ@Ìÿ@Ìÿ=Ìÿ=Ìÿ9Ìÿ9Ìÿ9Ìÿ6Ìÿ3Ìü3Ìú3Ìø3Ìõ3Ìó3Ìñ3Ìñ3Ìç3Ì×3ÌÎ3ÌÇ1ÇÃ0ÿ.¿¼-¼¸,¸¶+¶´+´¯)¯«(«¡$¡• •ŽŽ• •À/ÀÜ3Ìÿ9ÌÿXÌÿmÏÿtÓÿzÖÿ„Ûÿ˜åÿ²òÿÌÿÿØÿÿàÿÿçÿÿòÿÿúÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿúÿÿòÿÿçÿÿàÿÿØÿÿÌÿÿ·ôÿ¥ëÿ˜åÿŒßÿ„ÛÿØÿ{ÖÿxÕÿtÓÿpÑÿmÏÿiÍÿfÌÿ_ÌÿXÌÿQÌÿNÌÿNÌÿGÌÿ@Ìÿ@Ìÿ=Ìÿ=Ìÿ9Ìÿ9Ìÿ9Ìÿ3Ìÿ3Ìú3Ìø3Ìõ3Ìó3Ìó3Ìñ3Ìç3Ì×3ÌÎ3ÌÇ1ÇÃ0ÿ.¿¼-¼¸,¸¶+¶´+´¯)¯«(«¡$¡• •ŽŽ• •À/ÀÜ3Ìÿ9ÌÿXÌÿmÏÿtÓÿzÖÿ„Ûÿ˜åÿ²òÿÌÿÿØÿÿàÿÿçÿÿòÿÿúÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿúÿÿòÿÿçÿÿàÿÿØÿÿÌÿÿ·ôÿ¥ëÿ˜åÿŒßÿ„ÛÿØÿ{ÖÿxÕÿtÓÿpÑÿmÏÿiÍÿfÌÿ_ÌÿXÌÿQÌÿNÌÿNÌÿGÌÿDÌÿ@Ìÿ=Ìÿ=Ìÿ=Ìÿ=Ìÿ9Ìÿ6Ìÿ3Ìü3Ìú3Ìõ3Ìõ3Ìó3Ìñ3Ìç3Ì×3ÌÎ3ÌÇ1ÇÃ0ÿ.¿¼-¼¸,¸¶+¶´+´¯)¯ª'ª¡$¡• •ŠŠ• •À/ÀÜ3Ìÿ9ÌÿXÌÿmÏÿtÓÿzÖÿ„Ûÿ˜åÿ²òÿÌÿÿØÿÿàÿÿçÿÿòÿÿúÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿúÿÿòÿÿçÿÿàÿÿØÿÿÌÿÿ·ôÿ¥ëÿ˜åÿŒßÿ„ÛÿØÿ{ÖÿxÕÿtÓÿpÑÿmÏÿiÍÿfÌÿ_ÌÿXÌÿQÌÿNÌÿNÌÿGÌÿDÌÿ@Ìÿ@Ìÿ=Ìÿ9Ìÿ9Ìÿ9Ìÿ6Ìÿ3Ìÿ3Ìú3Ìõ3Ìõ3Ìó3Ìñ3Ìç3Ì×3ÌÎ3ÌÇ1ÇÃ0ÿ.¿¼-¼¸,¸¶+¶´+´¯)¯ª'ª¡$¡• •ŠŠÀ/ÀÜ3Ìÿ9ÌÿXÌÿmÏÿtÓÿzÖÿ„Ûÿ˜åÿ²òÿÌÿÿØÿÿàÿÿçÿÿòÿÿúÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿúÿÿòÿÿçÿÿàÿÿØÿÿÌÿÿ·ôÿ¥ëÿ˜åÿŒßÿ„ÛÿØÿ{ÖÿxÕÿtÓÿpÑÿmÏÿiÍÿfÌÿ_ÌÿXÌÿQÌÿNÌÿNÌÿJÌÿGÌÿGÌÿDÌÿDÌÿ@Ìÿ=Ìÿ9Ìÿ9Ìÿ6Ìÿ3Ìü3Ìú3Ìõ3Ìó3Ìñ3Ìç3Ì×3ÌÎ3ÌÇ1ÇÃ0ÿ.¿¼-¼¸,¸¶+¶´+´¯)¯ª'ª¡$¡• •ŠŠÀ/ÀÜ3Ìÿ9ÌÿXÌÿmÏÿtÓÿzÖÿ„Ûÿ˜åÿ²òÿÌÿÿØÿÿàÿÿçÿÿòÿÿúÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿúÿÿòÿÿçÿÿàÿÿØÿÿÌÿÿ·ôÿ¥ëÿ˜åÿŒßÿ„ÛÿØÿ{ÖÿxÕÿtÓÿrÒÿoÐÿmÏÿiÍÿgÌÿfÌÿbÌÿ_Ìÿ[ÌÿXÌÿXÌÿTÌÿQÌÿJÌÿDÌÿ@Ìÿ=Ìÿ9Ìÿ9Ìÿ3Ìü3Ìú3Ìú3Ìó3Ìñ3Ìç3Ì×3ÌÎ3ÌÇ1ÇÃ0ÿ.¿¼-¼¸,¸¶+¶´+´¯)¯§&§¡$¡• •Ü3Ìÿ9ÌÿXÌÿmÏÿtÓÿzÖÿ„Ûÿ˜åÿ²òÿÌÿÿØÿÿàÿÿçÿÿòÿÿúÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿúÿÿòÿÿçÿÿàÿÿØÿÿÌÿÿ·ôÿ¥ëÿ˜åÿŒßÿ„ÛÿØÿ{ÖÿxÕÿtÓÿrÒÿoÐÿmÏÿiÍÿgÌÿfÌÿbÌÿ_Ìÿ_Ìÿ[ÌÿXÌÿXÌÿTÌÿQÌÿNÌÿJÌÿDÌÿ=Ìÿ9Ìÿ3Ìü3Ìü3Ìú3Ìó3Ìñ3Ìç3Ì×3ÌÎ3ÌÇ1ÇÃ0ÿ.¿¼-¼¸,¸¶+¶´+´¯)¯¤%¤œ#œŒŒÿ9ÌÿXÌÿmÏÿtÓÿzÖÿ„Ûÿ˜åÿ²òÿÌÿÿØÿÿàÿÿçÿÿòÿÿúÿÿúÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿúÿÿòÿÿçÿÿàÿÿØÿÿÌÿÿ·ôÿ¥ëÿ˜åÿŒßÿ„ÛÿØÿ{ÖÿxÕÿtÓÿrÒÿoÐÿmÏÿkÎÿiÍÿgÌÿbÌÿbÌÿ_Ìÿ_Ìÿ[ÌÿXÌÿXÌÿTÌÿQÌÿQÌÿJÌÿ@Ìÿ9Ìÿ3Ìü3Ìü3Ìú3Ìõ3Ìñ3Ìç3Ì×3ÌÉ2ÉÇ1ÇÃ0ÿ.¿¼-¼¸,¸¶+¶´+´¯)¯¡$¡• •ÿXÌÿmÏÿtÓÿzÖÿ„Ûÿ˜åÿ²òÿÌÿÿØÿÿàÿÿçÿÿçÿÿçÿÿçÿÿòÿÿòÿÿòÿÿòÿÿòÿÿúÿÿúÿÿúÿÿúÿÿúÿÿúÿÿúÿÿúÿÿúÿÿúÿÿúÿÿúÿÿòÿÿòÿÿòÿÿòÿÿòÿÿòÿÿçÿÿçÿÿçÿÿçÿÿàÿÿÒÿÿÊþÿ·ôÿ°ñÿ­ïÿ£êÿžèÿ˜åÿ“âÿŽàÿŠÞÿ†ÜÿƒÚÿØÿ}×ÿ{ÖÿzÖÿxÕÿxÕÿvÔÿvÔÿtÓÿrÒÿpÑÿpÑÿoÐÿkÎÿkÎÿgÌÿfÌÿ[ÌÿXÌÿQÌÿNÌÿJÌÿDÌÿ9Ìÿ6Ìø3Ìó3Ìñ3Ìç3Ìç3ÌÞ3ÌÐ3ÌÉ2ÉÀ/Àª'ªÿmÏÿtÓÿzÖÿ„Ûÿ˜åÿ²òÿÌÿÿØÿÿàÿÿçÿÿçÿÿçÿÿòÿÿòÿÿòÿÿòÿÿúÿÿúÿÿúÿÿúÿÿúÿÿúÿÿúÿÿúÿÿúÿÿúÿÿúÿÿúÿÿúÿÿúÿÿúÿÿúÿÿúÿÿúÿÿúÿÿúÿÿúÿÿúÿÿòÿÿòÿÿçÿÿçÿÿàÿÿØÿÿØÿÿÒÿÿÐÿÿÍÿÿÄûÿ¿øÿ¿øÿ·ôÿ¶ôÿ²òÿ¥ëÿ¢êÿšæÿ‘áÿàÿŠÞÿ†Üÿ†Üÿ„ÛÿÙÿÙÿØÿ{Öÿ{ÖÿzÖÿvÔÿtÓÿtÓÿpÑÿoÐÿkÎÿgÌÿgÌÿfÌÿ_Ìÿ_Ìÿ_ÌÿTÌÿNÌÿNÌÿ@Ìÿ9Ìÿ9Ìø3Ìñ3ÌÉ2ÉÿtÓÿzÖÿ„Ûÿ˜åÿ²òÿÌÿÿØÿÿàÿÿàÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿçÿÿàÿÿØÿÿØÿÿÒÿÿÒÿÿÍÿÿÌÿÿÄûÿ¿øÿ½÷ÿ·ôÿ²òÿ°ñÿ¥ëÿšæÿ˜åÿ‘áÿŠÞÿŠÞÿ†Üÿ„ÛÿƒÚÿÙÿØÿØÿ{ÖÿzÖÿvÔÿvÔÿtÓÿtÓÿpÑÿkÎÿkÎÿgÌÿfÌÿfÌÿ_Ìÿ_Ìÿ[ÌÿTÌÿNÌÿJÌÿ@Ìÿ9Ìÿ3Ìñ3ÌÜ3ÌÉ2ÉÃ0ÃÃ0ÃÿNÌÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìÿ3Ìÿ9ÌÿDÌÿNÌÿ_ÌÿgÌÿmÏÿtÓÿxÕÿ}×ÿ}×ÿ„ÛÿŠÞÿŽàÿàÿ•ãÿ˜åÿ¥ëÿ¥ëÿ²òÿ´óÿ½÷ÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÁùÿÁùÿ¹õÿ²òÿ«îÿ¥ëÿ¢êÿ‘áÿŒßÿƒÚÿÙÿ{ÖÿzÖÿxÕÿvÔÿvÔÿtÓÿtÓÿrÒÿrÒÿrÒÿpÑÿoÐÿoÐÿkÎÿiÍÿgÌÿ_Ìÿ_ÌÿXÌÿTÌÿQÌÿNÌÿQÌÿ_ÌÿbÌÿfÌÿfÌÿgÌÿiÍÿkÎÿiÍÿgÌÿgÌÿbÌÿXÌÿGÌÿ9ÌÃ0ÃÃ0ÃÿNÌÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìú3Ìÿ3Ìÿ9ÌÿDÌÿNÌÿ_ÌÿgÌÿmÏÿtÓÿxÕÿ}×ÿ}×ÿ„ÛÿŠÞÿŽàÿàÿ•ãÿœçÿ¥ëÿ«îÿ²òÿ¶ôÿÂúÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿ½÷ÿ·ôÿ­ïÿ¢êÿ—äÿ‘áÿˆÝÿƒÚÿ}×ÿxÕÿrÒÿoÐÿkÎÿiÍÿgÌÿfÌÿXÌÿXÌÿTÌÿQÌÿNÌÿNÌÿJÌÿGÌÿ=Ìÿ6Ìü3Ìñ3Ìó3Ìü3Ìÿ3Ìÿ6Ìÿ=ÌÿQÌÿTÌÿ[ÌÿfÌÿiÍÿkÎÿoÐÿrÒÿvÔÿvÔÿvÔÿvÔÿpÑÿpÑÿiÍÃ0ÃÃ0ÃÿNÌÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìõ3Ìú3Ìÿ3Ìÿ9ÌÿDÌÿNÌÿ_ÌÿgÌÿmÏÿtÓÿxÕÿ}×ÿ}×ÿ„ÛÿŠÞÿŽàÿàÿ•ãÿœçÿ¥ëÿ«îÿ²òÿ·ôÿÂúÿÌÿÿÌÿÿÌÿÿÌÿÿÌÿÿ½÷ÿ­ïÿ¢êÿ•ãÿŽàÿ„ÛÿØÿzÖÿvÔÿoÐÿiÍÿ_ÌÿXÌÿQÌÿJÌÿ@Ìÿ9Ìü3Ìø3Ìõ3Ìñ3Ìñ3Ìì3Ìê3Ìç3Ìç3Ìå3Ìà3Ìå3Ìì3Ìó3Ìø3Ìÿ3ÌÿJÌÿTÌÿ[ÌÿiÍÿkÎÿpÑÿvÔÿvÔÿxÕÿxÕÿxÕÿxÕÿpÑÿtÓÿiÍÃ0ÃÃ0ÃÿNÌÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ9ÌÿDÌÿNÌÿ_ÌÿgÌÿmÏÿtÓÿxÕÿ}×ÿØÿ„ÛÿŠÞÿŽàÿàÿ•ãÿœçÿ¥ëÿ«îÿ²òÿ·ôÿÂúÿÌÿÿÌÿÿÌÿÿÌÿÿ¹õÿ¢êÿ•ãÿŒßÿ„ÛÿØÿzÖÿtÓÿoÐÿgÌÿXÌÿJÌÿ@Ìÿ6Ìÿ3Ìø3Ìó3Ìê3Ìå3Ìà3ÌÞ3ÌÜ3Ì×3ÌÕ3ÌÕ3ÌÐ3ÌÐ3ÌÐ3ÌÜ3ÌÞ3Ìê3Ìì3Ìú3ÌÿGÌÿXÌÿgÌÿkÎÿpÑÿvÔÿxÕÿ{Öÿ}×ÿ}×ÿ{ÖÿxÕÿtÓÿpÑÿbÌÃ0ÃÃ0ÃÿNÌÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ9ÌÿDÌÿNÌÿ_ÌÿgÌÿmÏÿtÓÿxÕÿ}×ÿØÿ†ÜÿŠÞÿŽàÿàÿ•ãÿœçÿ¥ëÿ­ïÿ²òÿ·ôÿÂúÿÌÿÿÌÿÿÌÿÿ¶ôÿ˜åÿŽàÿ„ÛÿØÿzÖÿtÓÿmÏÿgÌÿTÌÿDÌÿ3Ìü3Ìú3Ìõ3Ìñ3Ìê3Ìå3ÌÞ3ÌÜ3Ì×3ÌÕ3ÌÐ3ÌÐ3ÌÎ3ÌÌ3ÌÌ3ÌÌ3ÌÎ3ÌÜ3ÌÞ3Ìê3Ìõ3ÌÿJÌÿfÌÿkÎÿpÑÿvÔÿ{Öÿ}×ÿØÿØÿ}×ÿ{ÖÿtÓÿpÑÿpÑÿ9ÌÃ0ÃÃ0ÃÿNÌÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ9ÌÿDÌÿNÌÿ_ÌÿgÌÿmÏÿtÓÿxÕÿ}×ÿØÿ†ÜÿŠÞÿŽàÿàÿ•ãÿœçÿ¥ëÿ­ïÿ²òÿ·ôÿÂúÿÌÿÿÌÿÿ²òÿ“âÿ„ÛÿØÿzÖÿtÓÿmÏÿgÌÿQÌÿ@Ìú3Ìõ3Ìñ3Ìì3Ìç3Ìà3ÌÞ3ÌÜ3ÌÕ3ÌÐ3ÌÐ3ÌÎ3ÌÌ3ÌÌ3ÌÉ2ÉÉ2ÉÉ2ÉÉ2ÉÌ3ÌÕ3ÌÞ3Ìê3Ìú3ÌÿQÌÿiÍÿpÑÿvÔÿ{Öÿ}×ÿØÿ„ÛÿØÿ}×ÿtÓÿpÑÿpÑÿ9Ì»-»Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿ6Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ9ÌÿDÌÿNÌÿ_ÌÿgÌÿmÏÿtÓÿxÕÿ}×ÿØÿ†ÜÿŠÞÿŽàÿàÿ•ãÿœçÿ¥ëÿ­ïÿ²òÿ·ôÿÂúÿÌÿÿ­ïÿŒßÿØÿzÖÿtÓÿmÏÿgÌÿQÌÿ=Ìõ3Ìê3Ìà3ÌÜ3Ì×3ÌÐ3ÌÌ3ÌÉ2ÉÉ2ÉÇ1ÇÇ1ÇÄ0ÄÄ0ÄÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÇ1ÇÎ3ÌÞ3Ìó3Ìÿ9Ìÿ_ÌÿkÎÿtÓÿ{Öÿ}×ÿŠÞÿŠÞÿ„ÛÿØÿzÖÿpÑÿpÑÿ9Ì»-»Ã0ÃÃ0ÃÿNÌÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ9ÌÿDÌÿNÌÿ_ÌÿgÌÿmÏÿtÓÿxÕÿ}×ÿØÿ†ÜÿŠÞÿŽàÿàÿ•ãÿœçÿ¥ëÿ­ïÿ²òÿ·ôÿÂúÿ§ìÿ„ÛÿzÖÿtÓÿmÏÿgÌÿQÌÿ=Ìó3Ìç3Ì×3ÌÕ3ÌÌ3ÌÇ1ÇÄ0ÄÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÄ0ÄÎ3Ìå3Ìü3ÌÿJÌÿgÌÿpÑÿvÔÿ}×ÿŠÞÿŠÞÿŠÞÿØÿzÖÿtÓÿpÑÿ9Ì»-»Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ9ÌÿDÌÿNÌÿ_ÌÿgÌÿmÏÿtÓÿxÕÿ}×ÿØÿ†ÜÿŠÞÿŽàÿàÿ•ãÿœçÿ¥ëÿ­ïÿ²òÿ·ôÿšæÿØÿvÔÿoÐÿgÌÿQÌÿ=Ìó3Ìå3ÌÕ3ÌÉ2ÉÇ1ÇÃ0ÃÀ/ÀÀ/ÀÀ/ÀÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÉ2ÉÜ3Ìõ3Ìÿ@Ìÿ_ÌÿmÏÿvÔÿ{ÖÿŠÞÿŠÞÿŠÞÿ„Ûÿ{ÖÿtÓÿpÑÿXÌ»-»Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ9ÌÿDÌÿNÌÿ_ÌÿgÌÿoÐÿtÓÿxÕÿ}×ÿØÿ†ÜÿŠÞÿŽàÿàÿ•ãÿœçÿ¥ëÿ­ïÿ²òÿ‘áÿ{ÖÿrÒÿgÌÿQÌÿ=Ìó3Ìå3ÌÐ3ÌÃ0ü-¼¸,¸¸,¸¸,¸¸,¸»-»¿.¿Ã0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÄ0ÄÐ3Ìì3Ìÿ6Ìÿ[ÌÿkÎÿrÒÿzÖÿ„ÛÿŠÞÿŠÞÿ„Ûÿ{ÖÿtÓÿpÑÿXÌ»-»Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ9ÌÿDÌÿNÌÿ_ÌÿiÍÿoÐÿtÓÿxÕÿ}×ÿØÿ†ÜÿŠÞÿŽàÿàÿ•ãÿœçÿ¥ëÿ­ïÿŽàÿxÕÿmÏÿXÌÿ=Ìó3Ìç3ÌÕ3ÌÃ0ø,¸´+´²*²¯)¯¯)¯²*²´+´¸,¸¿.¿Ã0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÄ0ÄÌ3Ìç3Ìú3ÌÿQÌÿiÍÿpÑÿxÕÿØÿ„Ûÿ„Ûÿ„Ûÿ{ÖÿtÓÿpÑÿXÌ»-»Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ9ÌÿDÌÿQÌÿ_ÌÿiÍÿoÐÿtÓÿxÕÿ}×ÿØÿ†ÜÿŠÞÿŽàÿàÿ•ãÿœçÿ¥ëÿ„ÛÿtÓÿgÌÿJÌø3Ìç3ÌÕ3ÌÄ0Ä»-»´+´®)®«(«§&§§&§ª'ª«(«¯)¯¸,¸À/ÀÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÄ0ÄÉ2Éå3Ìõ3ÌÿGÌÿfÌÿoÐÿvÔÿØÿØÿ„Ûÿ„Ûÿ{ÖÿtÓÿpÑÿiÍ»-»Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ9ÌÿDÌÿQÌÿ_ÌÿiÍÿoÐÿtÓÿxÕÿ}×ÿØÿ†ÜÿŠÞÿŽàÿàÿ•ãÿœçÿƒÚÿpÑÿ[Ìÿ3Ìç3Ì×3ÌÄ0ÄÀ/À¶+¶®)®ª'ª¦&¦¡$¡¡$¡¡$¡¦&¦«(«¸,¸À/ÀÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÉ2Éà3Ìñ3ÌÿDÌÿbÌÿoÐÿrÒÿ{ÖÿØÿØÿØÿ{ÖÿtÓÿpÑÿiÍ»-»Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ9ÌÿDÌÿQÌÿ_ÌÿiÍÿoÐÿtÓÿxÕÿ}×ÿØÿ†ÜÿŠÞÿŽàÿàÿ•ãÿØÿmÏÿNÌì3Ì×3ÌÇ1ÇÀ/À¸,¸¯)¯§&§¤%¤ $ ## $ ¤%¤ª'ª¶+¶¿.¿Ã0ÃÃ0ÃÃ0ÃÃ0ÃÃ0ÃÉ2Éà3Ìñ3Ìÿ6Ìÿ_ÌÿoÐÿrÒÿ{Öÿ{ÖÿØÿØÿ{ÖÿtÓÿpÑÿiÍ»-»Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ9ÌÿDÌÿQÌÿ_ÌÿiÍÿoÐÿtÓÿxÕÿ}×ÿØÿ†ÜÿŠÞÿŽàÿàÿØÿiÍÿ=Ìå3ÌÉ2ÉÀ/À¸,¸¯)¯¦&¦¡$¡™"™˜!˜“ “• •#¡$¡ª'ª¶+¶¿.¿Ã0ÃÃ0ÃÃ0ÃÃ0ÃÉ2Éà3Ìñ3Ìÿ3Ìÿ_ÌÿmÏÿpÑÿzÖÿ{Öÿ{ÖÿØÿzÖÿtÓÿpÑÿiÍ»-»Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ9ÌÿDÌÿQÌÿ_ÌÿiÍÿoÐÿtÓÿxÕÿ}×ÿØÿ†ÜÿŠÞÿŽàÿzÖÿ[Ìú3ÌÕ3ÌÀ/À¸,¸¯)¯¦&¦ $ ™"™• •’’ŽŽ˜!˜ $ §&§¶+¶¿.¿Ã0ÃÃ0ÃÃ0ÃÉ2ÉÜ3Ìó3Ìÿ6ÌÿXÌÿiÍÿpÑÿvÔÿzÖÿzÖÿzÖÿvÔÿtÓÿpÑÿiÍ»-»Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ9ÌÿDÌÿQÌÿ_ÌÿiÍÿoÐÿtÓÿxÕÿ}×ÿØÿ†ÜÿŠÞÿzÖÿTÌì3ÌÉ2ɸ,¸¯)¯¦&¦ $ ™"™“ “ŒŒŒŒŽŽ“ “#¦&¦´+´¿.¿Ã0ÃÃ0ÃÃ0ÃÕ3Ìñ3Ìÿ6ÌÿXÌÿgÌÿmÏÿvÔÿvÔÿvÔÿvÔÿvÔÿtÓÿpÑÿiÍ»-»Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ9ÌÿDÌÿQÌÿ_ÌÿiÍÿoÐÿtÓÿxÕÿ}×ÿØÿ†ÜÿxÕÿGÌÞ3Ì¿.¿²*²¦&¦ $ ™"™’’ŽŽŠŠ‡‡‡‡ŽŽ“ “œ#œ¦&¦´+´¿.¿Ã0ÃÃ0ÃÉ2ÉÞ3Ìú3ÌÿQÌÿfÌÿmÏÿpÑÿvÔÿvÔÿvÔÿvÔÿrÒÿpÑÿiÍ»-»Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ9ÌÿDÌÿQÌÿbÌÿiÍÿoÐÿtÓÿxÕÿ}×ÿØÿvÔÿ=ÌÎ3̶+¶§&§ $ ™"™ŒŒ‡‡‡‡ŽŽ“ “™"™¦&¦²*²¿.¿Ã0ÃÃ0ÃÕ3Ìñ3Ìÿ9Ìÿ[ÌÿkÎÿpÑÿtÓÿvÔÿvÔÿvÔÿrÒÿpÑÿiÍ»-»Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ9ÌÿDÌÿQÌÿbÌÿiÍÿpÑÿtÓÿxÕÿ}×ÿpÑñ3ÌÃ0î)® $ ™"™ŒŒ‡‡‡‡ŒŒ“ “™"™¦&¦¯)¯¿.¿Ã0ÃÃ0Ãà3Ìõ3Ìÿ@ÌÿbÌÿoÐÿpÑÿtÓÿvÔÿtÓÿpÑÿiÍÿXÌ»-»Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ9ÌÿGÌÿQÌÿbÌÿiÍÿpÑÿtÓÿxÕÿoÐê3̼-¼¦&¦™"™ŒŒ‡‡‡‡ŒŒ“ “™"™¦&¦¯)¯¿.¿Ã0ÃÉ2Éà3Ìú3ÌÿDÌÿfÌÿoÐÿrÒÿtÓÿrÒÿmÏÿbÌÿ9Ì»-»Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ=ÌÿGÌÿQÌÿbÌÿiÍÿpÑÿtÓÿkÎå3Ì´+´#’’ŒŒ‡‡‡‡ŒŒ“ “™"™¦&¦¯)¯¿.¿Ã0ÃÉ2Éê3Ìÿ3ÌÿDÌÿbÌÿoÐÿpÑÿoÐÿmÏÿbÌÿ9Ì»-»Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ=ÌÿGÌÿQÌÿbÌÿiÍÿpÑÿbÌ×3Ì®)®˜!˜ŽŽŒŒ‡‡ŒŒ“ “™"™¦&¦¯)¯¿.¿Ã0ÃÐ3Ìê3Ìÿ9ÌÿGÌÿbÌÿmÏÿmÏÿiÍÿbÌÿ9Ì»-»Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ=ÌÿGÌÿTÌÿbÌÿiÍÿQÌÕ3̪'ª“ “ŒŒŠŠ‡‡ŒŒ“ “™"™¦&¦²*²¿.¿É2É×3Ìõ3Ìÿ9ÌÿJÌÿ_ÌÿiÍÿ_ÌÿQÌÿ9Ì»-»Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ=ÌÿGÌÿTÌÿbÌÿDÌÐ3̦&¦’’ŒŒˆˆ‡‡ŒŒ“ “™"™¦&¦²*²¿.¿Î3ÌÞ3Ìõ3Ìÿ=ÌÿTÌÿ[ÌÿXÌÿ9Ìì3̧&§Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ=ÌÿGÌÿTÌÿ6ÌÌ3̤%¤’’ŒŒ‡‡‡‡ŒŒ“ “™"™¦&¦²*²Ã0ÃÐ3Ìç3Ìø3Ìÿ=ÌÿQÌÿQÌÿ3ÌÕ3̧&§Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ=ÌÿGÌø3ÌÉ2ɤ%¤ŒŒŒŒ‡‡‡‡ŒŒ“ “™"™ª'ª¶+¶É2É×3Ìç3Ìø3Ìÿ9Ìÿ=Ìü3ÌÀ/À§&§Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìÿ=Ìñ3ÌÇ1Çœ#œŒŒŒŒ‡‡‡‡ŽŽ“ “™"™ª'ª¶+¶É2ÉÞ3Ìì3Ìú3Ìÿ6Ìø3ÌÀ/À§&§Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìÿ3Ìê3ÌÃ0Ù"™ŒŒŒŒ‡‡‡‡ŽŽ“ “™"™«(«»-»Ð3Ìç3Ìì3Ìó3Ìì3ÌÀ/À§&§Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìú3Ìå3ÌÀ/À™"™ŒŒŒŒ‡‡‡‡ŒŒ“ “™"™«(«»-»Þ3Ìì3Ìì3ÌÞ3̯)¯˜!˜Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3Ìõ3Ìà3Ì¿.¿™"™ŒŒŒŒ‡‡‡‡ŒŒ“ “ $ ¯)¯¿.¿ì3Ìì3Ìì3̯)¯˜!˜Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìó3ÌÞ3̼-¼˜!˜ŒŒŒŒˆˆ‡‡ˆˆŒŒ™"™¦&¦¯)¯Ä0Äì3Ìì3̯)¯˜!˜Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3ÌÜ3Ì»-»˜!˜ŒŒŒŒŠŠˆˆ‡‡‡‡ŠŠ“ “ $ ¯)¯¸,¸Î3Ìì3̯)¯˜!˜Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3Ìñ3Ì×3̸,¸˜!˜ŒŒŒŒŠŠŠŠˆˆ‡‡‡‡‡‡ŠŠŒŒ™"™¤%¤¸,¸Î3ÌÞ3̯)¯˜!˜Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3Ìñ3ÌÕ3̶+¶• •ŒŒŒŒŠŠŠŠŠŠˆˆ‡‡ŠŠŒŒ“ “¤%¤«(«¸,¸Î3̯)¯˜!˜Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3Ìñ3ÌÕ3Ì´+´• •ŒŒŒŒŒŒŠŠŠŠŠŠŒŒŒŒ“ “¤%¤«(«¯)¯Ä0į)¯˜!˜Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3Ìñ3ÌÐ3̲*²• •ŒŒŒŒŒŒŒŒŒŒŒŒŒŒ#¯)¯¯)¯¸,¸¯)¯˜!˜Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3Ìñ3ÌÐ3̲*²• •ŒŒŒŒŒŒŒŒŒŒŒŒ#¯)¯¯)¯¸,¸¯)¯˜!˜Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌñ3ÌÐ3̲*²• •ŒŒŒŒŒŒŒŒŒŒ#¯)¯¯)¯¯)¯¯)¯˜!˜Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍÿNÌÐ3̯)¯• •ŒŒŒŒŒŒŒŒ• •¯)¯¯)¯¯)¯¯)¯˜!˜Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿiÍê3̯)¯• •ŒŒŒŒŒŒ• •¯)¯¯)¯¯)¯¯)¯˜!˜Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿiÍÿ6̯)¯• •ŒŒŒŒ• •¯)¯¯)¯¯)¯¯)¯˜!˜Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÿX̯)¯• •¯)¯¯)¯¯)¯¯)¯˜!˜Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿiÍÕ3Ì• •¯)¯¯)¯¯)¯¯)¯˜!˜Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿiÍÿ6̯)¯¯)¯¯)¯¯)¯¯)¯˜!˜Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÿX̯)¯¯)¯¯)¯¯)¯¯)¯˜!˜Ã0ÃÃ0Ãì3ÌÿiÍÿiÍÕ3̯)¯¯)¯¯)¯˜!˜Ã0ÃÃ0Ãì3ÌÿNÌÿiͯ)¯¯)¯˜!˜Ã0ÃÃ0ÃÃ0ÃÃ0ï)¯˜!˜Ã0ÃÃ0ÃÃ0Ø!˜Ã0à \ No newline at end of file
diff --git a/bubbob/images/diamond_big_red.ppm b/bubbob/images/diamond_big_red.ppm
new file mode 100644
index 0000000..f612f53
--- /dev/null
+++ b/bubbob/images/diamond_big_red.ppm
Binary files differ
diff --git a/bubbob/images/diamond_big_yellow.ppm b/bubbob/images/diamond_big_yellow.ppm
new file mode 100644
index 0000000..7ace2ba
--- /dev/null
+++ b/bubbob/images/diamond_big_yellow.ppm
Binary files differ
diff --git a/bubbob/images/digits_0.ppm b/bubbob/images/digits_0.ppm
new file mode 100644
index 0000000..54cc4e0
--- /dev/null
+++ b/bubbob/images/digits_0.ppm
Binary files differ
diff --git a/bubbob/images/door.ppm b/bubbob/images/door.ppm
new file mode 100644
index 0000000..dbc8b69
--- /dev/null
+++ b/bubbob/images/door.ppm
Binary files differ
diff --git a/bubbob/images/dragon_0.ppm b/bubbob/images/dragon_0.ppm
new file mode 100644
index 0000000..14272ec
--- /dev/null
+++ b/bubbob/images/dragon_0.ppm
Binary files differ
diff --git a/bubbob/images/dragon_bubble_0.ppm b/bubbob/images/dragon_bubble_0.ppm
new file mode 100644
index 0000000..7457ffe
--- /dev/null
+++ b/bubbob/images/dragon_bubble_0.ppm
Binary files differ
diff --git a/bubbob/images/extend.ppm b/bubbob/images/extend.ppm
new file mode 100644
index 0000000..bed1ae8
--- /dev/null
+++ b/bubbob/images/extend.ppm
Binary files differ
diff --git a/bubbob/images/extra1.ppm b/bubbob/images/extra1.ppm
new file mode 100644
index 0000000..bf57813
--- /dev/null
+++ b/bubbob/images/extra1.ppm
Binary files differ
diff --git a/bubbob/images/extra2.ppm b/bubbob/images/extra2.ppm
new file mode 100644
index 0000000..0aa664d
--- /dev/null
+++ b/bubbob/images/extra2.ppm
Binary files differ
diff --git a/bubbob/images/extra3.ppm b/bubbob/images/extra3.ppm
new file mode 100644
index 0000000..42d3030
--- /dev/null
+++ b/bubbob/images/extra3.ppm
Binary files differ
diff --git a/bubbob/images/extra4.ppm b/bubbob/images/extra4.ppm
new file mode 100644
index 0000000..c2b1066
--- /dev/null
+++ b/bubbob/images/extra4.ppm
Binary files differ
diff --git a/bubbob/images/extra5.ppm b/bubbob/images/extra5.ppm
new file mode 100644
index 0000000..d162d9e
--- /dev/null
+++ b/bubbob/images/extra5.ppm
@@ -0,0 +1,5 @@
+P6
+# CREATOR: The GIMP's PNM Filter Version 1.1
+32 576
+255
+```fffeeeEXo?QhRRRRRRRRRkkk‘‘‘ŸŸŸ¨¨¨¤¨¬Yˆ¸V…µ–šž•••†††lllWWW†††¦¦¦¸¸¸¿¿¿¿¿¿§ºa‘ÀZ‹½™¯§§§žžžwwwZZZ~~~···ÐÐÐÚÚÚÙÙÙÎÏÏw Åe”Â^Ž¾cŽº¬­­ªªª   rrrZZZuuu¾¾¾çççðððíííååå¿ÉÏm›Æd“Á`À]¾˜¤¯¬¬¬¦¦¦žžžŒŒŒtttZZZeee³³³éééùùùúúúòòòãããš´Çm›Æg–Ãa‘À_¿|šµ®®®ªªª¥¥¥šššˆˆˆqqqYYYWWW›››ÕÕÕñññûûûùùùíííÛÜÜ~§ÊnœÆi—Äd“Á`Àc¼®¯¯¬¬¬§§§ŸŸŸ’’’………lllVVVˆˆˆÈÈÈêêêñññôôôñññæææÃÌÑv¢ÊnœÆi—Äe”Â`À]¾›¦°­­­¨¨¨¡¡¡———ŒŒŒ{{{___‚‚‚¾¾¾èèèøøøôôôíííçççÞÞÞ¢¹Ês Ém›Æi—Äe”Âa‘À]¾€š³­­­¨¨¨¢¢¢™™™‚‚‚lllVVVŸŸŸÛÛÛ÷÷÷ýýýôôôéééâââÙÙÙ§ÈpÇk™Åg–Ãd“Á`À]¾eº­­­¨¨¨¢¢¢ššš………uuu\\\{{{³³³âââúúúúúúîîîâââÛÛÛÁÉÍs Ém›Æi—Äf•Âc’Á_¿\Œ½YŠ¼›¤«§§§¡¡¡ššš†††|||fffTTT7^a‘À†°ÒšÁÜ—¾Ú‹´Ô¬Ðz¦Ìv¢ÊpÇlšÅg–Ãe”Âa‘À^Ž¾Z‹½X‰¼V‡¹R‚´O}°Ly«Fr¤@kœ;c•3YŠ!Br;_Še”ƒ­Ð“»Ø¸×…¯Ñ{§Íw£ËqžÈm›Æi—Äe”Âc’Á`À\Œ½YŠ¼WˆºT„¶Q³O}°Ly«Gs¥Al;d–5\)Gq`kxf”Á~©ÎŠ³Ô¬Ðz¦Ìw£ËqžÈm›Æj˜Äg–Ãd“Áa‘À^Ž¾Z‹½X‰¼V‡¹T„¶Q€²N|¯KxªGs¥Al<e—:a’IS_nnn©³{¥Ê€«Ïw£Ët¡ÉqžÈm›Æj˜Äg–Ãe”Âa‘À_¿\Œ½Z‹½WˆºV†¸SƒµP±Mz¬Jw©Fr¤Al@i˜gt‚YYYlll¯¯¯½ÂƆªÈrŸÈpÇlšÅi—Äg–Ãe”Âc’Á`À]¾Z‹½X‰¼WˆºT„¶R‚´O}°Ly«Iu§Eq£Kpšzƒ‹___iii§§§ÁÁÁÅÆÆ’ª¾i—Ãg–Ãe”Âd“Áa‘À`À]¾Z‹½X‰¼WˆºV†¸SƒµP±Mz¬Iv¨Fr£b|˜…‡ˆŒŒŒ‚‚‚```fffžžž´´´»»»ººº§±¹o—¾a‘À_¿^Ž¾\Œ½YŠ¼WˆºWˆºV†¸SƒµQ€²Mz¬Jw©Px£y…‘………ŠŠŠ~~~YYYbbb•••¨¨¨¯¯¯²²²´´´›©´^Ž¾\Œ½Z‹½X‰¼WˆºV†¸T„¶SƒµQ³O}°Ly«Ht¦t„”ˆˆˆˆˆˆ•••”””wwwTTT___‰‰‰¡¡¡§§§«««®®®t•¶Z‹½YŠ¼WˆºV‡¹T„¶SƒµR‚´Q³O}°Mz¬Iv¨Eq£St˜ŠŠŠ———   ’’’oooTTTxxx™™™¡¡¡¥¥¥œ¢¨W‡¹WˆºV†¸T„¶R‚´Q€²P±O}°O}°Mz¬Jw©Gs¥Cn CnŸ‹‘˜¦¦¦¢¢¢†††___kkkŒŒŒšššžžžsŽ©Q³R‚´Q³Q€²O}°N|¯N{­N{­Ly«Iv¨Fr¤Cn Cn Iv¨w‘«­­­žžžwwwVVVuuuŽŽŽ•––S}«Mz¬N|¯N|¯N|¯Mz¬Q|«‚Žš™Lv¥Eq£BmžAlHt¦Q³_‹¸¥¦¦ŒŒŒccc\\\zzzy†”Ht¦Iu§Iv¨Iv¨Jw©T|§ˆ–”””‚‰‘LqœBmžGs¥O}°V†¸R‚´|‰–rrrVVV___Zo‡BmžCn Do¡Dp¢XzžŠŽŽŽŠŠŠ‰‰‰‡ŠZ| Mz¬R‚´Q³Ht¦VkƒVVV@Vr7^<e—?i™bx‘‰ŠŠŠŠŠ‰‰‰‰‰‰ŠŠŠŒŒŒ’’’™ššw¦P}¯Fr¤9a“<Qm2PvJd‚qw~‚‚‚‰‰‰‰‰‰‰‰‰ŠŠŠŽŽŽ‘‘‘•••———–––}ƒŠJd‚0Ms]]]llluuu~~~………ˆˆˆ‰‰‰ŒŒŒŠŠŠ‰‰‰………{{{iiiTTTTTT]]]ooo{{{{{{ttteeeZZZRRRRRRRRRRRRRRRRRRRRRRRRDDDRRRRRRRRR8G\?L]RRRRRRRRRbbb~~~›››§§§Š®X‰¼Xˆº›§›››‘‘‘uuuYYYbbb‚‚‚§§§¼¼¼ººº·¹ºg“¾_¿]¾k’·®®®ªªªŸŸŸŒŒŒcccRRRRRRžžžÔÔÔÞÞÞÎÎÎËËË«¹Ãg–Ãc’Á`À]¾›¦°®®®ªªª¡¡¡‰‰‰tttRRRRRR§§§äääøøøøøøæææØØØŽ¬ÅlšÅf•Âa‘À_¿€›µ°°°«««¦¦¦¡¡¡ŒŒŒxxxRRR]]]šššéééüüüÿÿÿüüüïïïÖØØy¤ÊpÇi—Äe”Âa‘Àd‘½°±±®®®ªªª§§§šššŠŠŠRRRQQQŽŽŽÔÔÔæææøøøþþþöööçççÀÊÑw£ËpÇk™Åe”Âa‘À^Ž¾œ§±¯¯¯¬¬¬¨¨¨žžž”””ŠŠŠuuuRRR†††ÃÃÃðððøøøîîîðððèèèßßߢ¹Êt¡ÉpÇj˜Äf•Âa‘À_¿‚œ´¯¯¯¬¬¬¨¨¨¡¡¡———ŒŒŒ‚‚‚ffffffÉÉÉîîîÿÿÿÿÿÿîîîêêêáááÛÛÛ„©ÈrŸÈm›Æi—Äf•Âa‘À_¿fº¯¯¯¬¬¬§§§¡¡¡™™™ŽŽŽ†††uuuRRRŸŸŸÑÑÑùùùÿÿÿùùùèèèãããÜÜÜËÎÐs ÈpÇk™Åg–Ãe”Âa‘À^Ž¾Z‹½Ÿ¨¯¬¬¬§§§¡¡¡™™™ŽŽŽˆˆˆfff:J`v˜¸ƒ©É¡ÄÝ ÄÞ“»Ø‡±Ó¬Ð{§Ív¢ÊqžÈlšÅi—Äf•Âd“Á`À]¾Z‹½X‰¼V‡¹R‚´O}°KxªFr£CmœCh“Db‡:Ja5Ide”Âw£Ë—¾ÚšÁÜŒµÕ¬Ð{§Ít¡ÉrŸÈnœÆj˜Äg–Ãe”Âa‘À_¿\Œ½YŠ¼WˆºT„¶Q³O}°Ly«Fr¤Al;d–9a“4IeRRR¥ºs É‹´Ô„®Ñ~©Îz¦Ìw£ËqžÈnœÆk™Åi—Äe”Âc’Á`À]¾Z‹½WˆºV‡¹T„¶Q€²N|¯Ly«Fr¤Bmž;d–YsQRRRRR···¥µÁz¥Êy¥Ìw£Ët¡ÉpÇm›Æk™Åi—Äf•Âd“Áa‘À^Ž¾\Œ½YŠ¼WˆºV†¸SƒµP±Mz¬KxªFr¤EoŸcuˆ………RRRRRR±±±ÀÀÀºÀÅ„¨ÆrŸÈpÇlšÅi—Äg–Ãe”Âd“Áa‘À_¿]¾Z‹½WˆºV‡¹T„¶Q³N|¯Ly«Iv¨PuŸ|…‰‰‰‰‰‰```RRR¦¦¦¹¹¹¿¿¿ÀÁÁ•¬¿i—Ãg–Ãe”Âd“Áa‘À`À^Ž¾\Œ½Z‹½X‰¼WˆºT„¶R‚´O}°Ly«Iv¨h‚Š‹Œ………‘‘‘ŠŠŠ___RRRššš±±±···¹¹¹»»»§±¸o—¾a‘À`À^Ž¾\Œ½Z‹½YŠ¼WˆºV‡¹T„¶R‚´O}°Mz¬R{§~Š–•••ƒƒƒRRRRRR‘‘‘¥¥¥¬¬¬°°°²²²´´´œªµ^Ž¾\Œ½Z‹½X‰¼WˆºV‡¹V†¸T„¶SƒµP±Mz¬Jw©v‡˜ŽŽŽŸŸŸšššxxxRRR   ¦¦¦¨¨¨¬¬¬®®®t•¶\Œ½X‰¼WˆºV†¸T„¶SƒµSƒµR‚´P±N|¯KxªHt¦Vxœ‚‚‚”””¤¤¤¨¨¨”””lllZZZ–––ŸŸŸ¡¡¡¥¥¥›¡§V†¸V‡¹T„¶T„¶Q³Q€²P±O}°O}°Mz¬KxªIv¨Dp¢Al†”¥¥¥±±±¤¤¤RRR–––›››žžžr¨P±Q€²Q€²P±O}°N|¯N{­Mz«KxªIv¨Gs¥Al?išIu§x’«±±±±±±———lllRRR”””•––R|ªLy«Ly«Mz¬Mz¬Mz¬Q|«‚Žš€Œ˜Lv¥Eq£AlAlGs¥R‚´cŽº¬­­ššš{{{RRRRRR‚‚‚z‡”Eq£Fr¤Fr¤Iu§Iu§Rz¤…Œ“’’’ŽŽŽ~…Kp›Cn Iu§Q³WˆºV†¸~‹˜ƒƒƒRRRRRR]rŠ?iš?iš@kœ@kœVw›†‰ŒŠŠŠŠŠŠ‰‰‰‰‰‰‹Ž‘]£P±T„¶Q€²Iv¨awRRR5Id3Z‹;d–<d•e|”‘‘ŠŠŠ‰‰‰ŠŠŠ’’’ššš›œœw¦Mz«Cn 4[Œ3Hd)DkC[zhnuƒƒƒŒŒŒŒŒŒŽŽŽ‘‘‘‘‘‘‰‰‰u{‚2Hf)DkRRR```nnn{{{ƒƒƒrrriiiRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR%Cm Aq Aq%CmRRRRRRrrr–––¥¥¥¤©X‰¼WˆºWˆºR‚´—•••………ooo\\\uuu®®®ÉÉÉÎÎÎÐÐЧ¸ÅlšÅg–Ãd“Áa‘ÀŽ¡²®®®¦¦¦”””|||cccWWWiii¤¤¤ÓÓÓëëëîîîìììææ昵ÌrŸÈk™Åg–Ãe”Â{œº´´´°°°¦¦¦———………nnnZZZnnn°°°âââôôôúúúöööñññßàà{¦ËnœÆi—Äe”Â`À^¼ª¬­¬¬¬¨¨¨¤¤¤™™™ˆˆˆttt___oooªªªÚÚÚòòòûûûûûûôôôêêêÅÎÓt¡ÉnœÆj˜Äe”Âa‘À]¾š¦±®®®«««§§§   •••ŠŠŠxxx___nnn¤¤¤ÙÙÙíííñññôôôòòòêêêßßߤºÊs ÉnœÆi—Äe”Âa‘À^Ž¾ƒœ³¯¯¯¬¬¬¨¨¨¢¢¢ššš………rrrZZZnnn¯¯¯ÙÙÙóóóúúúôôôíííèèèàààØØØ„©ÈqžÈlšÅi—Äe”Âa‘À^Ž¾j’¹¯¯¯¬¬¬¨¨¨¤¤¤œœœ’’’‰‰‰~~~kkkTTTŠŠŠÇÇÇíííûûûûûûñññçççáááÚÚÚÈÌÏrŸÈnœÆj˜Äg–Ãe”Â`À]¾Z‹½¤©­¬¬¬§§§¢¢¢œœœ”””ŒŒŒƒƒƒuuu______‚–ªŽ­Ç§ÇÜŸÃÜšÁܶՅ¯ÑªÏy¥Ìs ÉpÇlšÅg–Ãe”Âc’Á_¿\Œ½Z‹½WˆºV‡¹SƒµP±Mz¬Iu§FpŸFl—IiObxWWW___}—°y¥Ì˜¿Û•½Ú“»Ø†°Ò~©Îy¥Ìs ÉpÇlšÅi—Äf•Âd“Áa‘À^Ž¾Z‹½YŠ¼WˆºT„¶R‚´O}°Mz¬Iu§Do¡?iš;c•MdWWW___¬¬¬­ÂÑ”¼Ù“»Ø¬Ð}¨Íw£Ës ÉpÇlšÅi—Äg–Ãd“Áa‘À_¿\Œ½YŠ¼WˆºV†¸T„¶Q€²O}°Ly«Iu§Dp¢@j›WpŒvwwbbb]]]¦¦¦ÙÙÙË×݈±Òw£Ës ÉqžÈnœÆk™Åi—Äf•Âe”Âa‘À_¿]¾Z‹½X‰¼V‡¹T„¶R‚´P±Mz¬KxªHt¦Ep h{‚‚‚zzzbbb]]]   ÐÐÐßßßÄÉÍ¥ÅnœÆk™Åi—Äg–Ãe”Âd“Áa‘À_¿]¾Z‹½X‰¼WˆºV†¸SƒµQ€²N|¯Ly«Iv¨Rw¡|…ˆˆˆ‰‰‰bbbZZZ•••ÁÁÁÓÓÓÄÄĽ¾¾’©½f•Âe”Âc’Áa‘À_¿]¾Z‹½Z‹½X‰¼WˆºV†¸T„¶Q€²O}°Ly«Iu§gœŠ‹Œ†††ŠŠŠ~~~bbbWWWŒŒŒ²²²Â³³³¶¶¶¸¸¸¤®¶l•½^Ž¾]¾Z‹½YŠ¼WˆºWˆºV‡¹T„¶SƒµQ€²O}°Ly«Rz¥}‰•ŒŒŒ………‰‰‰”””xxxYYY___ƒƒƒ¤¤¤®®®«««­­­°°°±±±–¥±Z‹½YŠ¼WˆºV‡¹T„¶T„¶T„¶R‚´Q€²O}°Mz¬Iv¨v‡˜ŒŒŒˆˆˆ™™™žžžŽŽŽrrrYYYkkk”””¡¡¡¢¢¢¥¥¥¨¨¨«««p’³WˆºV†¸T„¶R‚´Q€²Q€²P±O}°N|¯Ly«Iv¨Fr¤UvšŠŠŠ‘‘‘   §§§œœœbbbYYY‚‚‚’’’›››žžž¡¡¡–œ¢Q²Q³Q³P±O}°N|¯Mz¬Mz¬Ly«Jw©Ht¦Eq£BmžCnŸˆ–¢¢¢¬¬¬¨¨¨‘‘‘tttTTT```|||–––šššn‰¥Mz¬Mz¬Mz¬Mz¬Ly«KxªKw¨Jv§Ht¦Eq£Cn AlCn Iv¨uª¬¬¬§§§———zzz\\\]]]wwwŠŠŠ‘Jt¢Fr¤Gs¥Iu§Iu§Ht¦Ku¤}‰–z†“GpžBmžBmžDp¢Jw©Q³Y‡¶ž ¡”””zzz]]]ZZZrrrewŠ?iš@j›@kœBmžDo¡Lr~…ŠŠŠ‰‰‰}„ŒMtžGs¥KxªO}°Q€²Mz¬qƒ–uuu\\\WWW>[~5\9a“<e—@kœVwšƒ†‰‰‰‰ŠŠŠŒŒŒŽŽŽŽ‘”^€¤Ly«Ly«Eq£;d–>[~YYY,Fk&Iz.S„9]‰_q…‚‚‚†††‰‰‰ŠŠŠŒŒŒŒŒŒŒŒŒŠŠŠhzŽ@f”.S„#Ev+EjDO\VXYeeettt~~~~~~wwwooo\\\UVVBMZRRRRRR___ccc___YYYRRRRRR```fffeeebbbYYYRRRRRRRRRkkk‘‘‘ŸŸŸ¨¨¨¬¬¬­“r«qžžž•••†††lllWWW†††¦¦¦¸¸¸¿¿¿¿¿¿º¶®½„H¸|E­¨ §§§žžžwwwZZZ~~~···ÐÐÐÚÚÚÙÙÙÐÐЫ„¿…G»D²–q®®®ªªª   rrrZZZuuu¾¾¾çççðððíííåååÖÖÔÄ’S¿„F¼E¹ƒK¯®­¬¬¬¦¦¦žžžŒŒŒtttZZZeee³³³éééùùùúúúòòòãããÊÁ«ÅJÁˆH½‚E¼€D²£®®®ªªª¥¥¥šššˆˆˆqqqYYYWWW›››ÕÕÕñññûûûùùùíííÝÝÝÈ­}ÆŽK‰H¿„F¼Eµ’g°°°¬¬¬§§§ŸŸŸ’’’………lllVVVˆˆˆÈÈÈêêêñññôôôñññæææØØÖÊšUÆŽK‰H¿…G¼E¹I¯®­­­­¨¨¨¡¡¡———ŒŒŒ{{{___‚‚‚¾¾¾èèèøøøôôôíííçççÞÞÞÎƲɓMÅJ‰H¿…G½‚Eº~C°£­­­¨¨¨¢¢¢™™™‚‚‚lllVVVŸŸŸÛÛÛ÷÷÷ýýýôôôéééâââÙÙÙȯƒÆKËIÁˆH¿„F¼Eº~C²’k­­­¨¨¨¢¢¢ššš………uuu\\\{{{³³³âââúúúúúúîîîâââÛÛÛÓÓÓÈšYÅJ‰HÀ†G¾ƒF¼€D¹|C¶L¬¬¬§§§¡¡¡ššš†††|||fffTTTxxx¹¹¹âââøøøõõõçççÜÜÜÕÕÕÊĵÆKÄŒJÁˆH¿…G½‚E»D¹{B·yA« ‘¥¥¥   ššš‘‘‘ˆˆˆ~~~qqqTTT}`I¿ŠOÒ¥]Ù¸tØ·uÒ¬jÍ¢^ÊšUÇ’MÅJ‰H¿…G¾ƒF¼E¹|C¸zB¶xA³s?±p=­m<¨i:Ÿd7•^3‰X0|Q,Z< yV9¿„FÐQÕ¨\Ò TÍšPË–NÇ‘LÅJÊIÁˆH¿„F½‚E»D¹{B·yAµw@³s?°o=¬l;¦i9Ÿd7•^3‹Y0‚T.\@&rf\»GË–NÑŸSË–NÊ”MÇ‘LÅJÊIÁˆH¿…G½‚E¼€D¹|C¹{B¶xAµv@²r>¯n<©j:¥h9žc6•^3Y1…V/[QHlll¯¡‹È”QË–NÈ’LÆKÄŒJ‰HÁˆH¿…G¾ƒF¼Eº~C¹{B·yA¶xA³s?²q>­m<¨i:¢f8b6•^3•_6‡ug___iii§§§¾¸¬Å™\ÊI‰HÁˆH¿…G¿„F½‚E¼Eº~C¹{B·yA¶xAµv@²r>¯n<©j:¤g8žc6˜_4”c>Ž„z‚‚‚```fffžžž´´´»¹·¹œs¾„G¾ƒF½‚E¼€D»D¹|C¸zB¶xA¶xAµv@²r>°o=©j:¥h9Ÿd7™`5mRŠˆ†~~~YYYbbb•••¨¨¨¯¯¯²²²³§•º‡P»D¹|C¹{B·yA¶xAµv@³s?²r>±p=­m<¨i:¡e7œg?}pˆˆˆ•••”””wwwTTT___‰‰‰¡¡¡§§§«««®®®° ‡¹{B¸zB¶xAµw@³s?²r>²q>±p=­m<©j:¤g8b6ziŠŠŠ———   ’’’oooTTTxxx™™™¡¡¡¥¥¥¨¨¨±†Z¶xAµv@³s?²q>°o=¯n<­m<­m<©j:¥h9Ÿd7˜_4—jH———¦¦¦¢¢¢†††___kkkŒŒŒšššžžž¢™°p>²q>±p=°o=­m<¬l;©j:©j:¨i:¤g8žc6˜_4˜_4£g9¥ž“­­­žžžwwwVVVuuuŽŽŽ———¢€`©j:¬l;¬l;¬l;©j:¨i:¥i:¤h:¡e7b6—_4•^3¡e7±p=¯j¦¦¦ŒŒŒccc\\\zzz‹‰¡g<¢f8¤g8¤g8¥h9¥h9¢h<—‡x”„u›d:—_4—_4Ÿd7­m<µv@°tB‘rrrVVV___ƒkX—_4˜_4š`5›a5b6h@‡~ŠŠŠ‹‚z˜e>Ÿd7©j:²q>±p=¡e7hVVVVhJ0S-‹Y0ŽZ1’\2—iFŠ†‚‰‰‰‰‰‰ŠŠŠŒŒŒ’Ž‰¦vO­m<¬l;žc6„U.aF,eE(yO+„U.ŒgL‰‡†‰‰‰‰‰‰ŠŠŠŽŽŽ‘‘‘•••–•”ŸxX”]3yO+`B'bF-r\Juuu~~~………ˆˆˆ‰‰‰ŒŒŒŠŠŠ‰‰‰………{{{nYHW?(TTT]]]ooo{{{{{{ttteeeZZZRRRRRRRRRRRRRRRRRRRRRRRRDDDRRRRRRRRRRB4VF7RRRRRRRRRbbb~~~›››§§§­¨ ·{DµyC¦¡™›››‘‘‘uuuYYYbbb‚‚‚§§§¼¼¼ººº»»»·œv¼€Dº~C±•p®®®ªªªŸŸŸŒŒŒcccRRRRRRžžžÔÔÔÞÞÞÎÎÎËËËÇÆÅÀP¾ƒF¼E¹ƒK°¯®®®®ªªª¡¡¡‰‰‰tttRRRRRR§§§äääøøøøøøæææØØØȾ¨ÄŒJÀ†G½‚E¼€D²¤Ž°°°«««¦¦¦¡¡¡ŒŒŒxxxRRR]]]šššéééüüüÿÿÿüüüïïïÛÛÛÈ­}ÆK‰H¿…G½‚E¶“g²²²®®®ªªª§§§šššŠŠŠRRRQQQŽŽŽÔÔÔæææøøøþþþöööçççÙÙ×Ë›VÆKËI¿…G½‚EºƒJ±°¯¯¯¯¬¬¬¨¨¨žžž”””ŠŠŠuuuRRR†††ÃÃÃðððøøøîîîðððèèèßßßÎƲʔMÆKÊIÀ†G½‚E¼€D±¤¯¯¯¬¬¬¨¨¨¡¡¡———ŒŒŒ‚‚‚ffffffÉÉÉîîîÿÿÿÿÿÿîîîêêêáááÛÛÛȯƒÈ’LÅJ‰HÀ†G½‚E¼€D³“k¯¯¯¬¬¬§§§¡¡¡™™™ŽŽŽ†††uuuRRRŸŸŸÑÑÑùùùÿÿÿùùùèèèãããÜÜÜÓÓÓÈšYÆKËIÁˆH¿…G½‚E»D·‚L¯¯¯¬¬¬§§§¡¡¡™™™ŽŽŽˆˆˆfffRRR´´´ÑÑÑúúúüüüðððãããÜÜÜÖÖÖÊĵǑLÄŒJ‰HÀ†G¿„F¼Eº~C¹{B®£”«««¥¥¥   ™™™‘‘‘ˆˆˆtttRRRVA/ÀˆJËšTÛ¼wÝÁÖ´rЩfΡ\Ê—SÈ“MÆŽKÊIÁˆH¿…G½‚E¼€D¹|C¸zB¶xA³s?±p=­m<¨i:c7–_7‹\8†^?VF8VF8¿…GÉ“MÖ©]Ó£VÐQÍšPË–NÇ‘LÆŽKËI‰H¿…G¾ƒF¼Eº~C¹{B¶xAµw@³s?°o=¬l;¨i:žc6—_4‰X0Y1WD2RRR·šsÅJË–N͘OË–NÊ”MÆKÅJËI‰HÀ†G¿„F½‚E»D¹|C¸zB¶xAµv@²r>¯n<©j:¦i9žc6˜_4‰X0lQRQQRRR±±±½¬ŽÆ’OÉ“MÈ’LÆKÄŒJ‰HÁˆH¿…G¿„F½‚E¼€Dº~C¹{B¶xAµw@³s?±p=¬l;¨i:¤g8›a5—_6‘wc‰‰‰```RRR¦¦¦¹¹¹¼¶©Á”ZËI‰HÁˆH¿…G¿„F½‚E¼E»D¹|C¹{B·yA¶xA³s?²q>­m<¨i:¤g8Ÿd7˜fAˆ}t‘‘‘ŠŠŠ___RRRššš±±±···¸·¶ºžv¾„G¾ƒF½‚E¼E»D¹|C¹{B¸zB¶xAµw@³s?²q>­m<©j:¤g8Ÿd7—tX}|•••ƒƒƒRRRRRR‘‘‘¥¥¥¬¬¬°°°²²²³§•»‰Q»D¹|C¹{B·yA¶xAµw@µv@³s?²r>¯n<©j:¥h9ži@‘‚tŸŸŸšššxxxRRR   ¦¦¦¨¨¨¬¬¬®®®° ‡¹|C·yA¶xAµv@³s?²r>²r>²q>¯n<¬l;¦i9¡e7“~l‚‚‚”””¤¤¤¨¨¨”””lllZZZ–––ŸŸŸ¡¡¡¥¥¥§§§°…Yµw@³s?³s?±p=°o=¯n<­m<­m<©j:¦i9¤g8›a5•hG’’’¥¥¥±±±¤¤¤RRR–––›››žžž¡˜Ž®n=°o=°o=¯n<­m<¬l;©j:¨i:¦i9¤g8Ÿd7•^3[2¢f8¦Ÿ”±±±±±±———lllRRR”””———¡`¨i:¨i:©j:©j:©j:¨i:¥j=¢h<¡e7b6•^3•^3Ÿd7²q>³•o®®®ššš{{{RRRRRR‚‚‚œg>žc6žc6¢f8¢f8¢f8Ÿg=”‰}‘†{—b:•^3˜_4¢f8±p=¶xA²}L”””ƒƒƒRRRRRRzu[2[2”]3”]3˜_4˜d=Œƒ{ŠŠŠ‰‰‰‹‚zžh?¤g8¯n<³s?°o=¤g8‡yRRRSNIyO+‰X0‰X0•^3nI‹†‰‰‰ŠŠŠ’’’›–§uM­m<¨i:˜_4{P+SNIW?)oI'zR0‡lX‚€ŒŒŒŒŒŒŽŽŽ‘‘‘Ž‘u_‹[5U:W?)RQQ```nnn{{{ƒƒƒrrriiiRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRUF9UI>RRRRRRRRRrrr–––¥¥¥ªªª¯™{¶xA¶xA¦–‚œœœ•••………ooo\\\uuu®®®ÉÉÉÎÎÎÐÐÐÊÉÇÄ‘QÁˆH¿„FºŒW²²²®®®¦¦¦”””|||cccWWWiii¤¤¤ÓÓÓëëëîîîìììæææÎèȒLËIÁˆH¿…G¶ª—´´´°°°¦¦¦———………nnnZZZnnn°°°âââôôôúúúöööñññâââɨqÆŽK‰H¿…G¼E´’h®®®¬¬¬¨¨¨¤¤¤™™™ˆˆˆttt___oooªªªÚÚÚòòòûûûûûûôôôêêêÙÙÔÉ•PÆŽKÊI¿…G½‚E¹I°¯®®®®«««§§§   •••ŠŠŠxxx___nnn¤¤¤ÙÙÙíííñññôôôòòòêêêßßßÍÄ­É“MÆŽK‰H¿…G½‚E»D±¤¯¯¯¬¬¬¨¨¨¢¢¢ššš………rrrZZZnnn¯¯¯ÙÙÙóóóúúúôôôíííèèèàààØØØÇ®‚Ç‘LÄŒJ‰H¿…G½‚E»D´”m¯¯¯¬¬¬¨¨¨¤¤¤œœœ’’’‰‰‰~~~kkkTTTŠŠŠÇÇÇíííûûûûûûñññçççáááÚÚÚÒÒÒÇ›]ÆŽKÊIÁˆH¿…G¼Eº~C·‚L®®®¬¬¬§§§¢¢¢œœœ”””ŒŒŒƒƒƒuuu______¦¦¦ÏÏÏ÷÷÷øøø÷÷÷êêêàààÚÚÚÓÓÓÊÇÁÆLÄŒJÁˆH¿…G¾ƒF¼€D¹|C¹{B­ Ž«©¨¦¦¦¡¡¡›››•••ŒŒŒ………{{{lllWWWeM7¶xAÍœUÛ½xÚ½zÙºxÒ­kϤ`ÌXÈ•QÇ‘LÄŒJ‰HÀ†G¿„F½‚E»D¹{B¸zB¶xA³s?±r@­qB©pE¢nF™jF‘fG‰cGz\C\I7`\Y²‚RÒ¡UÚ²fÙ±eÒ TÏœQË–NÉ“MÆKÄŒJ‰HÁˆH¿„F½‚E¼€D¹|C¸zB¶xAµv@³s?°o=­m<¨i:¢f8›a5’\2‰X0W4eZQ]]]¦¤¢Íµ„Ø®aÓ¤WË–NÉ“MÇ‘LÆŽKËI‰HÀ†G¿…G½‚E¼€Dº~C¹{B·yAµw@³s?²q>¯n<©j:¦i9¡e7š`5’\2fI{wtbbb]]]   ÐÐÐÓÉ­ÌšSÆKÆŽKËI‰HÁˆH¿…G¿„F½‚E¼€Dº~C¹{B·yA¶xAµv@²r>°o=¬l;¨i:¤g8žc6–_6wc‰‰‰bbbZZZ•••ÁÁÁÓÓÓÁ¼±¿“[ÁˆHÀ†G¿…G¾ƒF½‚E¼€Dº~C¹{B¹{B·yA¶xAµv@³s?°o=­m<¨i:¢f8žc6˜hCˆwŠŠŠ~~~bbbWWWŒŒŒ²²²Â³³³µ´³¸›s½‚F¼E»Dº~C¹{B¸zB¶xA¶xAµw@³s?²r>°o=­m<¨i:¢f8c7–sX…ƒ‚‰‰‰”””xxxYYY___ƒƒƒ¤¤¤®®®«««­­­°°°±£¸K¹{B¸zB¶xAµw@³s?³s?³s?²q>°o=­m<©j:¤g8Ÿg>‘€qˆˆˆ™™™žžžŽŽŽrrrYYYkkk”””¡¡¡¢¢¢¥¥¥¨¨¨«««®›‚¶xAµv@³s?²q>°o=°o=¯n<­m<¬l;¨i:¤g8žc6’|iŠŠŠ‘‘‘   §§§œœœbbbYYY‚‚‚’’’›››žžž¡¡¡¢¢¢«U±p=±p=¯n<­m<¬l;©j:©j:¨i:¥h9¡e7b6—_4—jH•••¢¢¢¬¬¬¨¨¨‘‘‘tttTTT```|||–––šššœ“‰©j;©j:©j:©j:¨i:¦i9¤g8¢f8¡e7b6˜_4•^3˜_4£g9¤’¬¬¬§§§———zzz\\\]]]wwwŠŠŠ›wZžc6Ÿd7¢f8¢f8¡e7Ÿd7že;›d:˜_4—_4—_4›a5¥h9±p=¬‹g¢¢¢”””zzz]]]ZZZrrr‚}‘]6’\2”]3—_4š`5˜_4—b:‚wŒw–a9š`5Ÿd7¦i9­m<°o=©m?‹‰uuu\\\WWWr]M|Q,„U.‹Y0”]3˜_4–b;‹‚zŠŠŠŒŒŒ‡~¡j@¥h9¨i:¨i:b6‰X0r]MYYYW?(aA#oI'yO+„U.Ž_;ˆ€z‰‰‰ŠŠŠŒŒŒŒŒŒ‡€–d>[2‡W/oI'[=!V>'XB.]?#lL/upl~~~~~~wwwqlhaD+Z="WA.RRRRRR___ccc___YYYRRRRRR```fffeeebbbnAMRRRRRRRRRkkk‘‘‘ŸŸŸ¨¨¨¬¬¬¬¾ll¡‘‘•••†††lllWWW†††¦¦¦¸¸¸¿¿¿¿¿¿¼¼¼º““Átt´‰‰§§§žžžwwwZZZ~~~···ÐÐÐÚÚÚÙÙÙÐÐÐÇÆÆÄ‚‚ÃwwÀxx­¬¬ªªª   rrrZZZuuu¾¾¾çççðððíííååå×××Á´´Å}}ÄyyÂvv±ŸŸ¬¬¬¦¦¦žžžŒŒŒtttZZZeee³³³éééùùùúúúòòòãããÐÐПŸÇ€€Ä{{Ãxx·ŒŒ®®®ªªª¥¥¥šššˆˆˆqqqYYYWWW›››ÕÕÕñññûûûùùùíííÝÝÝÏÎÎÈ‹‹ÇÅ}}ÄyyÀzz¯®®¬¬¬§§§ŸŸŸ’’’………lllVVVˆˆˆÈÈÈêêêñññôôôñññæææÙÙÙÉ¿¿Ê‡‡ÇÆ~~ÄyyÂvv°¡¡­­­¨¨¨¡¡¡———ŒŒŒ{{{___‚‚‚¾¾¾èèèøøøôôôíííçççÞÞÞÔÔÔÆ©©É††ÇÆ~~Ä{{ÂvvµŽŽ­­­¨¨¨¢¢¢™™™‚‚‚lllVVVŸŸŸÛÛÛ÷÷÷ýýýôôôéééâââÙÙÙÏÏÏÈ‘‘È„„Ç€€Å}}ÄyyÂvv½{{­­­¨¨¨¢¢¢ššš………uuu\\\{{{³³³âââúúúúúúîîîâââÛÛÛÓÓÓÈÀÀɆ†ÇÆÅ||ÃxxÂuuÁss¬  §§§¡¡¡ššš†††|||fffTTT‹``½ŒŒÒ§§ÜµµÛ¯¯Ö¢¢Ò™™Ï’’ÍŽŽÊˆˆÉ……Ç€€Æ~~Ä{{ÃwwÁttÀrr¿oo½jj»ee¹aaµYY²QQ®IIC[y-;YYÆ~~ÒššÙªªØ§§ÓÏ””Íˉ‰É††ÇÆ~~Å||ÄyyÂuuÁssÀpp¾ll¼ii»ee¹aa¶ZZ²RR¯JJ•EZw4Cqqq»™™Ð––Õ¡¡Ò™™Ï’’Íˉ‰É††ÈƒƒÇ€€Å}}Ä{{ÃwwÁttÀrr¿oo¾ll¼ggºdd¸``¶ZZ²RR¯KK¨HKaMSnnn´´´È¸¸ÑššÍÌˉ‰É††ÈƒƒÇ€€Æ~~Ä{{ÃxxÂuuÁttÀpp¿nn½kk»ff¹bb¸^^µYY²RR­PP†mmYYYlll¯¯¯ÊÊÊËÅÅÈ——ʈˆÉ……ÇÇ€€Æ~~Å||ÄyyÂvvÁttÀrrÀpp¾ll½jj»ee¹aa·\\µXX©[[Œ~~___iii§§§ÁÁÁÈÈÈÁÀÀ¾  Æ€€Æ~~Å}}Ä{{ÄyyÂvvÁttÀrrÀpp¿nn½kk»ff¹bb·]]µYYžnnˆ††ŒŒŒ‚‚‚```fffžžž´´´»»»ººº»»»¸¬¬ÀƒƒÃxxÃwwÂuuÁssÀppÀpp¿nn½kk¼gg¹bb¸^^¯bb’………ŠŠŠ~~~YYYbbb•••¨¨¨¯¯¯²²²´´´¶¶¶´ŸŸÂuuÁttÀrrÀpp¿nn¾ll½kk¼ii»ee¹aa¶[[–||ˆˆˆˆˆˆ•••”””wwwTTT___‰‰‰¡¡¡§§§«««®®®°°°¹„„ÁssÀpp¿oo¾ll½kk½jj¼ii»ee¹bb·]]µXX¤aaŠŠŠ———   ’’’oooTTTxxx™™™¡¡¡¥¥¥¨¨¨«££¿pp¿nn¾ll½jj¼gg»ff»ee»ee¹bb¸^^¶ZZ³TT³TT™ŽŽ¦¦¦¢¢¢†††___kkkŒŒŒšššžžž¡¡¡¬……½jj¼ii¼gg»eeºdd¹bb²jj¸bb·]]µYY³TT³TT·]]­„„­­­žžžwwwVVVuuuŽŽŽ———›ššµhhºddºddºdd¹bb¹aa¦vv–””™……±[[³SS²RR¶[[¼ii½tt¥££ŒŒŒccc\\\zzzŽŽŽš……·\\·]]·]]¸^^´cc›ƒƒ”””ŽŽŽŽ‚‚«[[¶ZZ»ee¿nn½jj›||rrrVVV___{{{žjj³TT´UU´WW§hh’ŠŠŽŽŽŠŠŠ‰‰‰ŒŒŒ’®qq½jj¼ii¶[[ MMVVVbbb NN¯KK¬QQ“qqŠŠŠŠŠŠ‰‰‰‰‰‰ŠŠŠŒŒŒ’’’šššŸžž«{{µYY©HKy:IpLXˆ[[}ww‚‚‚‰‰‰‰‰‰‰‰‰ŠŠŠŽŽŽ‘‘‘•••———–––ˆˆˆ‡[[x:I]]]llluuu~~~………ˆˆˆ‰‰‰ŒŒŒŠŠŠ‰‰‰………{{{iiiTTTTTT]]]ooo{{{{{{ttteeeZZZRRRRRRRRRRRRRRRRRRRRRRRRDDDRRRRRRRRRONOc?HTPQRRRRRRbbb~~~›››§§§­­­±””Àpp­††›››‘‘‘uuuYYYbbb‚‚‚§§§¼¼¼ººº»»»¶´´À~~Âvv¿ww­««ªªªŸŸŸŒŒŒcccRRRRRRžžžÔÔÔÞÞÞÎÎÎËËËÈÈȼ©©Å||ÄyyÂvv±œœ®®®ªªª¡¡¡‰‰‰tttRRRRRR§§§äääøøøøøøæææØØØÍÍ͘˜ÆÄ{{Ãxx¸‰‰°°°«««¦¦¦¡¡¡ŒŒŒxxxRRR]]]šššéééüüüÿÿÿüüüïïïÛÛÛÐÎÎʉ‰ÇÆ~~Ä{{Âzz²°°®®®ªªª§§§šššŠŠŠRRRQQQŽŽŽÔÔÔæææøøøþþþöööçççÚÚÚɽ½ÊˆˆÈ„„Æ~~Ä{{Ãww²¢¢¯¯¯¬¬¬¨¨¨žžž”””ŠŠŠuuuRRR†††ÃÃÃðððøøøîîîðððèèèßßßÕÕÕÆ©©ÊˆˆÈƒƒÆÄ{{Ãxxµ¯¯¯¬¬¬¨¨¨¡¡¡———ŒŒŒ‚‚‚ffffffÉÉÉîîîÿÿÿÿÿÿîîîêêêáááÛÛÛÏÏÏÈ––Ɇ†ÇÆÄ{{Ãxx¼¯¯¯¬¬¬§§§¡¡¡™™™ŽŽŽ†††uuuRRRŸŸŸÑÑÑùùùÿÿÿùùùèèèãããÜÜÜÒÑÑÉÃÃʈˆÈ„„Ç€€Æ~~Ä{{ÃwwÁtt¯¨¨¬¬¬§§§¡¡¡™™™ŽŽŽˆˆˆffff=G·É§§ßÉÉàÇÇÙ¸¸Ó¨¨ÑžžÏ••ÌŽŽË‰‰É……ÇÆÅ}}ÄyyÂvvÁttÀrr¿oo½jj»ee¸``µYY¯TT§QQ™OOc@IZHM††ÍÛ®®Ü±±Ö¤¤Ò™™Ï””ÌËŠŠÊ‡‡ÈƒƒÇ€€Æ~~Ä{{ÃxxÂuuÁssÀpp¾ll¼ii»ee¹aaµYY²RR¯JJ©HKh<GRRRº¸¸Æ££Ö¢¢ÓœœÐ––Ï’’Íˉ‰Ê‡‡È„„ÇÆ~~Å||ÄyyÂvvÁttÀpp¿oo¾ll¼ggºdd¹aaµYY³SS¯JJ˜ddRQRRRR···ÆÆÆɹ¹Í””ÍÌʈˆÉ††È„„ÇÆÅ}}Ä{{ÃwwÂuuÁssÀpp¿nn½kk»ff¹bb¸``µYY±VVŽkk………RRRRRR±±±ÀÀÀÈÈÈÊÄÄǘ˜ÊˆˆÉ……ÇÇ€€Æ~~Å}}Ä{{ÃxxÂvvÁttÀpp¿oo¾ll¼iiºdd¹aa·]]«``Ž‰‰‰‰‰‰```RRR¦¦¦¹¹¹¿¿¿ÂÂÂÂÁÁ¾  Æ€€Æ~~Å}}Ä{{ÄyyÃwwÂuuÁttÀrrÀpp¾ll½jj»ee¹aa·]]¡uuŒŠŠ………‘‘‘ŠŠŠ___RRRššš±±±···¹¹¹»»»»»»¸««ÁÄyyÃwwÂuuÁttÁssÀpp¿oo¾ll½jj»ee¹bb±ee—„„•••ƒƒƒRRRRRR‘‘‘¥¥¥¬¬¬°°°²²²´´´···´žžÂuuÁttÀrrÀpp¿oo¿nn¾ll½kk»ff¹bb¸^^™ŽŽŽŸŸŸšššxxxRRR   ¦¦¦¨¨¨¬¬¬®®®±±±¹……ÀrrÀpp¿nn¾ll½kk½kk½jj»ffºdd¸``¶[[¦ff‚‚‚”””¤¤¤¨¨¨”””lllZZZ–––ŸŸŸ¡¡¡¥¥¥§§§ª¢¢¿oo¾ll¾ll¼ii¼gg»ff»ee»ee¹bb¸``·]]´WW²RR•ŠŠ¥¥¥±±±¤¤¤RRR–––›››žžž   ªƒƒ¼gg¼gg»ff»eeºdd¹bb²ii¸aa·]]¶ZZ²RR±OO·\\®††±±±±±±———lllRRR”””———š™™´gg¹aa¹bb¹bb¹bb¹aa¦vv•““™……±[[²RR²RR¶ZZ½jjÀxx­««ššš{{{RRRRRR‚‚‚–µYYµYY·\\·\\³aa™€€’’’ŽŽŽŠŠŠ«\\·\\¼iiÀpp¿nn~~ƒƒƒRRRRRR›ee±OO²QQ²QQ¥ccŽ††ŠŠŠŠŠŠ‰‰‰‰‰‰‘‘‘–‘‘±vv¾ll¼gg·]]¦YYRRRRRR”JS¯JJªNN•tt‘‘‘ŠŠŠ‰‰‰ŠŠŠ’’’šššœœœŸžž¨xx³TT‘E^r3@d@I€STtnnƒƒƒŒŒŒŒŒŒŽŽŽ‘‘‘‘‘‘‰‰‰c@Ir3@RRR```nnn{{{ƒƒƒrrriiiRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRYJNr2>YJNRRRRRRrrr–––¥¥¥ªªª®®®¹||Àpp¶vvœœœ•••………ooo\\\uuu®®®ÉÉÉÎÎÎÐÐÐÌÌÌÀ²²Ç€€Å}}Ä{{²¡¡®®®¦¦¦”””|||cccWWWiii¤¤¤ÓÓÓëëëîîîìììæææ×××Æ¢¢È„„Ç€€Æ~~»´´´°°°¦¦¦———………nnnZZZnnn°°°âââôôôúúúöööñññâââÐÏÏÉŠŠÇÆ~~ÄyyÀxx­««¬¬¬¨¨¨¤¤¤™™™ˆˆˆttt___oooªªªÚÚÚòòòûûûûûûôôôêêêÜÜÜȽ½Ê‡‡ÈƒƒÆ~~Ä{{Âvv±¡¡®®®«««§§§   •••ŠŠŠxxx___nnn¤¤¤ÙÙÙíííñññôôôòòòêêêßßßÕÕÕƪªÊ‡‡ÇÆ~~Ä{{Ãwwµ¯¯¯¬¬¬¨¨¨¢¢¢ššš………rrrZZZnnn¯¯¯ÙÙÙóóóúúúôôôíííèèèàààØØØÏÏÏÇ””É……ÇÆ~~Ä{{Ãww¼¯¯¯¬¬¬¨¨¨¤¤¤œœœ’’’‰‰‰~~~kkkTTTŠŠŠÇÇÇíííûûûûûûñññçççáááÚÚÚÑÐÐÇÀÀɇ‡ÈƒƒÇ€€Æ~~ÄyyÂvvÁtt®§§¬¬¬§§§¢¢¢œœœ”””ŒŒŒƒƒƒuuu___d[[­‡‡Ç©©ÞÊÊÞÈÈÝÃÃÖ³³Ò¦¦ÏœœÎ’’ËŒŒÊˆˆÉ……Ç€€Æ~~Å||ÃxxÂuuÁttÀpp¿oo½kk»ff¹bb¶\\±XX©VVUU„VVWWW___¬ŸŸÌššÛ¯¯Ú­­ÙªªÔžžÐ––Α‘ÌŒŒÊˆˆÉ……ÇÆÅ}}Ä{{ÃwwÁttÁssÀpp¾ll½jj»ee¹bb·\\´UU±OO®IIŽUUWWW___¬¬¬ÛÚÚÛÅÅÙªªÒ™™Ð••ÍÌŒŒÊˆˆÉ……ÇÇ€€Å}}Ä{{ÃxxÂuuÁssÀpp¿nn¾ll¼gg»ee¹aa·\\´WW±PP–aawvvbbb]]]¦¦¦ÙÙÙìììÔÈÈÌ’’ÌŒŒË‰‰Ê‡‡È„„ÇÆÆ~~Ä{{ÃxxÂvvÁttÀrr¿oo¾ll½jj»ff¹bb¸``¶[[²WW“qq‚‚‚zzzbbb]]]   ÐÐÐßßßÒÒÒÆÀÀÆ••È„„ÇÇ€€Æ~~Å}}Ä{{ÃxxÂvvÁttÀrrÀpp¿nn½kk¼ggºdd¹aa·]]¬ccŽˆˆˆ‰‰‰bbbZZZ•••ÁÁÁÓÓÓÄÄÄ¿¿¿¾½½½žžÅ~~Å||Ä{{ÃxxÂvvÁttÁttÀrrÀpp¿nn¾ll¼gg»ee¹aa¶\\ ttŒŠŠ†††ŠŠŠ~~~bbbWWWŒŒŒ²²²Â³³³¶¶¶¸¸¸¸¸¸µ§§¿}}ÂvvÁttÁssÀppÀpp¿oo¾ll½kk¼gg»ee¹aa±dd–ƒƒŒŒŒ………‰‰‰”””xxxYYY___ƒƒƒ¤¤¤®®®«««­­­°°°±±±²²²²ÁssÀpp¿oo¾ll¾ll¾ll½jj¼gg»ee¹bb·]]™ŒŒŒˆˆˆ™™™žžžŽŽŽrrrYYYkkk”””¡¡¡¢¢¢¥¥¥¨¨¨«««¬¬¬¶‚‚¿nn¾ll½jj¼gg¼gg»ff»eeºdd¹aa·]]µYY¥ccŠŠŠ‘‘‘   §§§œœœbbbYYY‚‚‚’’’›››žžž¡¡¡¢¢¢¥››¼ii¼ii»ff»eeºdd¹bb¹bb¹aa¸^^¶[[µXX³SS³TT—ŒŒ¢¢¢¬¬¬¨¨¨‘‘‘tttTTT```|||–––ššš›››§||¹bb¹bb¹bb¹aa¸``·]]·\\¶[[µXX³TT²RR³TT·]]­ƒƒ¬¬¬§§§———zzz\\\]]]wwwŠŠŠ‘ŽŽ²\\¶ZZ·\\·\\¶[[µ\\ tt’‚‚˜uu¯VV³SS´WW¸^^¼ii»pp¢  ”””zzz]]]ZZZrrr“pp±PP²QQ³SS´UU¬\\ŠŠŠ‰‰‰‰‰‰……®bb¸``»ee¼gg¹bb—zzuuu\\\WWWkkk›KN©HK¯KK²QQ¦ccŠ„„‰‰‰ŠŠŠŒŒŒŽŽŽ”””—””§zz¸bbµXX¯JJ„JZYYYTRSz;K‰=RD\–\\‚€€†††‰‰‰ŠŠŠŒŒŒŒŒŒŒŒŒŠŠŠ………‚yyRUs9Go5As4AiFPeeettt~~~~~~wwwooo\\\VVVRRRRRRRRR___ccc___YYYRRRRRR```fffeeebbbYYYRRRRRRRRRkkk‘‘‘ŸŸŸ¨¨¨¬¬¬u¤ur ržžž•••†††lllWWW†††¦¦¦¸¸¸¿¿¿¿¿¿°¹°I§IEŸE¡«¡§§§žžžwwwZZZ~~~···ÐÐÐÚÚÚÙÙÙÐÐЋ»‹H©HD¢Dt¨t®®®ªªª   rrrZZZuuu¾¾¾çççðððíííåååÕÖÕX²XG¨GE¥EK¤K­¯­¬¬¬¦¦¦žžžŒŒŒtttZZZeee³³³éééùùùúúúòòòããã³Ç³N°NJ«JE¦ED£D­®®®ªªª¥¥¥šššˆˆˆqqqYYYWWW›››ÕÕÕñññûûûùùùíííÝÝ݇À‡O±OK¬KG¨GE¥Ej¨j°°°¬¬¬§§§ŸŸŸ’’’………lllVVVˆˆˆÈÈÈêêêñññôôôñññæææ×Ø×]¹]O±OK¬KH©HE¥EI£I­¯­­­­¨¨¨¡¡¡———ŒŒŒ{{{___‚‚‚¾¾¾èèèøøøôôôíííçççÞÞ޻˻SµSN°NK¬KH©HE¦EC¡C’¬’­­­¨¨¨¢¢¢™™™‚‚‚lllVVVŸŸŸÛÛÛ÷÷÷ýýýôôôéééâââÙÙÙÀP²PM®MJ«JG¨GE¥EC¡Cn¦n­­­¨¨¨¢¢¢ššš………uuu\\\{{{³³³âââúúúúúúîîîâââÛÛÛÓÓÓ`¸`N°NK¬KIªIF§FD£DC CL L¬¬¬§§§¡¡¡ššš†††|||fffTTTxxx¹¹¹âââøøøõõõçççÜÜÜÕÕջȻP²PM¯MJ«JH©HE¦ED¢DBžBAœA“§“¥¥¥   ššš‘‘‘ˆˆˆ~~~qqqTTT,i,H©H_Ã_xÌxsÊsbÄbY¼YV¸VQ³QN°NK¬KH©HF§FE¥EC CBBA›A?–?=’=<<:Š:7ƒ73{30r0,i, M 1g1G¨G[¾[iÇi^Á^X»XV¸VQ³QN°NL­LJ«JG¨GE¦ED¢DBžBAœA@š@?–?=‘=;Ž;9‰97ƒ73{30s0.n.&P&YlYF£FV¸V]À]V¸VT¶TQ³QN°NL­LJ«JH©HE¦ED£DC CBžBA›A@˜@>•><<:‹:9ˆ96‚63{31u1/p/HWHlllŽªŽV´VV¸VR´RP²PM¯MK¬KJ«JH©HF§FE¥EC¡CBžBAœAA›A?–?>”><<:Š:8…8663{36{6g~g___iii§§§¯¼¯c¶cL­LK¬KJ«JH©HG¨GE¦EE¥EC¡CBžBAœAA›A@˜@>•><<:‹:8†86‚64}4>|>zŠz‚‚‚```fffžžž´´´¸º¸x¯xH¨HF§FE¦ED£DD¢DC CBBA›AA›A@˜@>•>=‘=:‹:9ˆ97ƒ75~5RR†‰†~~~YYYbbb•••¨¨¨¯¯¯²²²—¯—R¦RD¢DC CBžBAœAA›A@˜@?–?>•>=’=<<:Š:7„7?ƒ?p†pˆˆˆ•••”””wwwTTT___‰‰‰¡¡¡§§§«««®®®Š«ŠBžBBBA›A@š@?–?>•>>”>=’=<<:‹:8†866i†iŠŠŠ———   ’’’oooTTTxxx™™™¡¡¡¥¥¥¨¨¨Z ZA›A@˜@?–?>”>=‘=<<<<<<:‹:9ˆ97ƒ74}4H‚H———¦¦¦¢¢¢†††___kkkŒŒŒšššžžžŸ>’>>”>=’==‘=<<;Ž;:‹::‹::Š:8†86‚64}44}49†9”£”­­­žžžwwwVVVuuuŽŽŽ———`”`:‹:;Ž;;Ž;;Ž;:‹::Š::‰::ˆ:7„7664|43{37„7=’=l£l¦¦¦ŒŒŒccc\\\zzz‰‰<…<8…88†88†89ˆ99ˆ9<‡<x‘xuu::4|44|47ƒ7<<@˜@B•B‘rrrVVV___XwX4|44}45~55566@ƒ@~~ŠŠŠz‡z>€>7ƒ7:‹:>”>=’=7„7VtVVVV0[0-k-0s01v12x2FF‚ˆ‚‰‰‰‰‰‰ŠŠŠŒŒŒŠŠOO<<;Ž;6‚6.o.,U,(V(+f+.o.L{L†ˆ†‰‰‰‰‰‰ŠŠŠŽŽŽ‘‘‘•••”–”XŽX3y3+f+'S'-V-JhJuuu~~~………ˆˆˆ‰‰‰ŒŒŒŠŠŠ‰‰‰………{{{HeH(M(TTT]]]ooo{{{{{{ttteeeZZZRRRRRRRRRRRRRRRRRRRRRRRRDDDRRRRRRRRR4K47O7RRRRRRRRRbbb~~~›››§§§¡«¡DžDCœCš¤š›››‘‘‘uuuYYYbbb‚‚‚§§§¼¼¼ººº»»»z®zD£DC¡Cs§s®®®ªªªŸŸŸŒŒŒcccRRRRRRžžžÔÔÔÞÞÞÎÎÎËËËÆÇÆS­SF§FE¥EK¤K®°®®®®ªªª¡¡¡‰‰‰tttRRRRRR§§§äääøøøøøøæææØØØ°Ä°M¯MIªIE¦ED£D‘®‘°°°«««¦¦¦¡¡¡ŒŒŒxxxRRR]]]šššéééüüüÿÿÿüüüïïïÛÛÛ‡À‡P²PK¬KH©HE¦Ek©k²²²®®®ªªª§§§šššŠŠŠRRRQQQŽŽŽÔÔÔæææøøøþþþöööçççØÙØ^º^P²PM®MH©HE¦EJ¤J¯±¯¯¯¯¬¬¬¨¨¨žžž”””ŠŠŠuuuRRR†††ÃÃÃðððøøøîîîðððèèèßß߻˻T¶TP²PL­LIªIE¦ED£D“­“¯¯¯¬¬¬¨¨¨¡¡¡———ŒŒŒ‚‚‚ffffffÉÉÉîîîÿÿÿÿÿÿîîîêêêáááÛÛÛÀR´RN°NK¬KIªIE¦ED£Do§o¯¯¯¬¬¬§§§¡¡¡™™™ŽŽŽ†††uuuRRRŸŸŸÑÑÑùùùÿÿÿùùùèèèãããÜÜÜÓÓÓ`¸`P²PM®MJ«JH©HE¦ED¢DL¢L¯¯¯¬¬¬§§§¡¡¡™™™ŽŽŽˆˆˆfffRRR´´´ÑÑÑúúúüüüðððãããÜÜÜÖÖֻȻQ³QM¯MK¬KIªIG¨GE¥EC¡CBžB–ª–«««¥¥¥   ™™™‘‘‘ˆˆˆtttRRR8O8e®ed»d†Ï†…Ñ…mÈm^Á^Y¼YT¶TR´RO±OL­LJ«JH©HE¦ED£DC CBBA›A?–?=’=<<:Š:7‚77|78u8?s?8O82N2H©HSµSkÇk`Ã`[¾[X»XV¸VQ³QO±OM®MK¬KH©HF§FE¥EC¡CBžBA›A@š@?–?=‘=;Ž;:Š:6‚64|40r01u12O2RRRu­uN°NV¸VWºWV¸VT¶TP²PN°NM®MK¬KIªIG¨GE¦ED¢DC CBBA›A@˜@>•><<:‹:9‰96‚64}40r0Q~QQRQRRR±±±”·”T´TSµSR´RP²PM¯MK¬KJ«JH©HG¨GE¦ED£DC¡CBžBA›A@š@?–?=’=;Ž;:Š:8†8556|6c…c‰‰‰```RRR¦¦¦¹¹¹­º­_±_M®MK¬KJ«JH©HG¨GE¦EE¥ED¢DC CBžBAœAA›A?–?>”><<:Š:8†87ƒ7AAtƒt‘‘‘ŠŠŠ___RRRššš±±±···¶¸¶{±{H¨HF§FE¦EE¥ED¢DC CBžBBBA›A@š@?–?>”><<:‹:8†87ƒ7X†X|~|•••ƒƒƒRRRRRR‘‘‘¥¥¥¬¬¬°°°²²²˜¯˜S§SD¢DC CBžBAœAA›A@š@@˜@?–?>•><<:‹:9ˆ9@…@t‹tŸŸŸšššxxxRRR   ¦¦¦¨¨¨¬¬¬®®®Š«ŠC CAœAA›A@˜@?–?>•>>•>>”><<;Ž;9‰97„7lŠl‚‚‚”””¤¤¤¨¨¨”””lllZZZ–––ŸŸŸ¡¡¡¥¥¥§§§YŸY@š@?–??–?=’==‘=<<<<<<:‹:9‰98†855G€G’’’¥¥¥±±±¤¤¤RRR–––›››žžžž===‘==‘=<<<<;Ž;:‹::Š:9‰98†87ƒ73{32w28…8•¤•±±±±±±———lllRRR”””———`“`:Š::Š::‹::‹::‹::Š:=‰=<‡<7„7663{33{37ƒ7>”>r¨r®®®ššš{{{RRRRRR‚‚‚>ƒ>6‚66‚68…88…88…8=…=}}{Œ{:~:3{34}48…8=’=A›ALœL”””ƒƒƒRRRRRRu~u2w22w23y33y34}4=={ˆ{ŠŠŠ‰‰‰z‡z?„?8†8<<?–?=‘=8†8y„yRRRIQI+f+0r00r03{3I‡I‰‰‰‰ŠŠŠ’’’™M‘M<<:Š:4}4+h+IQI)M)'^'0h0XzX€‚€ŒŒŒŒŒŒŽŽŽ‘‘‘ŽŽ_„_5t5I)M)QRQ```nnn{{{ƒƒƒrrriiiRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRQRQ9O9>P>RRRRRRRRRrrr–––¥¥¥ªªª~§~A›AA›A„¡„œœœ•••………ooo\\\uuu®®®ÉÉÉÎÎÎÐÐÐÈÊÈU°UJ«JG¨GY©Y²²²®®®¦¦¦”””|||cccWWWiii¤¤¤ÓÓÓëëëîîîìììæææ³Ê³R´RM®MJ«JH©Hš²š´´´°°°¦¦¦———………nnnZZZnnn°°°âââôôôúúúöööñññâââz¿zO±OK¬KH©HE¥Ek¨k®®®¬¬¬¨¨¨¤¤¤™™™ˆˆˆttt___oooªªªÚÚÚòòòûûûûûûôôôêêê×Ù×V·VO±OL­LH©HE¦EI£I®°®®®®«««§§§   •••ŠŠŠxxx___nnn¤¤¤ÙÙÙíííñññôôôòòòêêêßß߶ʶSµSO±OK¬KH©HE¦ED¢D’­’¯¯¯¬¬¬¨¨¨¢¢¢ššš………rrrZZZnnn¯¯¯ÙÙÙóóóúúúôôôíííèèèàààØØØ‹¿‹Q³QM¯MK¬KH©HE¦ED¢Dp©p¯¯¯¬¬¬¨¨¨¤¤¤œœœ’’’‰‰‰~~~kkkTTTŠŠŠÇÇÇíííûûûûûûñññçççáááÚÚÚÒÒÒd¸dO±OL­LJ«JH©HE¥EC¡CL¢L®®®¬¬¬§§§¢¢¢œœœ”””ŒŒŒƒƒƒuuu______¦¦¦ÏÏÏ÷÷÷øøø÷÷÷êêêàààÚÚÚÑÒѽǽQ²QM¯MJ«JH©HF§FD£DC CBžB¨¨ª¨¦¦¦¡¡¡›››•••ŒŒŒ………{{{lllWWW<[<h£h‡Á‡­Ö­ Ó –ЖyÆydÁdZ»ZSµSP²PM¯MK¬KIªIG¨GE¦ED¢DBžBBBA›A?–?@”@B‘BEŽEFŠFFƒFG}GGwGClC7T7O^OKžK_Ã_zÍzxÌx^Á^Z½ZV¸VSµSP²PM¯MK¬KJ«JG¨GE¦ED£DC CBBA›A@˜@?–?=‘=<<:Š:8…8552x20r04n4Q`Q]]] ¥ ÄsÊsbÄbV¸VSµSQ³QO±OM®MK¬KIªIH©HE¦ED£DC¡CBžBAœA@š@?–?>”><<:‹:9‰97„75~52x2I{Itytbbb]]]   ÐÐйй[º[P²PO±OM®MK¬KJ«JH©HG¨GE¦ED£DC¡CBžBAœAA›A@˜@>•>=‘=;Ž;:Š:8†86‚66|6c„c‰‰‰bbbZZZ•••ÁÁÁÓÓÓµ¿µ`¯`J«JIªIH©HF§FE¦ED£DC¡CBžBBžBAœAA›A@˜@?–?=‘=<<:Š:8…86‚6CCw„wŠŠŠ~~~bbbWWWŒŒŒ²²²Â³³³´µ´y¯yF¦FE¥ED¢DC¡CBžBBBA›AA›A@š@?–?>•>=‘=<<:Š:8…87‚7X…X‚„‚‰‰‰”””xxxYYY___ƒƒƒ¤¤¤®®®«««­­­°°°’¬’K¢KBžBBBA›A@š@?–??–??–?>”>=‘=<<:‹:8†8>„>q‰qˆˆˆ™™™žžžŽŽŽrrrYYYkkk”””¡¡¡¢¢¢¥¥¥¨¨¨«««…§…A›A@˜@?–?>”>=‘==‘=<<<<;Ž;:Š:8†86‚6iˆiŠŠŠ‘‘‘   §§§œœœbbbYYY‚‚‚’’’›››žžž¡¡¡¢¢¢U™U=’==’=<<<<;Ž;:‹::‹::Š:9ˆ97„7664|4H‚H•••¢¢¢¬¬¬¨¨¨‘‘‘tttTTT```|||–––šššŠ™Š;‹;:‹::‹::‹::Š:9‰98†88…87„7664}43{34}49†9“¢“¬¬¬§§§———zzz\\\]]]wwwŠŠŠZŠZ6‚67ƒ78…88…87„77ƒ7;ƒ;::4}44|44|4559ˆ9=’=hŸh¢¢¢”””zzz]]]ZZZrrr}€}6x62x23y34|45~54}4:~:wˆww‡w9}95~57ƒ79‰9<<=‘=??‰‰uuu\\\WWWMhM,i,.o.0s03y34}4;};z‡zŠŠŠŒŒŒ~~@†@9ˆ9:Š::Š:660r0MhMYYY(M(#S#'^'+f+.o.;x;z…z‰‰‰ŠŠŠŒŒŒŒŒŒ€Š€>>2w2/q/'^'!N!'L'.O.#P#/^/lrl~~~~~~wwwhnh+T+"N".M.RRRRRR___ccc___YYYRRRRRR```fffeeemLmeEeRRRRRRRRRkkk‘‘‘ŸŸŸ¨¨¨¬¦ª¾l¥¼h¤Ÿ˜œ•••†††lllWWW†††¦¦¦¸¸¸¿¿¿¿¿¿»›±Äv«Áo¨±Œ¥§§§žžžwwwZZZ~~~···ÐÐÐÚÚÚÙÙÙÏÎÏÈŠ´Æz¬Ãr©¾u§­¬­ªªª   rrrZZZuuu¾¾¾çççðððíííåååÏÄÍÉ‚²Åx¬ÄuªÂq©°žª¬¬¬¦¦¦žžžŒŒŒtttZZZeee³³³éééùùùúúúòòòãããȦ¿É‚²Ç|®Äv«Ãtª·‰¨®®®ªªª¥¥¥šššˆˆˆqqqYYYWWW›››ÕÕÕñññûûûùùùíííÜÛÜÌ‘ºÊƒ³Ç}®Åx¬ÄuªÀv¨¯®¯¬¬¬§§§ŸŸŸ’’’………lllVVVˆˆˆÈÈÈêêêñññôôôñññæææÑÈÐÍŠ¸Êƒ³Ç}®Æz¬ÄuªÂq©° «­­­¨¨¨¡¡¡———ŒŒŒ{{{___‚‚‚¾¾¾èèèøøøôôôíííçççÞÞÞÊ­Ã̇¶É‚²Ç}®Æz¬Äv«Âq©µ‹§­­­¨¨¨¢¢¢™™™‚‚‚lllVVVŸŸŸÛÛÛ÷÷÷ýýýôôôéééâââÙÙÙË’ºÊ„³È°Ç|®Åx¬ÄuªÂq©½w§­­­¨¨¨¢¢¢ššš………uuu\\\{{{³³³âââúúúúúúîîîâââÛÛÛÍÅÌ̇¶É‚²Ç}®Æ{­Åw«ÃtªÂp¨Án§¬Ÿ¨§§§¡¡¡ššš†††|||fffTTTDÄv«ÔšÃÜ®ÑÛªÏÖŸÇÒ•ÀÏŽ»ÍŠ¸Ê„³É€±Ç|®Æz¬Äv«Ãr©Áo¨Àl§¿j¦½d£»_¡¹[ŸµRœªL˜•I•†@†f.f‰H‰Æz¬Ò—ÁÙ§ÌØ£ÊÓ™Âϼ͋¸Ë…´É‚²Ç}®Æz¬Åw«ÄuªÂp¨Án§Àk¦¾f¤¼c£»_¡¹[Ÿ¶Sœ­M™˜I•ŠCŠh4h}cvÄz¬Ð’½ÕžÆÒ•ÀÏŽ»Í‹¸Ë…´É‚²È~¯Ç|®Åx¬Äv«Ãr©Áo¨Àl§¿j¦¾f¤¼b¢º^¡¸YŸ¶Sœ­M™›J–‘G‘^L^nnn´£¯ÌŽ¹Ñ”¿Í‹¸Ì‰·Ë…´É‚²È~¯Ç|®Æz¬Äv«ÃtªÂp¨Áo¨Àk¦¿i¥½e¤»a¢¹\ ¸XžµRœ­M™¡L•†kYYYlll¯¯¯ÆÀÅÊ—»Ë†µÊ„³É€±Ç}®Ç|®Æz¬Åw«ÄuªÂq©Áo¨Àl§Àk¦¾f¤½d£»_¡¹[Ÿ·VµQ›©U”Œ~ˆ___iii§§§ÁÁÁÆÅÆ¿žµÇ}®Ç|®Æz¬Åx¬Äv«ÄuªÂq©Áo¨Àl§Àk¦¿i¥½e¤»a¢¹\ ·WžµR›žkˆ†ˆŒŒŒ‚‚‚```fffžžž´´´»»»ººº¹¬¶Á¬Äv«ÃtªÃr©Âp¨Án§Àk¦Àk¦¿i¥½e¤¼b¢¹\ ¸Xž¯\™’~Œ………ŠŠŠ~~~YYYbbb•••¨¨¨¯¯¯²²²´´´µ¡¯Ãr©Âp¨Áo¨Àl§Àk¦¿i¥¾f¤½e¤¼c£»_¡¹[Ÿ¶U–zŽˆˆˆˆˆˆ•••”””wwwTTT___‰‰‰¡¡¡§§§«««®®®¸‚¦Áo¨Án§Àk¦¿j¦¾f¤½e¤½d£¼c£»_¡¹\ ·WžµQ›¤]‘ŠŠŠ———   ’’’oooTTTxxx™™™¡¡¡¥¥¥¨Ÿ¦¿j¦Àk¦¿i¥¾f¤½d£¼b¢»a¢»_¡»_¡¹\ ¸Xž¶Sœ³Nš³N™™Ž•¦¦¦¢¢¢†††___kkkŒŒŒšššžžž«¼c£½d£¼c£¼b¢»_¡º^¡¹] ¹] ¹[Ÿ·WžµRœ³Nš³Nš·Wž­‚Ÿ­­­žžžwwwVVVuuuŽŽŽ–•–µaž¹\ º^¡º^¡º^¡¹\ ¶`ž›‡•š†”³Y›µQ›°M™­M™¶U¼c£½q¥¦¥¦ŒŒŒccc\\\zzz•Ž¶U·V·Wž·Wž¸Xž±b›—Œ“”””‘…«V•°M™¶Sœ»_¡¿i¥½d£—‚‘rrrVVV___`ƒ°M™³Nš´Oš´P›§c•‘‹ŽŽŽŠŠŠ‰‰‰ŽˆŒ¨f–¹\ ½d£¼c£¶U[VVVoHoD›J–£K––jŠŠ‰ŠŠŠŠ‰‰‰‰‰‰ŠŠŠŒŒŒ’’’š™š¨›º_¡µRœ’G’iCiq=q„R€s}‚‚‚‰‰‰‰‰‰‰‰‰ŠŠŠŽŽŽ‘‘‘•••———–––‹€‡„Rm;m]]]llluuu~~~………ˆˆˆ‰‰‰ŒŒŒŠŠŠ‰‰‰………{{{iiiTTTTTT]]]ooo{{{{{{ttteeeZZZRRRRRRRRRRRRRRRRRRRRRRRRDDDRRRRRRRRRX>XZDZRRRRRRRRRbbb~~~›››§§§¯“¦Àl§¿k¦¨”¢›››‘‘‘uuuYYYbbb‚‚‚§§§¼¼¼ºººº¸¹ÁzªÃtªÂq©»|¦®®®ªªªŸŸŸŒŒŒcccRRRRRRžžžÔÔÔÞÞÞÎÎÎËËËò¿Ç|®Åw«ÄuªÂq©° «®®®ªªª¡¡¡‰‰‰tttRRRRRR§§§äääøøøøøøæææØØØÇœ»É€±Æ{­Äv«Ãtª·Œ©°°°«««¦¦¦¡¡¡ŒŒŒxxxRRR]]]šššéééüüüÿÿÿüüüïïïØ×ØÍŒ¹Ê„³Ç}®Æz¬Äv«Áw©±°±®®®ªªª§§§šššŠŠŠRRRQQQŽŽŽÔÔÔæææøøøþþþöööçççÑÅÏÍ‹¸Ê„³È°Æz¬Äv«Ãr©±¡­¯¯¯¬¬¬¨¨¨žžž”””ŠŠŠuuuRRR†††ÃÃÃðððøøøîîîðððèèèßßßË­Ã̉·Ê„³È~¯Æ{­Äv«Ãtª¶Ž©¯¯¯¬¬¬¨¨¨¡¡¡———ŒŒŒ‚‚‚ffffffÉÉÉîîîÿÿÿÿÿÿîîîêêêáááÛÛÛÊ–ºË†µÉ‚²Ç}®Æ{­Äv«Ãtª¾x§¯¯¯¬¬¬§§§¡¡¡™™™ŽŽŽ†††uuuRRRŸŸŸÑÑÑùùùÿÿÿùùùèèèãããÜÜÜÐÍÏˇ¶Ê„³È°Ç|®Æz¬Äv«Ãr©Áo¨¯£¬¬¬¬§§§¡¡¡™™™ŽŽŽˆˆˆfff\@\»…©Ë•»Ý³ÔÞ²ÕÙ§ÌÔ›ÄÒ•Àϼ͊¸Ë…´É€±Ç}®Æ{­Åx¬ÄuªÂq©Áo¨Àl§¿j¦½d£»_¡¸YŸµR›¬N—œN‘‡N‡]@]_<_Æz¬Í‹¸ÛªÏÜ®ÑÖ ÇÒ•Àϼ̉·Ë†µÊƒ³È~¯Ç|®Æz¬Äv«ÃtªÂp¨Án§Àk¦¾f¤¼c£»_¡¹[ŸµRœ­M™˜I•’G’_<_RRR¼˜±Ì‡¶ÖŸÇÓ˜ÂÐ’½ÏŽ»Í‹¸Ë…´Êƒ³È°Ç}®Æz¬Åw«ÄuªÂq©Áo¨Àk¦¿j¦¾f¤¼b¢º^¡¹[ŸµRœ°M™˜I•˜a‰RQRRRR···Á­¼ÍŽºÎºÍ‹¸Ì‰·Ê„³É‚²È°Ç}®Æ{­Åx¬Äv«Ãr©Âp¨Án§Àk¦¿i¥½e¤»a¢¹\ ¸YŸµRœ±O™Ži„………RRRRRR±±±ÀÀÀžÃÈ•¹Ë†µÊ„³É€±Ç}®Ç|®Æz¬Åx¬Äv«ÃtªÂq©Áo¨Àk¦¿j¦¾f¤¼c£º^¡¹[Ÿ·Wž«\–Ž€Š‰‰‰‰‰‰```RRR¦¦¦¹¹¹¿¿¿ÁÀÁÀ ·Ç}®Ç|®Æz¬Åx¬Äv«ÄuªÃr©Âp¨Áo¨Àl§Àk¦¾f¤½d£»_¡¹[Ÿ·W¡r“ŒŠ‹………‘‘‘ŠŠŠ___RRRššš±±±···¹¹¹»»»¹¬µÁ¬Äv«ÄuªÃr©Âp¨Áo¨Án§Àk¦¿j¦¾f¤½d£»_¡¹\ ±`›—ƒ‘•••ƒƒƒRRRRRR‘‘‘¥¥¥¬¬¬°°°²²²´´´¶¢°Ãr©Âp¨Áo¨Àl§Àk¦¿j¦¿i¥¾f¤½e¤»a¢¹\ ¸Xž™}ŽŽŽŸŸŸšššxxxRRR   ¦¦¦¨¨¨¬¬¬®®®¸‚¦Âp¨Àl§Àk¦¿i¥¾f¤½e¤½e¤½d£»a¢º^¡¸YŸ¶U¦a”‚‚‚”””¤¤¤¨¨¨”””lllZZZ–––ŸŸŸ¡¡¡¥¥¥¨ž¥¾i¥¿j¦¾f¤¾f¤¼c£¼b¢»a¢»_¡»_¡¹\ ¸YŸ·Wž´P›¬M˜•‰‘¥¥¥±±±¤¤¤RRR–––›››žžžª~œ»a¢¼b¢¼b¢»a¢»_¡º^¡¹] ¸\Ÿ¸YŸ·Wž¶Sœ­M™¤K—·V®ƒŸ±±±±±±———lllRRR”””–•–´`¹[Ÿ¹[Ÿ¹\ ¹\ ¹\ ¶`ž›‡•š…“³Y›µQ›­M™­M™¶Sœ½d£¾u§­¬­ššš{{{RRRRRR‚‚‚–€µQ›µRœµRœ·V·V¯_š”ˆ’’’ŽŽŽŽŠªU•³Nš·V¼c£Àk¦¿i¥™„’ƒƒƒRRRRRR’c†¤K—¤K—ªL˜ªL˜¥`“‡‹ŠŠŠŠŠŠ‰‰‰‰‰‰‘Œªi˜»a¢¾f¤¼b¢·Wž–hŠRRR_<_‡A‡˜I•—I”™mŒ‘‘ŠŠŠ‰‰‰ŠŠŠ’’’šššœ›œ¨›¸\Ÿ³Nš‰B‰^;^b4bxLxxjtƒƒƒŒŒŒŒŒŒŽŽŽ‘‘‘‘‘‘‰‰‰ƒw€`;`b4bRRR```nnn{{{ƒƒƒrrriiiRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRc1cd-dd-dc1cRRRRRRrrr–––¥¥¥ª §Àl§Àk¦Àk¦½d£ž“š•••………ooo\\\uuu®®®ÉÉÉÎÎÎÐÐÐƯÀÉ€±Ç|®Åx¬Äv«´—«®®®¦¦¦”””|||cccWWWiii¤¤¤ÓÓÓëëëîîîìììæææ̦ˆµÈ°Ç|®Æz¬½Š¬´´´°°°¦¦¦———………nnnZZZnnn°°°âââôôôúúúöööñññàßàͺʃ³Ç}®Æz¬ÄuªÀr¨­«¬¬¬¬¨¨¨¤¤¤™™™ˆˆˆttt___oooªªªÚÚÚòòòûûûûûûôôôêêêÓÊÒ̉·Êƒ³È~¯Æz¬Äv«Âq©± ¬®®®«««§§§   •••ŠŠŠxxx___nnn¤¤¤ÙÙÙíííñññôôôòòòêêêßßß˯Ä̇¶Êƒ³Ç}®Æz¬Äv«Ãr©µŽ©¯¯¯¬¬¬¨¨¨¢¢¢ššš………rrrZZZnnn¯¯¯ÙÙÙóóóúúúôôôíííèèèàààØØØÊ–ºË…´É€±Ç}®Æz¬Äv«Ãr©¼{§¯¯¯¬¬¬¨¨¨¤¤¤œœœ’’’‰‰‰~~~kkkTTTŠŠŠÇÇÇíííûûûûûûñññçççáááÚÚÚÏÊΈµÊƒ³È~¯Ç|®Æz¬ÄuªÂq©Áo¨®§¬¬¬¬§§§¢¢¢œœœ”””ŒŒŒƒƒƒuuu______«‹¡È¼Ü·ÕܲÓÜ®ÑסÈÓ™ÂÑ“¾ÎºÌ‡¶Ê„³É€±Ç|®Æz¬Åw«ÃtªÂp¨Áo¨Àk¦¿j¦½e¤»a¢¹\ ¶V±Q™§P“–RŠyUxWWW___²ˆ¤ÎºÛ«ÐÚ©ÎÙ§ÌÔšÃÐ’½ÎºÌ‡¶Ê„³É€±Ç}®Æ{­Åx¬Äv«Ãr©Áo¨Án§Àk¦¾f¤½d£»_¡¹\ ·V´Oš¤K—•I•„T€WWW___¬¬¬Ñ·ËÚ¨ÍÙ§ÌÒ•ÀБ½Í‹¸Ì‡¶Ê„³É€±Ç}®Ç|®Åx¬Äv«ÃtªÂp¨Án§Àk¦¿i¥¾f¤¼b¢»_¡¹[Ÿ·V´P›§L˜–^‡wvwbbb]]]¦¦¦ÙÙÙÝÑÜÓœÄÍ‹¸Ì‡¶Ë…´Êƒ³È°Ç}®Æ{­Æz¬Äv«ÃtªÂq©Áo¨Àl§¿j¦¾f¤½d£»a¢¹\ ¸YŸ¶U²P™“o‰‚‚‚zzzbbb]]]   ÐÐÐßßßÍÇÌÇ‘¶Êƒ³È°Ç}®Ç|®Æz¬Åx¬Äv«ÃtªÂq©Áo¨Àl§Àk¦¿i¥½e¤¼b¢º^¡¹[Ÿ·Wž¬^—Ž€Šˆˆˆ‰‰‰bbbZZZ•••ÁÁÁÓÓÓÄÄľ½¾¾´Æ{­Æz¬Åw«Äv«ÃtªÂq©Áo¨Áo¨Àl§Àk¦¿i¥¾f¤¼b¢»_¡¹[Ÿ¶V q“ŒŠ‹†††ŠŠŠ~~~bbbWWWŒŒŒ²²²Â³³³¶¶¶¸¸¸¶©³À~ªÃr©Âq©Áo¨Án§Àk¦Àk¦¿j¦¾f¤½e¤¼b¢»_¡¹[Ÿ±_š–‚ŒŒŒ………‰‰‰”””xxxYYY___ƒƒƒ¤¤¤®®®«««­­­°°°±±±²«Áo¨Án§Àk¦¿j¦¾f¤¾f¤¾f¤½d£¼b¢»_¡¹\ ·Wž™}ŒŒŒˆˆˆ™™™žžžŽŽŽrrrYYYkkk”””¡¡¡¢¢¢¥¥¥¨¨¨«««¶¤Àk¦¿i¥¾f¤½d£¼b¢¼b¢»a¢»_¡º^¡¹[Ÿ·WžµRœ¥_’ŠŠŠ‘‘‘   §§§œœœbbbYYY‚‚‚’’’›››žžž¡¡¡£™ ¼c£¼c£¼c£»a¢»_¡º^¡¹\ ¹\ ¹[Ÿ¸Xž¶UµQ›°M™³N™—Œ“¢¢¢¬¬¬¨¨¨‘‘‘tttTTT```|||–––ššš§y™¹\ ¹\ ¹\ ¹\ ¹[Ÿ¸YŸ·Yž¶X¶UµQ›³Nš­M™³Nš·Wž­ž¬¬¬§§§———zzz\\\]]]wwwŠŠŠ‘Ž²VšµRœ¶Sœ·V·V¶U²Wš—‚”Ž°Q˜°M™°M™´P›¸Xž¼c£»l¤¡Ÿ¡”””zzz]]]ZZZrrrŽk…¤K—§L˜ªL˜°M™´Oš«W–ŽŠŠŠŠ‰‰‰€‰¬X–¶Sœ¸YŸ»_¡¼b¢¹\ ˜xuuu\\\WWW|H|ŠCŠ’G’›J–ªL˜¤a’Š„ˆ‰‰‰ŠŠŠŒŒŒŽŽŽ”“«k˜¹[Ÿ¹[ŸµQ›˜I•|H|YYYc6cp4p};}‡F‡‹d‚‚‚†††‰‰‰ŠŠŠŒŒŒŒŒŒŒŒŒŠŠŠ’nˆ™L’};}j1ja6aZHZYWYeeettt~~~~~~wwwooo\\\VUVXFXRRRRRR___ccc___YYYRRRRRR```fffeeebbbYYYRRRRRRRRRkkk‘‘‘ŸŸŸ¨¨¨¬¬¬€›}š˜žžž•••†††lllWWW†††¦¦¦¸¸¸¿¿¿¿¿¿²¸¸^™•V“¤ªª§§§žžžwwwZZZ~~~···ÐÐÐÚÚÚÙÙÙÐÐЖ´³]š–W• ž®®®ªªª   rrrZZZuuu¾¾¾çççðððíííåååÕÖÖm¤¡\™•Y—‘^—’®¯¯¬¬¬¦¦¦žžžŒŒŒtttZZZeee³³³éééùùùúúúòòòããã¶ÅÅe¡_œ˜Z˜“X–—¨¨®®®ªªª¥¥¥šššˆˆˆqqqYYYWWW›››ÕÕÕñññûûûùùùíííÝÝÝ“·¶f¢ža™\™•Y—‘xŸœ°°°¬¬¬§§§ŸŸŸ’’’………lllVVVˆˆˆÈÈÈêêêñññôôôñññæææ×ØØt«©f¢ža™]š–Y—‘\–‘®¯¯­­­¨¨¨¡¡¡———ŒŒŒ{{{___‚‚‚¾¾¾èèèøøøôôôíííçççÞÞÞ½ÉÉj¦£e¡a™]š–Z˜“V”Ž˜¨§­­­¨¨¨¢¢¢™™™‚‚‚lllVVVŸŸŸÛÛÛ÷÷÷ýýýôôôéééâââÙÙÙ—¸¸g£ŸcŸ›_œ˜\™•Y—‘V”Ž{žœ­­­¨¨¨¢¢¢ššš………uuu\\\{{{³³³âââúúúúúúîîîâââÛÛÛÓÓÓv«¨e¡a™^›—[˜”X–U“]”¬¬¬§§§¡¡¡ššš†††|||fffTTTxxx¹¹¹âââøøøõõõçççÜÜÜÕÕÕ¼ÇÇg£Ÿd œ_œ˜]š–Z˜“W•T’ŒRŠ˜¤¤¥¥¥   ššš‘‘‘ˆˆˆ~~~qqqTTT3a^]š–x³±ˆÀÀ„½½{µ³r­ªn©¦h¤¡e¡a™]š–[˜”Y—‘U“S‘‹Q‰NŠ„M‡‚KƒH{Cyu>qn8jg3a^&GE7`^\™•t¯¬¹·w²°q¬©n©¦h¤¡e¡bžš_œ˜\™•Z˜“W•T’ŒRŠPŽ‡NŠ„L†K‚~H~zCyu>qn9kh6fc,KI\ihY–n©¦v±¯n©¦k§¤h¤¡e¡bžš_œ˜]š–Z˜“X–U“T’ŒQ‰O†N‰ƒL…€I€|G}yCxt>qn:li7heJUTlll•¦¥l¦£n©¦i¥¢g£Ÿd œa™_œ˜]š–[˜”Y—‘V”ŽT’ŒRŠQ‰NŠ„MˆƒKƒH{E{wBws>qnArok{z___iii§§§²ººw©§bžša™_œ˜]š–\™•Z˜“Y—‘V”ŽT’ŒRŠQ‰O†N‰ƒL…€I€|F|xCxt?spHtq}ˆ‡‚‚‚```fffžžž´´´¸ºº…¦¥]™•[˜”Z˜“X–W•U“S‘‹Q‰Q‰O†N‰ƒL†I€|G}yCyu@tq[yw‡‰‰~~~YYYbbb•••¨¨¨¯¯¯²²²¬«dš–W•U“T’ŒRŠQ‰O†NŠ„N‰ƒM‡‚KƒH{DzvKzvtƒˆˆˆ•••”””wwwTTT___‰‰‰¡¡¡§§§«««®®®’¦¥T’ŒS‘‹Q‰PŽ‡NŠ„N‰ƒMˆƒM‡‚KƒI€|F|xBwsn‚€ŠŠŠ———   ’’’oooTTTxxx™™™¡¡¡¥¥¥¨¨¨j–“Q‰O†NŠ„MˆƒL†L…€KƒKƒI€|G}yCyu?spRzw———¦¦¦¢¢¢†††___kkkŒŒŒšššžžž“œœM‡‚MˆƒM‡‚L†KƒK‚~I€|I€|H{F|xCxt?sp?spF|x—  ­­­žžžwwwVVVuuuŽŽŽ———jŠI€|K‚~K‚~K‚~I€|H{H~zG}yDzvBws?ro>qnDzvM‡‚x›™¦¦¦ŒŒŒccc\\\zzzŠŒŒH{xE{wF|xF|xG}yG}yI|y}ŒzŠ‰Ewt?ro?roCyuKƒO†QŠ„ŽrrrVVV___^sr?ro?sp@tqAvrBwsLzw‹ŠŠŠŠ|†…IwtCyuI€|MˆƒM‡‚Dzv[poVVV5VT4c`9kh:mj<olPyvƒ‡‡‰‰‰‰‰‰ŠŠŠŒŒŒ‹Z‡ƒKƒK‚~Cxt6gd2PN.QO2_\6gdTur†ˆˆ‰‰‰‰‰‰ŠŠŠŽŽŽ‘‘‘••••––a†ƒ=pm2_\,NK2QONdcuuu~~~………ˆˆˆ‰‰‰ŒŒŒŠŠŠ‰‰‰………{{{Ka`-HGTTT]]]ooo{{{{{{ttteeeZZZRRRRRRRRRRRRRRRRRRRRRRRRDDDRRRRRRRRR7HG:LKRRRRRRRRRbbb~~~›››§§§¤ªªU’ŒSŠ££›››‘‘‘uuuYYYbbb‚‚‚§§§¼¼¼ººº»»»‡¦¥X–V”Ž€Ÿ®®®ªªªŸŸŸŒŒŒcccRRRRRRžžžÔÔÔÞÞÞÎÎÎËËËÆÇÇhŸœ[˜”Y—‘^—’¯°°®®®ªªª¡¡¡‰‰‰tttRRRRRR§§§äääøøøøøøæææØØزÂÂd œ^›—Z˜“X–˜©©°°°«««¦¦¦¡¡¡ŒŒŒxxxRRR]]]šššéééüüüÿÿÿüüüïïïÛÛÛ“·¶g£Ÿa™]š–Z˜“y ž²²²®®®ªªª§§§šššŠŠŠRRRQQQŽŽŽÔÔÔæææøøøþþþöööçççØÙÙu¬ªg£ŸcŸ›]š–Z˜“]—’°±±¯¯¯¬¬¬¨¨¨žžž”””ŠŠŠuuuRRR†††ÃÃÃðððøøøîîîðððèèèßßß½ÉÉk§¤g£Ÿbžš^›—Z˜“X–™©¨¯¯¯¬¬¬¨¨¨¡¡¡———ŒŒŒ‚‚‚ffffffÉÉÉîîîÿÿÿÿÿÿîîîêêêáááÛÛÛ—¸¸i¥¢e¡a™^›—Z˜“X–|Ÿ¯¯¯¬¬¬§§§¡¡¡™™™ŽŽŽ†††uuuRRRŸŸŸÑÑÑùùùÿÿÿùùùèèèãããÜÜÜÓÓÓv«¨g£ŸcŸ›_œ˜]š–Z˜“W•^•‘¯¯¯¬¬¬§§§¡¡¡™™™ŽŽŽˆˆˆfffRRR´´´ÑÑÑúúúüüüðððãããÜÜÜÖÖÖ¼ÇÇh¤¡d œa™^›—\™•Y—‘V”ŽT’Œ›§§«««¥¥¥   ™™™‘‘‘ˆˆˆtttRRR3IHaœ˜s«©–ÈÉŸÌÍÁÀ„·¶|±¯q©§k¦£f¢žbžš_œ˜]š–Z˜“X–U“S‘‹Q‰NŠ„M‡‚KƒH{CxtAsp@mjGmj;LK;LK]š–j¦£€¹¸z´²t¯¬q¬©n©¦h¤¡f¢žcŸ›a™]š–[˜”Y—‘V”ŽT’ŒQ‰PŽ‡NŠ„L†K‚~H{Cxt?ro8jg:li6KJRRR„¤£e¡n©¦p«¨n©¦k§¤g£Ÿe¡cŸ›a™^›—\™•Z˜“W•U“S‘‹Q‰O†N‰ƒL…€I€|H~zCxt?sp8jgYxvQRRRRR±±±œ²²k¥¢j¦£i¥¢g£Ÿd œa™_œ˜]š–\™•Z˜“X–V”ŽT’ŒQ‰PŽ‡NŠ„M‡‚K‚~H{F|xAvr@spi€~‰‰‰```RRR¦¦¦¹¹¹¯¸¸r¤¡cŸ›a™_œ˜]š–\™•Z˜“Y—‘W•U“T’ŒRŠQ‰NŠ„MˆƒKƒH{F|xCyuLxuv€‘‘‘ŠŠŠ___RRRššš±±±···¶¸¸ˆ¨§]™•[˜”Z˜“Y—‘W•U“T’ŒS‘‹Q‰PŽ‡NŠ„MˆƒKƒI€|F|xDyua€~|~~•••ƒƒƒRRRRRR‘‘‘¥¥¥¬¬¬°°°²²²ž««e›—W•U“T’ŒRŠQ‰PŽ‡O†NŠ„N‰ƒL…€I€|G}yL|xyˆ†ŸŸŸšššxxxRRR   ¦¦¦¨¨¨¬¬¬®®®’¦¥U“RŠQ‰O†NŠ„N‰ƒN‰ƒMˆƒL…€K‚~H~zDzvq†„‚‚‚”””¤¤¤¨¨¨”””lllZZZ–––ŸŸŸ¡¡¡¥¥¥§§§i•’PŽ‡NŠ„NŠ„M‡‚L†L…€KƒKƒI€|H~zF|xAvrQxu’’’¥¥¥±±±¤¤¤RRR–––›››žžž’››L…€L†L†L…€KƒK‚~I€|H{H~zF|xCyu>qn;nkF{w˜¡¡±±±±±±———lllRRR”””———jŒ‰H{H{I€|I€|I€|H{K~{I|yDzvBws>qn>qnCyuMˆƒ~ ž®®®ššš{{{RRRRRR‚‚‚JzvCxtCxtE{wE{wE{wI{wŒŠ‰Euq>qn?spE{wM‡‚Q‰[‘Œ”””ƒƒƒRRRRRRv}|;nk;nk=pm=pm?spHws}‡†ŠŠŠ‰‰‰|†…K{wF|xL…€NŠ„L†F|x{‚RRRJPO2_\8jg8jg>qnU{‚ˆ‡‰‰‰ŠŠŠ’’’’˜—Y‡ƒKƒH{?sp3`]JPO-HG.WU7b_^vt‚‚ŒŒŒŒŒŒŽŽŽ‘‘‘f}>mi$DB-HGQRR```nnn{{{ƒƒƒrrriiiRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR<LK@MMRRRRRRRRRrrr–––¥¥¥ªªªˆ¡ŸQ‰Q‰‹œ›œœœ•••………ooo\\\uuu®®®ÉÉÉÎÎÎÐÐÐÈÊÊl¤ _œ˜\™•kš²²²®®®¦¦¦”””|||cccWWWiii¤¤¤ÓÓÓëëëîîîìììæææµÇÇi¥¢cŸ›_œ˜]š– ¯®´´´°°°¦¦¦———………nnnZZZnnn°°°âââôôôúúúöööñññââ⊵³f¢ža™]š–Y—‘yžœ®®®¬¬¬¨¨¨¤¤¤™™™ˆˆˆttt___oooªªªÚÚÚòòòûûûûûûôôôêêê×ÙÙm¨¥f¢žbžš]š–Z˜“\–‘¯°°®®®«««§§§   •••ŠŠŠxxx___nnn¤¤¤ÙÙÙíííñññôôôòòòêêêßß߸ÈÈj¦£f¢ža™]š–Z˜“W•™©¨¯¯¯¬¬¬¨¨¨¢¢¢ššš………rrrZZZnnn¯¯¯ÙÙÙóóóúúúôôôíííèèèàààØØØ–··h¤¡d œa™]š–Z˜“W•} ž¯¯¯¬¬¬¨¨¨¤¤¤œœœ’’’‰‰‰~~~kkkTTTŠŠŠÇÇÇíííûûûûûûñññçççáááÚÚÚÒÒÒy«©f¢žbžš_œ˜]š–Y—‘V”Ž^•‘®®®¬¬¬§§§¢¢¢œœœ”””ŒŒŒƒƒƒuuu______¦¦¦ÏÏÏ÷÷÷øøø÷÷÷êêêàààÚÚÚÓÓÓÄÈÈh£Ÿd œ_œ˜]š–[˜”X–U“T’Œ–¥¤¨ªª¦¦¦¡¡¡›››•••ŒŒŒ………{{{lllWWW<VUQ‰u­«˜ÉÉ™ÉÉ–Çlj»º€´²x®¬n¨¥i¤¡d œa™^›—\™•Z˜“W•T’ŒS‘‹Q‰NŠ„O‰ƒP†R„€S€}QzwPvsNqnHge;POY]]b”x³±‰ÁÁˆÀÀw²°s®«n©¦j¦£g£Ÿd œa™_œ˜\™•Z˜“X–U“S‘‹Q‰O†NŠ„L†KƒH{E{wAvr<ol8jg;gdS^]]]]£¥¥š¾¾„½½{µ³n©¦j¦£h¤¡f¢žcŸ›a™^›—]š–Z˜“X–V”ŽT’ŒRŠPŽ‡NŠ„MˆƒL…€I€|H~zDzv@tq<olQtquxxbbb]]]   ÐÐмÍÍs«©g£Ÿf¢žcŸ›a™_œ˜]š–\™•Z˜“X–V”ŽT’ŒRŠQ‰O†N‰ƒL†K‚~H{F|xCxtAspi}‰‰‰bbbZZZ•••ÁÁÁÓÓÓ¶¾¾s£ _œ˜^›—]š–[˜”Z˜“X–V”ŽT’ŒT’ŒRŠQ‰O†NŠ„L†KƒH{E{wCxtNyvyƒ‚ŠŠŠ~~~bbbWWWŒŒŒ²²²Â³³³³µµ…¥¤[˜“Y—‘W•V”ŽT’ŒS‘‹Q‰Q‰PŽ‡NŠ„N‰ƒL†KƒH{E{wCxt`}‚„„‰‰‰”””xxxYYY___ƒƒƒ¤¤¤®®®«««­­­°°°˜¨§]–‘T’ŒS‘‹Q‰PŽ‡NŠ„NŠ„NŠ„MˆƒL†KƒI€|F|xJ{wu†…ˆˆˆ™™™žžžŽŽŽrrrYYYkkk”””¡¡¡¢¢¢¥¥¥¨¨¨«««¢¡Q‰O†NŠ„MˆƒL†L†L…€KƒK‚~H{F|xCxto„‚ŠŠŠ‘‘‘   §§§œœœbbbYYY‚‚‚’’’›››žžž¡¡¡¢¢¢b‹M‡‚M‡‚L…€KƒK‚~I€|I€|H{G}yDzvBws?roRzw•••¢¢¢¬¬¬¨¨¨‘‘‘tttTTT```|||–––ššš––J€|I€|I€|I€|H{H~zF|xE{wDzvBws?sp>qn?spF|x–ŸŸ¬¬¬§§§———zzz\\\]]]wwwŠŠŠb„€CxtCyuE{wE{wDzvCyuGyvEwt?sp?ro?roAvrG}yM‡‚t—”¢¢¢”””zzz]]]ZZZrrr~€€?pm<ol=pm?ro@tq?spEuqz†…z…„Dtp@tqCyuH~zKƒL†M‚~ŠŒŒuuu\\\WWWQed3a^6gd9kh=pm?spFuq|†…ŠŠŠŒŒŒ‹ŠL}yG}yH{H{Bws8jgQedYYY-HG)MK.WU2_\6gdDpm|ƒ‚‰‰‰ŠŠŠŒŒŒŒŒŒ‚‰ˆIvs;nk7if.WU&IF,GE2JI)KI5XVmqq~~~~~~wwwimm0OM'IF2IHRRRRRR___ccc___YYYRRRRRR \ No newline at end of file
diff --git a/bubbob/images/extra6.ppm b/bubbob/images/extra6.ppm
new file mode 100644
index 0000000..26dbee1
--- /dev/null
+++ b/bubbob/images/extra6.ppm
Binary files differ
diff --git a/bubbob/images/extra7.ppm b/bubbob/images/extra7.ppm
new file mode 100644
index 0000000..9482b77
--- /dev/null
+++ b/bubbob/images/extra7.ppm
@@ -0,0 +1,13 @@
+P6
+# CREATOR: The GIMP's PNM Filter Version 1.0
+8 56
+255
+989USU.,-=;=×××äää»»»€312XXXÍÍÍ×××µµµˆˆˆYYY*)*535¤¤¤ªªª•••rqr978 /-/EDE.,. 0.0dcdÖÖÖÖÕÖRPR «««ØØØÜÜܳ³³878
+
+±±±³³³šššqppjhiƒƒƒsssUUU*)*
+
+utuigi¤£¤ÖÖÖäääZXZ-+,£££ÌÌÌ××ו”•|z{¤¤¤ªªª”“”onnzzzlll ?>???? .-.A@AGEFÓÓÓÅÅÅ›š›qopéééÓÓÓ¤¤¤423KIK···”””"!"‘‘‘ŒŒŒqpqHFG]]]535///
+ 
+MKLcacA?AàààÖÖÖ¤££…„…ÚÚÚÒÒÒ¨¨¨767Ž®®®ªªª‰ˆ‰pppyyy#"#
+
+CCCJII%#$ECDÐÎÏÔÔÔ_]^/./²±²ÞÞÞÚÚÚ¬¬¬  poo›››µµµ´´´•”• *()WWWuuu‡†‡trt&%&=;<mkl\[\645‚¶¶¶ãããÞÞÞ_]_%#$QQQ€€€­­­ÏÏÏÌÌÌnnn,*,hgh£££   312323 \ No newline at end of file
diff --git a/bubbob/images/extra8.ppm b/bubbob/images/extra8.ppm
new file mode 100644
index 0000000..af6288d
--- /dev/null
+++ b/bubbob/images/extra8.ppm
Binary files differ
diff --git a/bubbob/images/fire_drop.ppm b/bubbob/images/fire_drop.ppm
new file mode 100644
index 0000000..dbb706b
--- /dev/null
+++ b/bubbob/images/fire_drop.ppm
Binary files differ
diff --git a/bubbob/images/fire_surface.ppm b/bubbob/images/fire_surface.ppm
new file mode 100644
index 0000000..8ef46ee
--- /dev/null
+++ b/bubbob/images/fire_surface.ppm
Binary files differ
diff --git a/bubbob/images/fish_0.ppm b/bubbob/images/fish_0.ppm
new file mode 100644
index 0000000..4f2040f
--- /dev/null
+++ b/bubbob/images/fish_0.ppm
Binary files differ
diff --git a/bubbob/images/flappy.ppm b/bubbob/images/flappy.ppm
new file mode 100644
index 0000000..0724f84
--- /dev/null
+++ b/bubbob/images/flappy.ppm
Binary files differ
diff --git a/bubbob/images/flapy_angry.ppm b/bubbob/images/flapy_angry.ppm
new file mode 100644
index 0000000..4e71e8b
--- /dev/null
+++ b/bubbob/images/flapy_angry.ppm
Binary files differ
diff --git a/bubbob/images/game_over_0.ppm b/bubbob/images/game_over_0.ppm
new file mode 100644
index 0000000..d76ecae
--- /dev/null
+++ b/bubbob/images/game_over_0.ppm
Binary files differ
diff --git a/bubbob/images/ghost.ppm b/bubbob/images/ghost.ppm
new file mode 100644
index 0000000..424668e
--- /dev/null
+++ b/bubbob/images/ghost.ppm
@@ -0,0 +1,6 @@
+P6
+# CREATOR: GIMP PNM Filter Version 1.1
+32 256
+255
+ßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÈÊÇÈÊÇÈÊÇëîêßáÞßáÞÕ×ÔÕ×ÔßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞÕ×ÔëîêßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞÕ×ÔÕ×ÔÕ×ÔöøõßáÞßáÞßáÞëîêëîêëîêëîêëîêëîêßáÞßáÞßáÞßáÞÕ×ÔÈÊÇÈÊÇÕ×ÔöøõöøõëîêëîêëîêöøõöøõöøõöøõöøõëîêíÖØìt‰ír‡özŽ÷~ðtŠíiŒÚ¾ÅÕ×Ôöøõöøõëîêëîêöøõöøõöøõöøõöøõöøõëãâõr‘íjír‡özŽ÷~ðtŠïd‰ñ`ÙÅÉßáÞöøõëîêëîêöøõöøõöøõöøõöøõöøõëîêëîêø£·õe‚ïs‰ë‡•ð’œõ˜ó‘îc‰í?|îQØÆÉÕÔÒöøõëîêëîêëîêöøõöøõöøõöøõëîêëîêëîêõzðfî뛠謭ï{“ên‚çIrê<oíW…Ô¡¯öøõëîêëîêëîêëîêëîêëîêëîêëîêßáÞßáÞõníl쇒êÇÁëÔÍìm|ëbvèMmæ9jé0oí-r§©¦ëîêßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞÕ×ÔðuŠëf|ó£©ïØÒîéãð\qðVvëDlå.gæ fâ f§©¦ßáÞßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞßáÞÕ×ÔÕ×ÔÈÊÇ爚ëYsïi|ó£©ðåØîîçíGjëGkè8hå%då_áZ§©¦²´°ßáÞßáÞÕ×ÔÈÊÇÈÊÇÈÊÇÕ×ÔÕ×ÔÕ×ÔÈÊǼ¾»Ò°³ò\tí_{ï^vïìÞó£©æ8fæ8få/bå WáIà<²´°²´°Õ×ÔÕ×ÔÈÊDz´°²´°¼¾»ÈÊÇÕ×ÔÈÊDz´°²´°¼¾»â~ìXuëQlïHgë9eå0då,cä'[áEÚ(Ö¼¾»¼¾»ÈÊÇÈÊǼ¾»²´°²´°²´°¼¾»ÈÊǼ¾»¼¾»¼¾»¼¾»¿·¶á\}ìIní<hè/bå&_ä ZâMÝ:ØÓÈÊǼ¾»¼¾»²´°¼¾»¼¾»²´°¼¾»ÈÊÇÈÊÇÈÊǼ¾»¿µµßj‰ì1fç#]åXáLÛ0ÛÝ,‘Õ³±ÈÊÇÈÊǧ©¦²´°²´°ÈÊÇÈÊǼ¾»¼¾»²´°²´°Á’™“6¾JÏGÖ/JÙfgÞÒÐÕ×ÔÕ×ÔÈÊǼ¾»²´°²´°²´°ßáÞßáÞßáÞßáÞßáÞÕ×ÔÕ×ÔÈÊǧ©¦§©¦ßáÞßáÞßáÞßáÞßáÞÕ×ÔÈÊÇÈÊÇßáÞßáÞßáÞÕ×ÔÈÊÇÕ×ÔßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞßáÞöøõöøõöøõÈÊÇÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔöøõëîêëîêßáÞëîêëîêöøõëîê¼¾»¼¾»ÈÊÇÕ×ÔÕ×ÔÕ×ÔÕ×ÔÈÊÇÈÊÇÈÊÇëîêßáÞÈÊǼ¾»ÈÊÇÕ×ÔÕ×ÔßáÞßáÞÕ×ÔÕ×ÔÈÊÇÈÊÇÈÊÇÈÊÇÈÊÇÕ×ÔÕ×ÔÈÊǼ¾»²´°²´°ëîêÕ×Ô¼¾»²´°²´°ÈÊÇÈÊÇÕ×ÔÈÊÇÕ×ÔÕ×ÔÈÊÇÈÊÇÕ×ÔÕ×ÔÕ×ÔÈÊÇÈÊÇÈÊÇÈÊÇÈÊÇÈÊǼ¾»²´°Õ×ÔÈÊǼ¾»¼¾»¼¾»ÈÊÇÈÊÇÈÊǼ¾»¼¾»¼¾»¼¾»¼¾»ÈÊÇÕ×ÔßáÞßáÞÕ×ÔÕ×ÔÈÊÇÈÊǼ¾»¼¾»¼¾»¼¾»Õ×ÔÕ×ÔÈÊǼ¾»¼¾»ÈÊÇÈÊÇÈÊDz´°²´°²´°¼¾»¼¾»¼¾»ÈÊÇÕ×ÔÈÊÇÈÊÇÈÊÇÈÊǼ¾»¼¾»²´°²´°ßáÞÕ×ÔÈÊǼ¾»¼¾»¼¾»¼¾»²´°²´°§©¦§©¦²´°²´°²´°²´°¼¾»²´°²´°²´°§©¦ßáÞÈÊǼ¾»¼¾»¼¾»²´°§©¦§©¦²´°²´°²´°Õ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÈÊÇÈÊÇÈÊÇëîêßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞßáÞëîêßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞÕ×ÔÕ×ÔÕ×ÔöøõßáÞßáÞßáÞëîêëîêëîêëîêëîêëîêßáÞßáÞßáÞßáÞÕ×ÔÈÊÇÈÊÇÕ×Ôöøõöøõëîêëîêëîêöøõöøõöøõöøõöøõëîêëëè襨ê|…ñn|ôwŒç Ñ¬²Õ×ÔÕ×Ôöøõöøõëîêëîêöøõöøõöøõöøõöøõöøõëëèô‡›îj}íq…õy÷}Žðt‰ìd†æ}œÖÍÎßáÞöøõëîêëîêöøõöøõöøõöøõöøõöøõëîêëîêø¸Æõeïs‰ë†•ð’œõ˜ó‘îc‰ì?|íP€ÙÀÅÕ×Ôöøõëîêëîêëîêöøõöøõöøõöøõëîêëîêëîêõ”ðfî뛠謭ï{“ên‚çIrê<oîO€Ï±¸öøõëîêëîêëîêëîêëîêëîêëîêëîêßáÞßáÞölíl쇒êÇÁëÔÍìm|ëbvèMmæ9jé0oê6w§©¦ëîêßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞÕ×Ôòj‚ëf|ó£©ïØÒîéãð\qðVvëDlå.gæ fâ f§©¦ßáÞßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞßáÞÕ×ÔÕ×ÔÈÊÇëyëYsïi|ó£©ðåØîîçíGjëGkè8hå%då_áZ§©¦²´°ßáÞßáÞÕ×ÔÈÊÇÈÊÇÈÊÇÕ×ÔÕ×ÔÕ×ÔÈÊǼ¾»Ö©­ò\tí_{ï^vïìÞó£©æ8fæ8få/bå WáIà<²´°²´°Õ×ÔÕ×ÔÈÊDz´°²´°¼¾»ÈÊÇÕ×ÔÈÊDz´°²´°¼¾»äzŽìXuëQlïHgë9eå0då,cä'[áEÚ(Ö¼¾»¼¾»ÈÊÇÈÊǼ¾»¼¾»²´°¼¾»¼¾»ÈÊǼ¾»¼¾»¼¾»¼¾»¾¹¸Þd‚ìIní<hè/bå&_ä ZâMÝ:ØÓÈÊǼ¾»¼¾»¼¾»¼¾»ÈÊÇÈÊǼ¾»¼¾»²´°¼¾»ÈÊÇÈÊÇÈÊǼ¾»½¹¸Ùw‘ì1fç#]åXáLÛ0ÛÝ,‘Õ³±ÈÊÇÈÊDz´°¼¾»ÈÊÇÕ×Ô¼¾»ÈÊÇÕ×ÔÕ×ÔÈÊǼ¾»²´°²´°½›ŸÓZyÝ0eÞOÕ!?Ø]^ÞÐÎÕ×ÔÕ×ÔÈÊÇÕ×ÔßáÞßáÞëîêßáÞÈÊDz´°²´°²´°²´°²´°¼¾»Õ×ÔßáÞßáÞßáÞßáÞÕ×ÔÕ×ÔÈÊÇÈÊǼ¾»²´°²´°Õ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞßáÞÕ×ÔÈÊÇÈÊÇÕ×Ô¼¾»ÈÊÇÈÊÇÕ×ÔßáÞßáÞÕ×ÔÈÊÇÕ×ÔßáÞöøõöøõöøõÕ×ÔßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞßáÞöøõöøõöøõöøõöøõöøõëîêëîê²´°¼¾»ÈÊÇÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔöøõëîêëîêßáÞëîêëîêßáÞöøõëîêëîêëîê¼¾»²´°¼¾»ÈÊÇÕ×ÔÕ×ÔÕ×ÔÕ×ÔÈÊÇÈÊÇÈÊÇëîêßáÞÈÊǼ¾»ÈÊÇÕ×ÔÕ×ÔÕ×ÔëîêëîêßáÞÕ×ÔÕ×ÔßáÞÈÊÇÈÊÇÈÊÇÈÊÇÈÊÇÕ×ÔÕ×ÔÈÊǼ¾»²´°²´°Õ×Ô¼¾»²´°²´°ÈÊÇÈÊÇÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÈÊÇÈÊÇßáÞßáÞÕ×ÔÕ×ÔÕ×ÔÈÊÇÈÊÇÈÊÇÈÊÇÈÊÇÈÊǼ¾»²´°ÈÊÇÈÊǼ¾»¼¾»¼¾»ÈÊÇÈÊÇÈÊǼ¾»¼¾»¼¾»¼¾»¼¾»ÈÊÇÕ×ÔßáÞßáÞÕ×ÔÕ×ÔÈÊÇÈÊǼ¾»¼¾»¼¾»¼¾»ÈÊÇÕ×ÔÈÊǼ¾»¼¾»ÈÊÇÈÊÇÈÊDz´°²´°²´°¼¾»¼¾»¼¾»ÈÊÇÕ×ÔÈÊÇÈÊÇÈÊÇÈÊǼ¾»¼¾»²´°²´°ßáÞÕ×ÔÈÊǼ¾»¼¾»¼¾»¼¾»²´°²´°§©¦§©¦²´°²´°²´°²´°¼¾»²´°²´°²´°§©¦ßáÞÈÊǼ¾»¼¾»¼¾»²´°§©¦§©¦²´°²´°²´°Õ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÈÊÇÈÊÇÈÊÇëîêßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞßáÞëîêßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞÕ×ÔÕ×ÔÕ×ÔöøõßáÞßáÞßáÞëîêëîêëîêëîêëîêëîêßáÞßáÞßáÞßáÞÕ×ÔÈÊÇÈÊÇÕ×Ôöøõöøõëîêëîêëîêöøõöøõöøõöøõöøõëîêëëè襨ê|…ñn|ôwŒç Ñ¬²Õ×ÔÕ×Ôöøõöøõëîêëîêöøõöøõöøõöøõöøõöøõëëèô‡›îj}íq…õy÷}Žðt‰ìd†æ}œÖÍÎßáÞöøõëîêëîêöøõöøõöøõöøõöøõöøõëîêëîêø¸Æõeïs‰ë†•ð’œõ˜ó‘îc‰ì?|íP€ÙÀÅÕ×Ôöøõëîêëîêëîêöøõöøõöøõöøõëîêëîêëîêõ”ðfî뛠謭ï{“ên‚çIrê<oîO€Ï±¸öøõëîêëîêëîêëîêëîêëîêëîêëîêßáÞßáÞölíl쇒êÇÁëÔÍìm|ëbvèMmæ9jé0oê6w§©¦ëîêßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞÕ×Ôòj‚ëf|ó£©ïØÒîéãð\qðVvëDlå.gæ fâ f§©¦ßáÞßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞßáÞÕ×ÔÕ×ÔÈÊÇëyëYsïi|ó£©ðåØîîçíGjëGkè8hå%då_áZ§©¦²´°ßáÞßáÞÕ×ÔÈÊÇÈÊÇÈÊÇÕ×ÔÕ×ÔÕ×ÔÈÊǼ¾»Ö©­ò\tí_{ï^vïìÞó£©æ8fæ8få/bå WáIà<²´°²´°Õ×ÔÕ×ÔÈÊDz´°²´°¼¾»ÈÊÇÕ×ÔÈÊDz´°²´°¼¾»äzŽìXuëQlïHgë9eå0då,cä'[áEÚ(Ö¼¾»¼¾»ÈÊÇÈÊǼ¾»²´°²´°²´°¼¾»ÈÊǼ¾»¼¾»¼¾»¼¾»¾¹¸Þd‚ìIní<hè/bå&_ä ZâMÝ:ØÓÈÊǼ¾»¼¾»²´°¼¾»¼¾»²´°¼¾»ÈÊÇÈÊÇÈÊǼ¾»½¹¸Ùw‘ì1fç#]åXáLÛ0ÛÝ,‘Õ³±ÈÊÇÈÊǧ©¦²´°²´°ÈÊÇÈÊǼ¾»¼¾»²´°²´°½›ŸŒ3ÃKÙKÖ#@Ø]^ÞÐÎÕ×ÔÕ×ÔÈÊǼ¾»²´°²´°²´°ßáÞßáÞßáÞßáÞßáÞÕ×ÔÕ×ÔÈÊǧ©¦§©¦ßáÞßáÞßáÞßáÞßáÞÕ×ÔÈÊÇÈÊÇßáÞßáÞßáÞÕ×ÔÈÊÇÕ×ÔßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞßáÞöøõöøõöøõÈÊÇÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔöøõëîêëîêßáÞëîêëîêöøõëîê¼¾»¼¾»ÈÊÇÕ×ÔÕ×ÔÕ×ÔÕ×ÔÈÊÇÈÊÇÈÊÇëîêßáÞÈÊǼ¾»ÈÊÇÕ×ÔÕ×ÔßáÞßáÞÕ×ÔÕ×ÔÈÊÇÈÊÇÈÊÇÈÊÇÈÊÇÕ×ÔÕ×ÔÈÊǼ¾»²´°²´°ëîêÕ×Ô¼¾»²´°²´°ÈÊÇÈÊÇÕ×ÔÈÊÇÕ×ÔÕ×ÔÈÊÇÈÊÇÕ×ÔÕ×ÔÕ×ÔÈÊÇÈÊÇÈÊÇÈÊÇÈÊÇÈÊǼ¾»²´°Õ×ÔÈÊǼ¾»¼¾»¼¾»ÈÊÇÈÊÇÈÊǼ¾»¼¾»¼¾»¼¾»¼¾»ÈÊÇÕ×ÔßáÞßáÞÕ×ÔÕ×ÔÈÊÇÈÊǼ¾»¼¾»¼¾»¼¾»Õ×ÔÕ×ÔÈÊǼ¾»¼¾»ÈÊÇÈÊÇÈÊDz´°²´°²´°¼¾»¼¾»¼¾»ÈÊÇÕ×ÔÈÊÇÈÊÇÈÊÇÈÊǼ¾»¼¾»²´°²´°ßáÞÕ×ÔÈÊǼ¾»¼¾»¼¾»¼¾»²´°²´°§©¦§©¦²´°²´°²´°²´°¼¾»²´°²´°²´°§©¦ßáÞÈÊǼ¾»¼¾»¼¾»²´°§©¦§©¦²´°²´°²´°Õ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÈÊÇÈÊÇÈÊÇëîêßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞßáÞëîêßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞÕ×ÔÕ×ÔÕ×ÔöøõßáÞßáÞßáÞëîêëîêëîêëîêëîêëîêßáÞßáÞßáÞßáÞÕ×ÔÈÊÇÈÊÇÕ×Ôöøõöøõëîêëîêëîêöøõöøõöøõöøõöøõëîêëäá雟ê}†ðt‚òƒ–啥ε¸Õ×ÔÕ×ÔöøõöøõëîêëîêöøõöøõöøõöøõöøõöøõëäâõsŒîj}íq…õy÷}Žðt‰ìd†æ}œÖÍÎßáÞöøõëîêëîêöøõöøõöøõöøõöøõöøõëîêëîêø§ºõeïs‰ë†•ð’œõ˜ó‘îc‰ì?|íP€ÙÀÅÕ×Ôöøõëîêëîêëîêöøõöøõöøõöøõëîêëîêëîêõ}‘ðfî뛠謭ï{“ên‚çIrê<oîO€Ï±¸öøõëîêëîêëîêëîêëîêëîêëîêëîêßáÞßáÞöm€íl쇒êÇÁëÔÍìm|ëbvèMmæ9jé0oê6w§©¦ëîêßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞÕ×Ôño†ëf|ó£©ïØÒîéãð\qðVvëDlå.gæ fâ f§©¦ßáÞßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞßáÞÕ×ÔÕ×ÔÈÊÇëzëYsïi|ó£©ðåØîîçíGjëGkè8hå%då_áZ§©¦²´°ßáÞßáÞÕ×ÔÈÊÇÈÊÇÈÊÇÕ×ÔÕ×ÔÕ×ÔÈÊǼ¾»ÚŸ¦ò\tí_{ï^vïìÞó£©æ8fæ8få/bå WáIà<²´°²´°Õ×ÔÕ×ÔÈÊDz´°²´°¼¾»ÈÊÇÕ×ÔÈÊDz´°²´°¼¼¹çtŠìXuëQlïHgë9eå0då,cä'[áEÚ(Ö¼¾»¼¾»ÈÊÇÈÊǼ¾»§©¦ÈÊÇÈÊǼ¾»¼¾»¼¾»ÁµµÜj†ìJoí=jè0cå&_ä ZâMÝ:ØÓ ÈÊǼ¾»²´°²´°ÈÊÇÈÊÇÈÊÇÈÊǼ¾»¼¾»Ì—¦ÞQ€æ%däYáMÛ0ÚÝ/’Õ­¬ÈÊÇÈÊǼ¾»¼¾»²´°²´°!U%q(nÚÞØÖÕ×ÔÕ×ÔÈÊǧ©¦§©¦ßáÞßáÞßáÞßáÞÕ×ÔÕ×ÔÈÊÇÕ×ÔßáÞßáÞÕ×ÔÈÊÇÈÊÇßáÞßáÞßáÞÕ×ÔÈÊÇÕ×ÔßáÞßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔëîêëîêßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÈÊÇÈÊÇÈÊÇëîêßáÞÈÊǼ¾»ÈÊÇÕ×ÔÕ×ÔÈÊÇÕ×ÔÕ×ÔÈÊÇÈÊÇÕ×ÔÕ×ÔÈÊǼ¾»²´°²´°ëîêÕ×Ô¼¾»²´°²´°ÈÊÇÕ×ÔÕ×ÔÕ×ÔÈÊǼ¾»Õ×ÔÕ×ÔÕ×ÔÈÊÇÈÊÇÈÊÇÈÊÇÈÊǼ¾»²´°ëîêÕ×ÔÈÊǼ¾»¼¾»¼¾»ÈÊÇÈÊDz´°¼¾»¼¾»¼¾»¼¾»Õ×ÔÕ×ÔÕ×ÔÈÊÇÈÊǼ¾»¼¾»¼¾»¼¾»Õ×ÔÕ×ÔÈÊǼ¾»¼¾»ÈÊÇÈÊǼ¾»²´°²´°²´°¼¾»¼¾»¼¾»ÈÊÇÕ×ÔÈÊÇÈÊÇÈÊÇÈÊǼ¾»¼¾»²´°²´°ßáÞÕ×ÔÈÊǼ¾»¼¾»¼¾»¼¾»²´°²´°§©¦§©¦²´°²´°²´°²´°¼¾»²´°²´°²´°§©¦ßáÞÈÊǼ¾»¼¾»¼¾»²´°§©¦§©¦²´°²´°²´°ÈÊÇÈÊÇÈÊÇÈÊÇÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞëîêÕ×ÔÕ×ÔÕ×ÔßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞëîêÕ×ÔÕ×ÔÈÊÇÕ×ÔßáÞßáÞßáÞßáÞëîêëîêëîêëîêëîêëîêßáÞßáÞßáÞöøõßáÞÕ×ÔØ´¸âŒ›òsŽöl„ê{†å¥¨ßßÜëîêöøõöøõöøõöøõöøõëîêëîêëîêöøõöøõßáÞß××èžëd…ït‰÷}öyŽíq†íj}ð†šëìéëîêöøõöøõöøõöøõöøõëîêëîêöøõöøõÕÖÓÙÀÅîR‚ì?{îc‰ó‘õ˜ð’œë†•ïs‰õeñ¶ÃëîêëîêöøõöøõöøõöøõöøõöøõëîêëîêöøõÏ°¸îPê<oçIrên‚ï{“謭뛠îðfô‚•ßáÞëîêëîêöøõöøõöøõöøõëîêëîêëîêöøõ§©¦ê6wé0oæ9jèMmëbvìm|ëÔÍêÇÁ쇒ílõl€ßáÞßáÞëîêëîêëîêëîêëîêëîêëîêëîêöøõ§©¦â fæ få.gëDlðVvð\qîéãïØÒó£©î€‹ëf|óg€Õ×ÔßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞëîê²´°§©¦áZå_å%dè8hëGkíGjîîçðåØó£©ïi|ëYsìvŒÈÊÇÕ×ÔÕ×ÔßáÞßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞßáÞ²´°²´°à<áIå Wå/bæ8fæ8fó£©ïìÞï^uí_zò\sؤ©¼¾»ÈÊÇÕ×ÔÕ×ÔÕ×ÔÈÊÇÈÊÇÈÊÇÕ×ÔßáÞßáÞ¼¾»¼¾»ÖÚ(áEä'[å,cå0dë9eïHgìRkíYræw‡¼½º²´°²´°ÈÊÇÕ×ÔÈÊǼ¾»²´°²´°ÈÊÇÕ×ÔÕ×Ô¼¾»ÈÊÇÓØÞ<âMä Zå&_è/bí;gîJlâg}¾¸¶¼¾»¼¾»¼¾»¼¾»ÈÊǼ¾»¼¾»²´°²´°¼¾»ÈÊÇÈÊÇÈÊÇÈÊÇÔ²®à6”ÜÛ.áJäWæ!^è+eØsŽ¾¸·¼¾»ÈÊÇÈÊÇÈÊǼ¾»¼¾»ÈÊǼ¾»²´°²´°ÈÊÇÕ×ÔÕÖÓßÒ×Úe[Ô$3ÙCà7k„5»™ ²´°²´°¼¾»¼¾»ÈÊÇÈÊǼ¾»¼¾»²´°ÈÊÇÕ×ÔÕ×ÔßáÞßáÞßáÞßáÞßáÞ²´°²´°§©¦²´°ÈÊÇÈÊÇÕ×ÔßáÞßáÞÕ×ÔßáÞßáÞ§©¦§©¦ßáÞÕ×ÔÈÊÇÕ×ÔÕ×ÔßáÞÕ×ÔßáÞßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÈÊÇöøõöøõöøõÈÊÇÈÊÇÈÊÇÕ×ÔÕ×ÔÕ×ÔÕ×ÔÈÊǼ¾»²´°ëîêöøõëîêëîêßáÞëîêëîêöøõ²´°²´°¼¾»ÈÊÇÕ×ÔÕ×ÔÈÊÇÈÊÇÈÊÇÈÊÇÈÊÇÕ×ÔÕ×ÔßáÞßáÞÕ×ÔÕ×ÔÈÊǼ¾»ÈÊÇßáÞëîꧩ¦²´°¼¾»ÈÊÇÈÊÇÈÊÇÈÊÇÈÊÇÕ×ÔÕ×ÔÕ×ÔÈÊÇÈÊÇÕ×ÔÕ×ÔÈÊÇÕ×ÔÈÊÇÈÊDz´°²´°¼¾»Õ×ÔßáÞ²´°¼¾»¼¾»¼¾»ÈÊÇÈÊÇÕ×ÔÕ×ÔßáÞßáÞÕ×ÔÈÊǼ¾»¼¾»¼¾»¼¾»¼¾»ÈÊÇÈÊÇÈÊǼ¾»¼¾»¼¾»ÈÊÇÈÊDz´°²´°¼¾»¼¾»ÈÊÇÈÊÇÈÊÇÈÊÇÕ×ÔÈÊǼ¾»²´°²´°²´°§©¦²´°ÈÊÇÈÊÇÈÊǼ¾»¼¾»ÈÊÇÕ×ÔÕ×Ô§©¦§©¦²´°²´°¼¾»²´°²´°²´°²´°§©¦§©¦§©¦²´°¼¾»¼¾»¼¾»¼¾»ÈÊÇÕ×ÔßáÞ§©¦²´°²´°§©¦§©¦²´°¼¾»¼¾»²´°¼¾»Õ×ÔÈÊÇÈÊÇÈÊÇÈÊÇÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞëîêÕ×ÔÕ×ÔÕ×ÔßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞëîêÕ×ÔÕ×ÔÈÊÇÕ×ÔßáÞßáÞßáÞßáÞëîêëîêëîêëîêëîêëîêßáÞßáÞßáÞöøõßáÞÕ×ÔØ´¸âŒ›òsŽöl„ê{†å¥¨ßßÜëîêöøõöøõöøõöøõöøõëîêëîêëîêöøõöøõßáÞß××èžëd…ït‰÷}öyŽíq†íj}ð†šëìéëîêöøõöøõöøõöøõöøõëîêëîêöøõöøõÕÖÓÙÀÅîR‚ì?{îc‰ó‘õ˜ð’œë†•ïs‰õeñ¶ÃëîêëîêöøõöøõöøõöøõöøõöøõëîêëîêöøõÏ°¸îPê<oçIrên‚ï{“謭뛠îðfô‚•ßáÞëîêëîêöøõöøõöøõöøõëîêëîêëîêöøõ§©¦ê6wé0oæ9jèMmëbvìm|ëÔÍêÇÁ쇒ílõl€ßáÞßáÞëîêëîêëîêëîêëîêëîêëîêëîêöøõ§©¦â fæ få.gëDlðVvð\qîéãïØÒó£©î€‹ëf|óg€Õ×ÔßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞëîê²´°§©¦áZå_å%dè8hëGkíGjîîçðåØó£©ïi|ëYsìvŒÈÊÇÕ×ÔÕ×ÔßáÞßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞßáÞ²´°²´°à<áIå Wå/bæ8fæ8fó£©ïìÞï^uí_zò\sؤ©¼¾»ÈÊÇÕ×ÔÕ×ÔÕ×ÔÈÊÇÈÊÇÈÊÇÕ×ÔßáÞßáÞ¼¾»¼¾»ÖÚ(áEä'[å,cå0dë9eïHgìRkíYræw‡¼½º²´°²´°ÈÊÇÕ×ÔÈÊǼ¾»²´°²´°ÈÊÇÕ×ÔÕ×Ô¼¾»ÈÊÇÓØÞ<âMä Zå&_è/bí;gîJlâg}¾¸¶¼¾»¼¾»¼¾»¼¾»ÈÊǼ¾»¼¾»²´°¼¾»¼¾»ÈÊÇÈÊÇÈÊÇÈÊÇÔ²®à6”ÜÛ.áJäWæ!^è+eØsŽ¾¸·¼¾»ÈÊÇÈÊÇÈÊǼ¾»²´°²´°¼¾»¼¾»ÈÊǼ¾»¼¾»¼¾»ÈÊÇÕ×ÔÕÖÓßÒ×Úe[Ô$3ÙCà7kÞjŽ»™ ²´°²´°¼¾»ÈÊÇÕ×ÔÕ×ÔÈÊDz´°Õ×ÔÈÊǼ¾»²´°ÈÊÇÕ×ÔÕ×ÔßáÞßáÞßáÞßáÞßáÞÕ×Ô¼¾»²´°²´°²´°²´°¼¾»Õ×ÔëîêßáÞßáÞÕ×ÔÈÊÇÈÊÇÕ×ÔßáÞßáÞÕ×ÔÕ×ÔÕ×ÔßáÞ§©¦²´°²´°¼¾»ßáÞÕ×ÔÈÊÇÕ×ÔßáÞßáÞÕ×ÔÕ×ÔÈÊDz´°¼¾»ßáÞßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞÕ×ÔöøõöøõöøõÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÈÊǼ¾»²´°Õ×ÔßáÞöøõöøõöøõöøõöøõöøõÈÊÇÈÊÇÈÊÇÕ×ÔÕ×ÔÕ×ÔÕ×ÔÈÊǼ¾»²´°¼¾»ëîêëîêëîêëîêßáÞëîêëîêßáÞëîêëîêöøõ²´°²´°¼¾»ÈÊÇÕ×ÔÕ×ÔÈÊÇÈÊÇÈÊÇÈÊÇÈÊÇßáÞÕ×ÔÕ×ÔßáÞëîêëîêÕ×ÔÕ×ÔÕ×ÔÈÊǼ¾»ÈÊÇÕ×ÔßáÞ§©¦²´°¼¾»ÈÊÇÈÊÇÈÊÇÈÊÇÈÊÇÕ×ÔÕ×ÔÕ×ÔßáÞßáÞÈÊÇÈÊÇÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÈÊÇÈÊDz´°²´°¼¾»ÈÊDz´°¼¾»¼¾»¼¾»ÈÊÇÈÊÇÕ×ÔÕ×ÔßáÞßáÞÕ×ÔÈÊǼ¾»¼¾»¼¾»¼¾»¼¾»ÈÊÇÈÊÇÈÊǼ¾»¼¾»¼¾»ÈÊÇÈÊDz´°²´°¼¾»¼¾»ÈÊÇÈÊÇÈÊÇÈÊÇÕ×ÔÈÊǼ¾»²´°²´°²´°§©¦²´°ÈÊÇÈÊÇÈÊǼ¾»¼¾»ÈÊÇÕ×ÔÕ×Ô§©¦§©¦²´°²´°¼¾»²´°²´°²´°²´°§©¦§©¦§©¦²´°¼¾»¼¾»¼¾»¼¾»ÈÊÇÕ×ÔßáÞ§©¦²´°²´°§©¦§©¦²´°¼¾»¼¾»²´°¼¾»Õ×ÔÈÊÇÈÊÇÈÊÇÈÊÇÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞëîêÕ×ÔÕ×ÔÕ×ÔßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞëîêÕ×ÔÕ×ÔÈÊÇÕ×ÔßáÞßáÞßáÞßáÞëîêëîêëîêëîêëîêëîêßáÞßáÞßáÞöøõßáÞÕ×ÔØ´¸âŒ›òsŽöl„ê{†å¥¨ßßÜëîêöøõöøõöøõöøõöøõëîêëîêëîêöøõöøõßáÞß××èžëd…ït‰÷}öyŽíq†íj}ð†šëìéëîêöøõöøõöøõöøõöøõëîêëîêöøõöøõÕÖÓÙÀÅîR‚ì?{îc‰ó‘õ˜ð’œë†•ïs‰õeñ¶ÃëîêëîêöøõöøõöøõöøõöøõöøõëîêëîêöøõÏ°¸îPê<oçIrên‚ï{“謭뛠îðfô‚•ßáÞëîêëîêöøõöøõöøõöøõëîêëîêëîêöøõ§©¦ê6wé0oæ9jèMmëbvìm|ëÔÍêÇÁ쇒ílõl€ßáÞßáÞëîêëîêëîêëîêëîêëîêëîêëîêöøõ§©¦â fæ få.gëDlðVvð\qîéãïØÒó£©î€‹ëf|óg€Õ×ÔßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞëîê²´°§©¦áZå_å%dè8hëGkíGjîîçðåØó£©ïi|ëYsìvŒÈÊÇÕ×ÔÕ×ÔßáÞßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞßáÞ²´°²´°à<áIå Wå/bæ8fæ8fó£©ïìÞï^uí_zò\sؤ©¼¾»ÈÊÇÕ×ÔÕ×ÔÕ×ÔÈÊÇÈÊÇÈÊÇÕ×ÔßáÞßáÞ¼¾»¼¾»ÖÚ(áEä'[å,cå0dë9eïHgìRkíYræw‡¼½º²´°²´°ÈÊÇÕ×ÔÈÊǼ¾»²´°²´°ÈÊÇÕ×ÔÕ×Ô¼¾»ÈÊÇÓØÞ<âMä Zå&_è/bí;gîJlâg}¾¸¶¼¾»¼¾»¼¾»¼¾»ÈÊǼ¾»¼¾»²´°²´°¼¾»ÈÊÇÈÊÇÈÊÇÈÊÇÔ²®à6”ÜÛ.áJäWæ!^è+eØsŽ¾¸·¼¾»ÈÊÇÈÊÇÈÊǼ¾»¼¾»ÈÊǼ¾»²´°²´°ÈÊÇÕ×ÔÕÖÓßÒ×Úe[Ô$3ÙCà7k„5»™ ²´°²´°¼¾»¼¾»ÈÊÇÈÊǼ¾»¼¾»²´°ÈÊÇÕ×ÔÕ×ÔßáÞßáÞßáÞßáÞßáÞ²´°²´°§©¦²´°ÈÊÇÈÊÇÕ×ÔßáÞßáÞÕ×ÔßáÞßáÞ§©¦§©¦ßáÞÕ×ÔÈÊÇÕ×ÔÕ×ÔßáÞÕ×ÔßáÞßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÈÊÇöøõöøõöøõÈÊÇÈÊÇÈÊÇÕ×ÔÕ×ÔÕ×ÔÕ×ÔÈÊǼ¾»²´°ëîêöøõëîêëîêßáÞëîêëîêöøõ²´°²´°¼¾»ÈÊÇÕ×ÔÕ×ÔÈÊÇÈÊÇÈÊÇÈÊÇÈÊÇÕ×ÔÕ×ÔßáÞßáÞÕ×ÔÕ×ÔÈÊǼ¾»ÈÊÇßáÞëîꧩ¦²´°¼¾»ÈÊÇÈÊÇÈÊÇÈÊÇÈÊÇÕ×ÔÕ×ÔÕ×ÔÈÊÇÈÊÇÕ×ÔÕ×ÔÈÊÇÕ×ÔÈÊÇÈÊDz´°²´°¼¾»Õ×ÔßáÞ²´°¼¾»¼¾»¼¾»ÈÊÇÈÊÇÕ×ÔÕ×ÔßáÞßáÞÕ×ÔÈÊǼ¾»¼¾»¼¾»¼¾»¼¾»ÈÊÇÈÊÇÈÊǼ¾»¼¾»¼¾»ÈÊÇÈÊDz´°²´°¼¾»¼¾»ÈÊÇÈÊÇÈÊÇÈÊÇÕ×ÔÈÊǼ¾»²´°²´°²´°§©¦²´°ÈÊÇÈÊÇÈÊǼ¾»¼¾»ÈÊÇÕ×ÔÕ×Ô§©¦§©¦²´°²´°¼¾»²´°²´°²´°²´°§©¦§©¦§©¦²´°¼¾»¼¾»¼¾»¼¾»ÈÊÇÕ×ÔßáÞ§©¦²´°²´°§©¦§©¦²´°¼¾»¼¾»²´°¼¾»Õ×ÔÈÊÇÈÊÇÈÊÇÈÊÇÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞëîêÕ×ÔÕ×ÔÕ×ÔßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞëîêÕ×ÔÕ×ÔÈÊÇÕ×ÔßáÞßáÞßáÞßáÞëîêëîêëîêëîêëîêëîêßáÞßáÞßáÞöøõßáÞÕ×Ô×½¿âžðwôqˆê€‹å©«ßÝÚëîêöøõöøõöøõöøõöøõëîêëîêëîêöøõöøõßáÞß××è‚Ÿêd…ït‰÷}öyŽíq†íj}ò”ëéæëîêöøõöøõöøõöøõöøõëîêëîêöøõöøõÕÖÓÙÀÅîR‚ì?{îc‰ó‘õ˜ð’œë†•ïs‰õeó£µëîêëîêöøõöøõöøõöøõöøõöøõëîêëîêöøõÏ°¸îPê<oçIrên‚ï{“謭뛠îðföp‡ßáÞëîêëîêöøõöøõöøõöøõëîêëîêëîêöøõ§©¦ê6wé0oæ9jèMmëbvìm|ëÔÍêÇÁ쇒íl÷cxßàÝßáÞëîêëîêëîêëîêëîêëîêëîêëîêöøõ§©¦â fæ få.gëDlðVvð\qîéãïØÒó£©î€‹ëf|ôa{ÕÕÒßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞßáÞëîê²´°§©¦áZå_å%dè8hëGkíGjîîçðåØó£©ïi|ëYsìuŒÈÊÇÕ×ÔÕ×ÔßáÞßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞßáÞ²´°²´°à<áIå Wå/bæ8fæ8fó£©ïìÞï^uí_zò\sᬲ¼¾»ÈÊÇÕ×ÔÕ×ÔÕ×ÔÕ×ÔÈÊÇÈÊÇÕ×ÔßáÞßáÞ¼¾»¼¾»ÖÚ(áEä'[å,cå0dë9eïHgìRkíYr¼½º²´°²´°ÈÊÇÕ×ÔÈÊÇÈÊDz´°²´°ÈÊÇÕ×ÔÕ×Ô¼¾»ÈÊÇÓ ØÞ<âMä Zå&_è/bí;hîKlèt‰¾¹·¼¾»²´°¼¾»ÈÊÇÕ×Ô§©¦¼¾»ÈÊÇÈÊÇÈÊÇÈÊÇÔ©¥àC˜ÝÜ0áKåUæ"[å7nЉœ¼»¹¼¾»¼¾»ÈÊÇÈÊÇÈÊǧ©¦²´°ÈÊÇÕ×ÔÙ ¾à†¶Ü Õ ÝFáK§>A
+¼š¢º¤§¼¾»¼¾»ÈÊÇÕ×ÔÕ×ÔßáÞÕ×ÔßáÞßáÞ²´°²´°ÈÊÇÈÊÇÕ×ÔßáÞÕ×ÔÕ×ÔßáÞÕ×ÔÈÊÇÕ×ÔßáÞßáÞÕ×ÔßáÞßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔÈÊÇÈÊÇÈÊÇÕ×ÔÕ×ÔÕ×ÔÕ×ÔÕ×ÔßáÞëîêëîê²´°²´°¼¾»ÈÊÇÕ×ÔÕ×ÔÈÊÇÈÊÇÕ×ÔÕ×Ô¼¾»Õ×ÔßáÞÈÊǼ¾»ÈÊÇßáÞëîꧩ¦²´°¼¾»ÈÊÇÈÊÇÈÊÇÈÊÇÕ×ÔÕ×ÔÕ×Ô¼¾»ÈÊÇÈÊÇÕ×ÔÕ×ÔÈÊDz´°²´°¼¾»Õ×Ôëîê²´°¼¾»¼¾»¼¾»ÈÊÇÈÊÇÕ×ÔÕ×ÔÕ×Ô¼¾»¼¾»¼¾»¼¾»²´°ÈÊÇÈÊǼ¾»¼¾»¼¾»ÈÊÇÕ×ÔßáÞ²´°²´°¼¾»¼¾»ÈÊÇÈÊÇÈÊÇÈÊÇÕ×ÔÈÊǼ¾»²´°¼¾»²´°²´°²´°¼¾»ÈÊÇÈÊǼ¾»¼¾»ÈÊÇÕ×ÔÕ×Ô§©¦§©¦²´°²´°¼¾»²´°²´°²´°²´°§©¦§©¦§©¦²´°¼¾»¼¾»¼¾»¼¾»ÈÊÇÕ×ÔßáÞ§©¦²´°²´°§©¦§©¦²´°¼¾»²´°²´°ÈÊÇÕ×Ô \ No newline at end of file
diff --git a/bubbob/images/ghosty.ppm b/bubbob/images/ghosty.ppm
new file mode 100644
index 0000000..52ff8d3
--- /dev/null
+++ b/bubbob/images/ghosty.ppm
Binary files differ
diff --git a/bubbob/images/ghosty_angry.ppm b/bubbob/images/ghosty_angry.ppm
new file mode 100644
index 0000000..95f533e
--- /dev/null
+++ b/bubbob/images/ghosty_angry.ppm
Binary files differ
diff --git a/bubbob/images/glue.ppm b/bubbob/images/glue.ppm
new file mode 100644
index 0000000..0723e34
--- /dev/null
+++ b/bubbob/images/glue.ppm
@@ -0,0 +1,4 @@
+P6
+32 32
+255
+áÝÝáÝÝÊ#áÝÝáÝÝÔ#Ô#áÝÝáÝÝÔ#Ô#áÝÝáÝÝè"Ü#Ü#áÝÝáÝÝÿ86ÿ86ÿ86è&Ü#Ü#è"áÝÝáÝÝÿ86ÿ86ÿCCÿCCÿCCí#è&Ü#è"ÿ86áÝÝáÝÝÿ86ÿ86ÿCCÿCCÿ86ÿ86í#è&è"Ü#è"ÿ86ÿCCáááÝÙÙÿ86ÿ86ÿCCÿ86ÿ86è&è&è&è"è"è"è"ÿ86ÿCCÿ86ÝÙÙÿ86ÿ86ÿCCÿ86è"è&è&Ü#Ü#Ü#è"è"è"è"ÿ86ÿ86ÿ86ÿ86ÿCCÿ86è"í#Ü#Ü#Ü#Ô#Ô#Ô#Ô#è"ÿ86ÿ86ÿ86ÿ86ÿCCÿ86è"è&Ü#Ü#Ü#Ô#Ô#Ê#Ê#Ä#¼#¼#ÿ86ÿ86ÿCCÿ86è"è&Ü#Ü#Ô#Ô#Ô#Ê#Ä#¼#¼#ÿ86ÿ86ÿCCÿ86è"è&Ü#Ü#Ô#Ô#Ê#Ä#¼#¼#ÿ86ÿ86ÿCCÿ86è"è&Ü#Ü#Ô#Ô#Ê#Ä#¼#ÿ86ÿ86ÿCCÿ86è"è&Ü#Ü#Ô#Ô#Ê#Ä#¼#æææÿ86ÿCCÿ86è"è&Ü#Ü#Ô#Ô#Ê#Ä#¼#æææÿÿÿÿ96è"è&Ü#Ü#Ô#Ô#Ê#Ä#¼#æææÿÿÿæææè&Ü#Ü#Ô#Ô#Ê#Ä#¼#ÿÿÿÿÿÿæéêáááÜ#Ô#Ô#Ê#Ä#¼#ÿÿÿæéêáááááá×××Ô#Ê#Ä#¼#¾ÀÂæéêááá×××××××××Ä#¼#¾À³¶·©«¬××××××ÍÍÍÍÍÍÍÍ;À³¶·©«¬¾À³¶·©«¬¾À³¶·©«¬ßß«©«¬ßß«ßØŸßß«ßØŸßØŸßØŸßØŸßØŸßØŸßß«ßß«ßß«ßß«ßß«ßß«ßß«ßß«¼±ußØžÿ÷µÿ÷µÿ÷µÿ÷µÿ÷µÿ÷µÿ÷µßØžßß«ßß«ßß«ßß«ßß«ßß«ßß«ßß«ÕÕ£ÕÕ£ÕÕ£ÕÕ£ÕÕ£´ªqçÚçÚçÚçÚçÚçÚçÚçÚçÚçÚçÚçÚçÚçÚçÚçÚçÚçÚçÚçÚ´ªq´ªq´ªq \ No newline at end of file
diff --git a/bubbob/images/gramy.ppm b/bubbob/images/gramy.ppm
new file mode 100644
index 0000000..ad2c757
--- /dev/null
+++ b/bubbob/images/gramy.ppm
Binary files differ
diff --git a/bubbob/images/gramy_angry.ppm b/bubbob/images/gramy_angry.ppm
new file mode 100644
index 0000000..c1672ee
--- /dev/null
+++ b/bubbob/images/gramy_angry.ppm
Binary files differ
diff --git a/bubbob/images/hat1.ppm b/bubbob/images/hat1.ppm
new file mode 100644
index 0000000..7e15239
--- /dev/null
+++ b/bubbob/images/hat1.ppm
@@ -0,0 +1,5 @@
+P6
+# CREATOR: The GIMP's PNM Filter Version 1.0
+192 48
+255
+###111<<<)))222222111&&&&&&111222222)))<<<111###+++666<<<777111)))333333888===BBBBBB===......$$$.........333333888888===::::::===888888333333.........$$$......===BBBBBB===888333333)))111777<<<666+++:::BBB@@@======888888333333)))...333333666888===BBBBBB===888888............))).........333333888888===BBBBBB======BBBBBB===888888333333.........)))............888888===BBBBBB===888666333333...)))333333888888======@@@BBB:::888BBBBBB@@@======888888333333............333333888===@@@BBBBBB===888888333333.........)))))).........333333888888===BBBBBB======  ======BBBBBB===888888333333.........)))))).........333333888888===BBBBBB@@@===888333333............333333888888======@@@BBBBBB888333888BBBBBB@@@======888888333333...............333333666888===BBBBBB===;;;888888333333333..................333333888888===BBBBBB=========888888=========BBBBBB===888888333333..................333333333888888;;;===BBBBBB===888666333333...............333333888888======@@@BBBBBB888333333888BBBBBBBBB@@@======888888333333.........))).........$$$......333333888===@@@BBBBBB===888888666333333333)))...............333333888888===BBB=========888333------333888=========BBB===888888333333...............)))333333333666888888===BBBBBB@@@===888333333......$$$.........))).........333333888888======@@@BBBBBBBBB888333)))333888BBBBBB@@@======888888888333333.........)))$$$............)))......333333888===BBBBBB===;;;888888333333333..................888888===BBBBBB======888888333000000333888888======BBBBBB===888888..................333333333888888;;;===BBBBBB===888333333......)))............$$$))).........333333888888888======@@@BBBBBB888333)))...333888BBBBBB@@@======888888333333............))))))..................333333888===BBBBBB===888888666333333333...............888===BBB======888888333000...!!!!!!...000333888888======BBB===888...............333333333666888888===BBBBBB===888333333..................))))))............333333888888======@@@BBBBBB888333.....................333888BBBBBB@@@======888888333333.........))))))))))))...............888===BBBBBB===888888333333333.........))).........BBBBBB======888333000...000))))))000...000333888======BBBBBB.........))).........333333333888888===BBBBBB===888...............)))))))))))).........333333888888======@@@BBBBBB888333................................................@@@======888888333333.........))))))))) ...............BBBBBB===888888333333333.........---.........======888888333000000000******000000000333888888======.........---.........333333333888888===BBBBBB............... ))))))))).........333333888888======@@@...............................................................333333.........)))))))))###..................888888333333333.........(((......888888333000000000...******...000000000333888888......(((.........333333333888888..................###))))))))).........333333..................................................................)))))))))###...............333333333.........$$$......888333000000000+++$$$$$$+++000000000333888......$$$.........333333333...............###)))))))))............................................................)))....................................000000+++))))))+++000000....................................)))....................................................................................++++++))))))++++++.............................................................................................)))$$$$$$))).......................................................................................................................................................................................... \ No newline at end of file
diff --git a/bubbob/images/hat2.ppm b/bubbob/images/hat2.ppm
new file mode 100644
index 0000000..bce2b98
--- /dev/null
+++ b/bubbob/images/hat2.ppm
@@ -0,0 +1,5 @@
+P6
+# CREATOR: The GIMP's PNM Filter Version 1.0
+192 48
+255
+{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{€€€{{{{{{{{{€€€€€€€€€€€€{{{{{{{{{€€€{{{{{{{{{{{{{{{{{{€€€†††ŒŒŒ––––––{{{{{{€€€€€€††††††{{{{{{€€€€€€€€€††††††††††††€€€€€€€€€{{{{{{††††††€€€€€€{{{{{{––––––ŒŒŒ†††€€€{{{{{{{{{{{{€€€†††ŒŒŒ––––––”””‘‘‘‘‘‘ŒŒŒŒŒŒ{{{{{{€€€€€€††††††ŒŒŒ‘‘‘{{{{{{{{{{{{€€€€€€€€€††††††††††††€€€€€€€€€{{{{{{{{{{{{‘‘‘ŒŒŒ††††††€€€€€€{{{{{{ŒŒŒŒŒŒ‘‘‘‘‘‘”””––––––ŒŒŒ†††€€€{{{{{{{{{{{{€€€†††ŒŒŒ––––––”””‘‘‘‘‘‘ŒŒŒŒŒŒ††††††€€€€€€€€€{{{{{{€€€€€€€€€††††††ŒŒŒ‘‘‘––––––‘‘‘{{{{{{{{{€€€€€€€€€††††††ŒŒŒŒŒŒ‘‘‘‘‘‘ŒŒŒŒŒŒ††††††€€€€€€€€€{{{{{{{{{‘‘‘––––––‘‘‘ŒŒŒ††††††€€€€€€€€€{{{{{{€€€€€€€€€††††††ŒŒŒŒŒŒ‘‘‘‘‘‘”””––––––ŒŒŒ†††€€€{{{{{{{{{{{{€€€†††ŒŒŒ––––––”””‘‘‘‘‘‘ŒŒŒŒŒŒ††††††€€€€€€€€€{{{{{{{{{uuuuuu{{{{{{€€€€€€††††††‰‰‰ŒŒŒ‘‘‘––––––‘‘‘ŒŒŒŒŒŒ€€€€€€{{{{{{{{{{{{€€€€€€€€€††††††ŒŒŒŒŒŒ‘‘‘––––––––––––‘‘‘ŒŒŒŒŒŒ††††††€€€€€€€€€{{{{{{{{{{{{€€€€€€ŒŒŒŒŒŒ‘‘‘––––––‘‘‘ŒŒŒ‰‰‰††††††€€€€€€{{{{{{uuuuuu{{{{{{{{{€€€€€€€€€††††††ŒŒŒŒŒŒ‘‘‘‘‘‘”””––––––ŒŒŒ†††€€€{{{{{{{{{{{{{{{€€€†††ŒŒŒ––––––”””‘‘‘‘‘‘ŒŒŒŒŒŒ††††††€€€€€€€€€{{{{{{{{{uuuuuu{{{{{{€€€€€€€€€††††††ŒŒŒ‘‘‘”””––––––‘‘‘ŒŒŒŒŒŒ†††††††††€€€€€€€€€€€€{{{{{{{{{€€€€€€€€€††††††ŒŒŒŒŒŒ‘‘‘––––––‘‘‘‘‘‘––––––‘‘‘ŒŒŒŒŒŒ††††††€€€€€€€€€{{{{{{{{{€€€€€€€€€€€€†††††††††ŒŒŒŒŒŒ‘‘‘––––––”””‘‘‘ŒŒŒ††††††€€€€€€€€€{{{{{{uuuuuu{{{{{{{{{€€€€€€€€€††††††ŒŒŒŒŒŒ‘‘‘‘‘‘”””––––––ŒŒŒ†††€€€{{{{{{{{{{{{{{{€€€†††ŒŒŒ–––––––––”””‘‘‘‘‘‘ŒŒŒŒŒŒ††††††€€€€€€€€€{{{{{{{{{uuuuuu{{{{{{€€€€€€††††††‰‰‰ŒŒŒ‘‘‘––––––‘‘‘ŒŒŒŒŒŒ†††††††††€€€€€€€€€€€€€€€{{{{{{€€€€€€€€€††††††ŒŒŒŒŒŒ‘‘‘––––––‘‘‘‘‘‘ŒŒŒŒŒŒ‘‘‘‘‘‘––––––‘‘‘ŒŒŒŒŒŒ††††††€€€€€€€€€{{{{{{€€€€€€€€€€€€€€€†††††††††ŒŒŒŒŒŒ‘‘‘––––––‘‘‘ŒŒŒ‰‰‰††††††€€€€€€{{{{{{uuuuuu{{{{{{{{{€€€€€€€€€††††††ŒŒŒŒŒŒ‘‘‘‘‘‘”””–––––––––ŒŒŒ†††€€€{{{{{{{{{{{{€€€†††ŒŒŒ––––––”””‘‘‘‘‘‘ŒŒŒŒŒŒŒŒŒ††††††€€€€€€€€€{{{{{{{{{uuuuuu€€€€€€€€€{{{{{{€€€€€€††††††ŒŒŒ‘‘‘”””––––––‘‘‘ŒŒŒŒŒŒ‰‰‰†††††††††€€€€€€€€€{{{{{{€€€€€€€€€€€€€€€€€€††††††ŒŒŒŒŒŒ‘‘‘––––––‘‘‘‘‘‘‘‘‘ŒŒŒ†††ƒƒƒƒƒƒ†††ŒŒŒ‘‘‘‘‘‘‘‘‘––––––‘‘‘ŒŒŒŒŒŒ††††††€€€€€€€€€€€€€€€€€€{{{{{{€€€€€€€€€†††††††††‰‰‰ŒŒŒŒŒŒ‘‘‘––––––”””‘‘‘ŒŒŒ††††††€€€€€€{{{{{{€€€€€€€€€uuuuuu{{{{{{{{{€€€€€€€€€††††††ŒŒŒŒŒŒŒŒŒ‘‘‘‘‘‘”””––––––ŒŒŒ†††€€€{{{{{{{{{{{{€€€†††ŒŒŒ––––––”””‘‘‘‘‘‘ŒŒŒŒŒŒ††††††€€€€€€€€€€€€{{{{{{{{{uuuuuu€€€€€€€€€€€€{{{{{{€€€€€€††††††ŒŒŒ‘‘‘––––––‘‘‘ŒŒŒŒŒŒ†††††††††€€€€€€€€€€€€{{{{{{€€€€€€€€€€€€€€€††††††ŒŒŒŒŒŒ‘‘‘–––‘‘‘‘‘‘‘‘‘ŒŒŒ††††††ƒƒƒƒƒƒ††††††ŒŒŒ‘‘‘‘‘‘‘‘‘–––‘‘‘ŒŒŒŒŒŒ††††††€€€€€€€€€€€€€€€{{{{{{€€€€€€€€€€€€†††††††††ŒŒŒŒŒŒ‘‘‘––––––‘‘‘ŒŒŒ††††††€€€€€€{{{{{{€€€€€€€€€€€€uuuuuu{{{{{{{{{€€€€€€€€€€€€††††††ŒŒŒŒŒŒ‘‘‘‘‘‘”””––––––ŒŒŒ†††€€€{{{{{{€€€€€€€€€€€€€€€€€€€€€†††ŒŒŒ––––––”””‘‘‘‘‘‘ŒŒŒŒŒŒ††††††€€€€€€€€€{{{{{{{{{uuuuuu€€€€€€€€€€€€€€€€€€††††††ŒŒŒ‘‘‘––––––‘‘‘ŒŒŒŒŒŒ‰‰‰†††††††††€€€€€€€€€€€€{{{{{{€€€€€€€€€€€€ŒŒŒŒŒŒ‘‘‘––––––‘‘‘‘‘‘ŒŒŒŒŒŒ†††ƒƒƒƒƒƒ}}}€€€€€€}}}ƒƒƒƒƒƒ†††ŒŒŒŒŒŒ‘‘‘‘‘‘––––––‘‘‘ŒŒŒŒŒŒ€€€€€€€€€€€€{{{{{{€€€€€€€€€€€€†††††††††‰‰‰ŒŒŒŒŒŒ‘‘‘––––––‘‘‘ŒŒŒ††††††€€€€€€€€€€€€€€€€€€uuuuuu{{{{{{{{{€€€€€€€€€††††††ŒŒŒŒŒŒ‘‘‘‘‘‘”””––––––ŒŒŒ†††€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€”””‘‘‘‘‘‘ŒŒŒŒŒŒ††††††€€€€€€€€€{{{{{{{{{uuuuuu€€€€€€€€€€€€€€€€€€ŒŒŒ‘‘‘––––––‘‘‘ŒŒŒŒŒŒ†††††††††€€€€€€€€€€€€{{{{{{{{{€€€€€€€€€ŒŒŒ‘‘‘–––‘‘‘‘‘‘ŒŒŒŒŒŒ†††ƒƒƒ€€€ƒƒƒ€€€}}}{{{{{{}}}€€€ƒƒƒ€€€ƒƒƒ†††ŒŒŒŒŒŒ‘‘‘‘‘‘–––‘‘‘ŒŒŒ€€€€€€€€€{{{{{{{{{€€€€€€€€€€€€†††††††††ŒŒŒŒŒŒ‘‘‘––––––‘‘‘ŒŒŒ€€€€€€€€€€€€€€€€€€uuuuuu{{{{{{{{{€€€€€€€€€††††††ŒŒŒŒŒŒ‘‘‘‘‘‘”””€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€††††††€€€€€€€€€{{{{{{{{{uuuuuu€€€€€€€€€€€€€€€––––––‘‘‘ŒŒŒŒŒŒ†††††††††€€€€€€€€€€€€{{{{{{{{{€€€€€€€€€––––––‘‘‘‘‘‘ŒŒŒ†††ƒƒƒ€€€ƒƒƒ€€€}}}{{{{{{{{{{{{}}}€€€ƒƒƒ€€€ƒƒƒ†††ŒŒŒ‘‘‘‘‘‘––––––€€€€€€€€€{{{{{{{{{€€€€€€€€€€€€†††††††††ŒŒŒŒŒŒ‘‘‘––––––€€€€€€€€€€€€€€€uuuuuu{{{{{{{{{€€€€€€€€€††††††€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€{{{{{{{{{uuuuuu€€€€€€€€€€€€€€€€€€ŒŒŒŒŒŒ†††††††††€€€€€€€€€{{{{{{{{{€€€€€€€€€‘‘‘‘‘‘ŒŒŒŒŒŒ†††ƒƒƒƒƒƒƒƒƒ}}}{{{{{{{{{{{{{{{{{{}}}ƒƒƒƒƒƒƒƒƒ†††ŒŒŒŒŒŒ‘‘‘‘‘‘€€€€€€€€€{{{{{{{{{€€€€€€€€€†††††††††ŒŒŒŒŒŒ€€€€€€€€€€€€€€€€€€uuuuuu{{{{{{{{{€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€†††††††††€€€€€€€€€{{{{{{{{{€€€€€€ŒŒŒŒŒŒ†††ƒƒƒƒƒƒƒƒƒ€€€}}}{{{{{{{{{{{{{{{{{{}}}€€€ƒƒƒƒƒƒƒƒƒ†††ŒŒŒŒŒŒ€€€€€€{{{{{{{{{€€€€€€€€€†††††††††€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€{{{{{{€€€€€€ŒŒŒ†††ƒƒƒƒƒƒƒƒƒ}}}{{{{{{{{{{{{{{{{{{}}}ƒƒƒƒƒƒƒƒƒ†††ŒŒŒ€€€€€€{{{{{{€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€{{{{{{€€€€€€€€€ƒƒƒƒƒƒ}}}{{{{{{{{{{{{{{{{{{}}}ƒƒƒƒƒƒ€€€€€€€€€{{{{{{€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€}}}}}}{{{{{{{{{{{{{{{{{{}}}}}}€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€{{{{{{{{{{{{{{{{{{€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€{{{{{{€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€ \ No newline at end of file
diff --git a/bubbob/images/hat5.ppm b/bubbob/images/hat5.ppm
new file mode 100644
index 0000000..78300f8
--- /dev/null
+++ b/bubbob/images/hat5.ppm
@@ -0,0 +1,5 @@
+P6
+# CREATOR: The GIMP's PNM Filter Version 1.0
+64 48
+255
+{{{{{{€€€€€€†††ŒŒŒ‘‘‘––––––”””‘‘‘ŒŒŒŒŒŒ††††††€€€€€€}}}}}}{{{{{{{{{{{{€€€€€€†††ŒŒŒ‘‘‘––––––”””‘‘‘ŒŒŒŒŒŒ††††††€€€€€€}}}}}}{{{{{{...BBBBBB@@@===;;;;;;888888333''' {{{{{{€€€€€€†††ŒŒŒ‘‘‘––––––”””‘‘‘ŒŒŒŒŒŒ††††††€€€€€€}}}}}}{{{{{{888===BBBBBB@@@===;;;;;;888888333333...{{{{{{€€€€€€†††ŒŒŒ‘‘‘––––––”””‘‘‘ŒŒŒŒŒŒ††††††€€€€€€}}}}}}{{{{{{333888===BBBBBB@@@===;;;;;;888888333333......{{{{{{€€€€€€†††ŒŒŒ‘‘‘––––––”””‘‘‘ŒŒŒŒŒŒ††††††€€€€€€}}}}}}{{{{{{...333888===BBBBBB@@@===;;;;;;888888333333......+++{{{{{{€€€€€€†††ŒŒŒ‘‘‘––––––”””‘‘‘ŒŒŒŒŒŒ††††††€€€€€€}}}}}}{{{{{{&&&...333888===BBBBBB@@@===;;;;;;888888333333......+++$$${{{{{{€€€€€€†††ŒŒŒ‘‘‘––––––”””‘‘‘ŒŒŒŒŒŒ††††††€€€€€€}}}}}}{{{{{{......333888===BBBBBB@@@===;;;;;;888888333333......++++++{{{{{{€€€€€€†††ŒŒŒ‘‘‘––––––”””‘‘‘ŒŒŒŒŒŒ††††††€€€€€€}}}}}}{{{{{{......333888===BBBBBB@@@===;;;;;;888888333333......++++++{{{{{{€€€€€€†††ŒŒŒ‘‘‘––––––”””‘‘‘ŒŒŒŒŒŒ††††††€€€€€€}}}}}}{{{{{{$$$......333888===BBBBBB@@@===;;;;;;888888333333......++++++$$$€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€(((......333888===BBBBBB@@@===;;;;;;888888333333......++++++(((€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€.............................................................................................................................................................................. \ No newline at end of file
diff --git a/bubbob/images/ice_cyan_big.ppm b/bubbob/images/ice_cyan_big.ppm
new file mode 100644
index 0000000..d22c371
--- /dev/null
+++ b/bubbob/images/ice_cyan_big.ppm
Binary files differ
diff --git a/bubbob/images/ice_violet_big.ppm b/bubbob/images/ice_violet_big.ppm
new file mode 100644
index 0000000..d1d967e
--- /dev/null
+++ b/bubbob/images/ice_violet_big.ppm
Binary files differ
diff --git a/bubbob/images/keys.ppm b/bubbob/images/keys.ppm
new file mode 100644
index 0000000..4d20147
--- /dev/null
+++ b/bubbob/images/keys.ppm
Binary files differ
diff --git a/bubbob/images/level_digits.ppm b/bubbob/images/level_digits.ppm
new file mode 100644
index 0000000..1110f8b
--- /dev/null
+++ b/bubbob/images/level_digits.ppm
Binary files differ
diff --git a/bubbob/images/lightning_large.ppm b/bubbob/images/lightning_large.ppm
new file mode 100644
index 0000000..67ac930
--- /dev/null
+++ b/bubbob/images/lightning_large.ppm
Binary files differ
diff --git a/bubbob/images/lightning_small.ppm b/bubbob/images/lightning_small.ppm
new file mode 100644
index 0000000..cd3cf22
--- /dev/null
+++ b/bubbob/images/lightning_small.ppm
Binary files differ
diff --git a/bubbob/images/monky.ppm b/bubbob/images/monky.ppm
new file mode 100644
index 0000000..01afbae
--- /dev/null
+++ b/bubbob/images/monky.ppm
Binary files differ
diff --git a/bubbob/images/monky_angry.ppm b/bubbob/images/monky_angry.ppm
new file mode 100644
index 0000000..786d39a
--- /dev/null
+++ b/bubbob/images/monky_angry.ppm
Binary files differ
diff --git a/bubbob/images/nasty.ppm b/bubbob/images/nasty.ppm
new file mode 100644
index 0000000..a006bcd
--- /dev/null
+++ b/bubbob/images/nasty.ppm
Binary files differ
diff --git a/bubbob/images/nasty_angry.ppm b/bubbob/images/nasty_angry.ppm
new file mode 100644
index 0000000..58375e1
--- /dev/null
+++ b/bubbob/images/nasty_angry.ppm
Binary files differ
diff --git a/bubbob/images/orcy.ppm b/bubbob/images/orcy.ppm
new file mode 100644
index 0000000..1ddb609
--- /dev/null
+++ b/bubbob/images/orcy.ppm
Binary files differ
diff --git a/bubbob/images/orcy_angry.ppm b/bubbob/images/orcy_angry.ppm
new file mode 100644
index 0000000..59b51c3
--- /dev/null
+++ b/bubbob/images/orcy_angry.ppm
Binary files differ
diff --git a/bubbob/images/palettes.dat b/bubbob/images/palettes.dat
new file mode 100644
index 0000000..5070cbb
--- /dev/null
+++ b/bubbob/images/palettes.dat
Binary files differ
diff --git a/bubbob/images/pastec_big.ppm b/bubbob/images/pastec_big.ppm
new file mode 100644
index 0000000..6805f73
--- /dev/null
+++ b/bubbob/images/pastec_big.ppm
@@ -0,0 +1,44 @@
+P6
+# CREATOR: GIMP PNM Filter Version 1.1
+90 90
+255
+ÿÿÎÿÿÎÿÿÎÿÿÎÿÿÎÿûÌÿ³­ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¥¥ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ  ÿ¦¦ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ¨¨ÿ³­ÿûÌÿÿÎÿÿÎÿÿÎÿÿÎÿÿÐÿÿÌÿÿÌÿÿÌÿÿÌÿùÉÿà½ÿ»«ÿ¢Ÿÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ˜˜ÿ’’ÿ••ÿ››ÿ››ÿ››ÿ››ÿ””ÿ””ÿ’’ÿ˜˜ÿ››ÿ››ÿ››ÿ››ÿ››ÿ››ÿ¢Ÿÿ»«ÿà½ÿùÉÿÿÌÿÿÌÿÿÍÿÿÎÿÿÌÿÿÌÿÿÌÿÿÌÿ÷ÈÿÞ»ÿºªÿ¡ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ——ÿ——ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ——ÿ‹‹ÿÿŠŠÿ””ÿ™™ÿ™™ÿ––ÿŒŒÿ‰‰ÿ‰‰ÿ’’ÿ––ÿ——ÿ™™ÿ™™ÿ™™ÿ™™ÿ¡ÿºªÿÞ»ÿ÷ÈÿÿÌÿÿÌÿÿÌÿÿÎûÿÌûÿÌûÿÌûÿÌûöÇüÛ¹þµ¤ÿ™•ÿ‘‘ÿ’’ÿ““ÿ””ÿ••ÿ••ÿ••ÿ••ÿ••ÿ––ÿ˜˜ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ˜˜ÿ––ÿ••ÿ““ÿ‘‘ÿ‘‘ÿ““ÿ••ÿ••ÿ••ÿÿ’’ÿÿ~~ÿ‹‹ÿÿŽŽÿ‘‘ÿ††ÿ‹‹ÿ~~ÿ„„ÿˆˆÿ‡‡ÿ‹‹ÿ‹‹ÿŒŒÿŒŒÿŒŒÿ•‘þ²¡üÚ·ûöÇûÿÌûÿÌüÿÌÿÿÎîÿÌîÿÌîÿÌîÿÌðôÅôÓ°ù¤“þƒÿyyÿ}}ÿƒƒÿ‡‡ÿˆˆÿˆˆÿˆˆÿˆˆÿŠŠÿŽŽÿ““ÿ˜˜ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ˜˜ÿ““ÿŽŽÿŠŠÿ††ÿ„„ÿˆˆÿŠŠÿˆˆÿˆˆÿˆˆÿ††ÿ„„ÿ~~ÿ„„ÿƒƒÿ€€ÿƒƒÿÿ€€ÿuuÿooÿjjÿeeÿppÿrrÿggþMMÿggÿggþsoù™ˆôÍ«ðóÄîÿÌîÿÌïÿÌýÿÎÝÿÌÝÿÌÝÿÌÛÿËßñÂèÇ¥ôŒ{üb^ÿWWÿ^^ÿiiÿppÿxxÿxxÿyyÿwwÿyyÿ€€ÿ‹‹ÿ’’ÿ––ÿ˜˜ÿ™™ÿ™™ÿ˜˜ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ˜˜ÿ––ÿ••ÿ——ÿ——ÿ’’ÿ‹‹ÿ€€ÿ{{ÿwwÿ}}ÿƒƒÿƒƒÿ}}ÿwwÿvvÿuuþxxþzzþ||ÿÿvvÿkkÿttÿwwÿkkÿTTÿWWÿWWÿMMÿMMÿ))ý ý..ÿ22üC?óud缚ßî¿ÝÿÌÝÿÌÜÿÌÐÿÎÐÿÌÐÿÌÏÿÌÐÿÌÒðÀÞÁžð{kûKGÿ>>ÿ^^ÿWWÿccÿssÿwwÿiiÿiiÿmmÿƒƒÿÿ‡‡ÿ‘‘ÿ““ÿ––ÿ––ÿ——ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ™™ÿ˜˜ÿ––ÿ••ÿ””ÿ‘‘ÿŒŒÿŠŠÿŒŒÿ‹‹ÿˆˆÿ~~ÿ^^ÿhhÿhhÿ}}ÿxxþ‚‚ÿmmÿmmÿmmÿooÿ~~ýqqývvÿllÿwwÿPPÿYYþOOÿRRÿMMÿEEÿUUÿWWÿ55þ þ''ûÿ ú!î\KÞ°Ôë¼ÐÿÌÐÿÌÐÿÌÎÿÎÌÿÌÌÿÌÌÿÌÊÿÊÏî¿Ý¼šîveûD@ÿTTþWWýhhÿ{{ÿnnÿnnÿ\\ÿaaÿxxÿrrþ€€ÿ‚‚ÿ~~ÿŒŒÿŠŠÿŒŒÿ””ÿ––ÿ––ÿ••ÿ––ÿ˜˜ÿ™™ÿ™™ÿ™™ÿ™™ÿ˜˜ÿ––ÿ••ÿ••ÿ••ÿ––ÿ––ÿ••ÿ‘‘ÿŒŒÿ‰‰ÿ‡‡ÿƒƒÿ||ÿ~~ÿˆˆÿÿuuÿccÿIIÿKKÿ]]ÿiiÿxxÿccþrrÿiiÿqqÿuuýmmþxxÿppÿnnÿPPÿAAþþ;;û%%ûÿBBý]]ý__ÿTTÿGGûÿ
+
+ÿÿýýEE÷ñí
+
+Ý Ê È C“
+ßÙ
+
+ú ýõ ûú,,úüü
+
+ÿÿÿúï
+
+ü77ÿ55þ88ÿý--óæîòùû--ùü==õ ÷ úù
+
+û ÷
+æåæÙ
+àé ãàä
+æ ßã
+
+øEEêè
+
+
+ô11êèêéçá
+Ñ
+
+
+ßâ
diff --git a/bubbob/images/peach_big.ppm b/bubbob/images/peach_big.ppm
new file mode 100644
index 0000000..3f2b6ca
--- /dev/null
+++ b/bubbob/images/peach_big.ppm
Binary files differ
diff --git a/bubbob/images/point_0.ppm b/bubbob/images/point_0.ppm
new file mode 100644
index 0000000..c6ef319
--- /dev/null
+++ b/bubbob/images/point_0.ppm
Binary files differ
diff --git a/bubbob/images/red_Hurry_up.ppm b/bubbob/images/red_Hurry_up.ppm
new file mode 100644
index 0000000..6f957ac
--- /dev/null
+++ b/bubbob/images/red_Hurry_up.ppm
Binary files differ
diff --git a/bubbob/images/sheep.ppm b/bubbob/images/sheep.ppm
new file mode 100644
index 0000000..66fe04a
--- /dev/null
+++ b/bubbob/images/sheep.ppm
Binary files differ
diff --git a/bubbob/images/shot.ppm b/bubbob/images/shot.ppm
new file mode 100644
index 0000000..74606ff
--- /dev/null
+++ b/bubbob/images/shot.ppm
Binary files differ
diff --git a/bubbob/images/spinning_drop.ppm b/bubbob/images/spinning_drop.ppm
new file mode 100644
index 0000000..6bb22f9
--- /dev/null
+++ b/bubbob/images/spinning_drop.ppm
Binary files differ
diff --git a/bubbob/images/springy.ppm b/bubbob/images/springy.ppm
new file mode 100644
index 0000000..16277ba
--- /dev/null
+++ b/bubbob/images/springy.ppm
Binary files differ
diff --git a/bubbob/images/springy_angry.ppm b/bubbob/images/springy_angry.ppm
new file mode 100644
index 0000000..07bb7f1
--- /dev/null
+++ b/bubbob/images/springy_angry.ppm
Binary files differ
diff --git a/bubbob/images/star_large.ppm b/bubbob/images/star_large.ppm
new file mode 100644
index 0000000..513c538
--- /dev/null
+++ b/bubbob/images/star_large.ppm
Binary files differ
diff --git a/bubbob/images/sugar_pie_big.ppm b/bubbob/images/sugar_pie_big.ppm
new file mode 100644
index 0000000..e63512b
--- /dev/null
+++ b/bubbob/images/sugar_pie_big.ppm
Binary files differ
diff --git a/bubbob/images/water_flow.ppm b/bubbob/images/water_flow.ppm
new file mode 100644
index 0000000..0950083
--- /dev/null
+++ b/bubbob/images/water_flow.ppm
@@ -0,0 +1,4 @@
+P6
+16 160
+255
+fÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿÿÿÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ™ÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿ™ÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿÌÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿ™ÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿÿÿÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿ™ÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿ™ÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿ™ÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿÌÌÿÿÿÿfÌÿÌÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿÌÌÿfÌÿfÌÿÿÿÿ™ÌÿfÌÿfÌÿÿÿÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿÿÿÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ™ÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÌÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿ™ÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿÿÿÿÿÿÿfÌÿfÌÿfÌÿ™ÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿÌÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿ™ÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿÿÿÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿ™ÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿ™ÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿ™ÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿfÌÿfÌÿÌÌÿÿÿÿfÌÿÌÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿÿÿÿ™ÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿ™ÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿÿÿÿfÌÿÿÿÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿ™ÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿ™ÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿÌÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿ™ÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿÿÿÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿÿÿÿfÌÿÿÿÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿ™ÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿ™ÌÿÿÿÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿÌÌÿfÌÿÿÿÿÌÌÿfÌÿfÌÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿÿÿÿfÌÿ™ÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿ™ÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿÿÿÿÿÿÿfÌÿfÌÿfÌÿÿÿÿfÌÿ™ÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿÌÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿÿÿÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÌÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿÿÿÿfÌÿÿÿÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿÌÌÿÿÿÿfÌÿÌÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿÿÿÿ™ÌÿfÌÿfÌÿÿÿÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿÌÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿÿÿÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿ™ÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿ™ÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿÿÿÿÿÿÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿ™ÌÿÿÿÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿÿÿÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿfÌÿfÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿÿÿÿfÌÿfÌÿ™ÌÿÿÿÿfÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿÌÌÿfÌÿÿÿÿÌÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿÿÿÿfÌÿÿÿÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÌÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿÿÿÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿfÌÿfÌÿÿÿÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿÿÿÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿÿÿÿ™ÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿÿÿÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿ™ÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿ™ÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿÿÿÿÿÿÿfÌÿfÌÿfÌÿÿÿÿfÌÿ™ÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÿÿÿfÌÿfÌÿÿÿÿfÌÿfÌÿÌÌÿfÌÿÿÿÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿÌÌÿfÌÿfÌÿfÌÿfÌÿfÌÿfÌÿ \ No newline at end of file
diff --git a/bubbob/images/water_still.ppm b/bubbob/images/water_still.ppm
new file mode 100644
index 0000000..ec040f0
--- /dev/null
+++ b/bubbob/images/water_still.ppm
@@ -0,0 +1,4 @@
+P6
+16 16
+255
+3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ™Ìÿ3™ÿ3™ÿ™Ìÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿÌÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ™Ìÿ3™ÿ3™ÿ3™ÿ™Ìÿ3™ÿÌÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿÌÌÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿÌÌÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿÌÌÿ3™ÿ™Ìÿ3™ÿ3™ÿ™Ìÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ™Ìÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿÌÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿÌÌÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ™Ìÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ™Ìÿ3™ÿ™Ìÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿÌÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ™Ìÿ3™ÿ™Ìÿ3™ÿ \ No newline at end of file
diff --git a/bubbob/images/water_surface.ppm b/bubbob/images/water_surface.ppm
new file mode 100644
index 0000000..fcbbc1a
--- /dev/null
+++ b/bubbob/images/water_surface.ppm
@@ -0,0 +1,4 @@
+P6
+16 64
+255
+ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ™Ìÿ3™ÿ3™ÿ3™ÿ3™ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿÿÿÿÿÿÿ3™ÿ3™ÿ™Ìÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ™Ìÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ™Ìÿ3™ÿ™Ìÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ™Ìÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ™Ìÿ3™ÿ™Ìÿ3™ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿÿÿÿ3™ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ3™ÿ™Ìÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿÿÿÿÿÿÿÿÿÿÿÿÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ™Ìÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ™Ìÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ™Ìÿ3™ÿ™Ìÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ™Ìÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ™Ìÿ3™ÿ™Ìÿ3™ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ3™ÿ3™ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ™Ìÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ™Ìÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ™Ìÿ3™ÿ™Ìÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ™Ìÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ™Ìÿ3™ÿ™Ìÿ3™ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ™Ìÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿÿÿÿÿÿÿÿÿÿÿÿÿ3™ÿ3™ÿ3™ÿ3™ÿ™Ìÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ™Ìÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ™Ìÿ3™ÿ™Ìÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ™Ìÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿfÌÿ3™ÿ3™ÿ3™ÿ™Ìÿ3™ÿ™Ìÿ3™ÿ \ No newline at end of file
diff --git a/bubbob/images/yellow_Hurry_up.ppm b/bubbob/images/yellow_Hurry_up.ppm
new file mode 100644
index 0000000..52682af
--- /dev/null
+++ b/bubbob/images/yellow_Hurry_up.ppm
Binary files differ
diff --git a/bubbob/levels/Arena.bin b/bubbob/levels/Arena.bin
new file mode 100644
index 0000000..73db756
--- /dev/null
+++ b/bubbob/levels/Arena.bin
Binary files differ
diff --git a/bubbob/levels/CompactLevels.py b/bubbob/levels/CompactLevels.py
new file mode 100644
index 0000000..e48908a
--- /dev/null
+++ b/bubbob/levels/CompactLevels.py
@@ -0,0 +1,1902 @@
+#
+# A series of compact levels.
+#
+
+import boarddef, mnstrmap, random
+from boarddef import LNasty, LMonky, LGhosty, LFlappy
+from boarddef import LSpringy, LOrcy, LGramy, LBlitzy
+from boarddef import RNasty, RMonky, RGhosty, RFlappy
+from boarddef import RSpringy, ROrcy, RGramy, RBlitzy
+
+
+class level01(boarddef.Level):
+ a = LNasty
+ b = RNasty
+
+ walls = """
+#############################
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## a b ##
+#### ############### ####
+## ##
+## ##
+## ##
+## a b ##
+#### ############### ####
+## ##
+## ##
+## ##
+## a b ##
+#### ############### ####
+## ##
+## ##
+## ##
+## ##
+#############################
+"""
+
+
+class level02(boarddef.Level):
+ a = LNasty
+ b = RNasty
+ g = RGhosty
+
+ walls = """
+#############################
+## # ##
+## # ##
+## # ##
+## # g ##
+## g # ##
+## # ##
+## # ##
+#### ############### ####
+## # # ##
+## # # ##
+## # # ##
+## # a b # ##
+#### ############### ####
+## ##
+## ##
+## ##
+## ##
+#### ####
+## ##
+## ##
+## ##
+## ##
+#############################
+"""
+
+
+class level03(boarddef.Level):
+ a = LNasty
+ b = RNasty
+
+ letter = 1
+
+ walls = """
+######### ### #########
+## # # ##
+## # # ##
+## # # ##
+## # # ##
+##b # # # # a ##
+###### ##### ##### ######
+## ##
+## ##
+## ##
+## #b # # # # a # ##
+## ##### ##### ##### ##
+## # # ##
+## # # ##
+## # # ##
+##b # # a ##
+####### # b # #######
+## ####### ##
+## # ##
+## # ##
+## # ##
+## # ##
+## # ##
+######### ### #########
+""" #|# #|# #|# """
+
+
+class level04(boarddef.Level):
+ a = LNasty
+ b = RNasty
+ g = LOrcy
+
+ fire = 1
+
+ walls = """
+######### ### #########
+## # # ##
+## # # ##
+## # # ##
+## # # ##
+##b # # # # a ##
+###### ##### ##### ######
+## ##
+## ##
+## ##
+## #b # #gg # # a # ##
+## ##### ##### ##### ##
+## # # ##
+## # # ##
+## # # ##
+##b # # a ##
+####### # b # #######
+## ####### ##
+## # ##
+## # ##
+## # ##
+## # ##
+## # ##
+######### ### #########
+""" #|# #|# #|# """
+
+class level05(boarddef.Level):
+ a = LNasty
+ b = RNasty
+ g = LOrcy
+ f = RFlappy
+
+ water = top = letter = 1
+
+ walls = """
+## # # ##
+## # # ##
+## # # ##
+## # # ##
+## # # ##
+##b # # # # a ##
+#### # ##### ##### # ####
+## ##
+## ##
+## ##
+## #b # #gg # # a # ##
+## # ### ## ## ### # ##
+## # # ##
+## # # ##
+## # f # ##
+##b # # a ##
+####### # b # #######
+## # ### # ##
+## ##
+## ##
+## ##
+## ##
+## ##
+######### #########
+""" #|# #|# #|# """
+
+ winds = """
+>> <<
+>>xxxxxxxxxxxxxxxxxxxxxxxxx<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+"""
+
+class level06(boarddef.Level):
+ a = LNasty, RNasty, LMonky
+
+ letter = fire = 1
+
+ walls = """
+#############################
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ### ##
+## # ## ##
+## ## # ## # ##
+## # # ## # # ##
+## ## # # # # # ##
+## # #### # # #a ##
+## a #### #############
+## #### ##
+#### ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+#############################
+""" #|# #|# #|# """
+
+class level07(boarddef.Level):
+ g = LGhosty, LGhosty, LGhosty, LGhosty
+ h = RGhosty, RGhosty, RGhosty, RGhosty
+
+ letter = lightning = 1
+
+ walls = """
+## ##
+## ##
+## # # ##
+## # # ##
+## # # ##
+## ### ### ##
+##h # # # # #g ##
+## # # # # # # # ##
+##### #### # #### #####
+## # # # ### # # # ##
+## # # # # # # ##
+## ### # # # ### ##
+## # #### #### # ##
+## # # # # # ##
+## # # # # ##
+## ### ##
+## # ##
+## # # # ##
+## # # # ##
+## # # ##
+## ### ### ##
+## # # # # ##
+## # # # # # # ##
+## #### #### #### #### ##
+""" #|# #|# #|# """
+
+class level08(boarddef.Level):
+ s = LSpringy
+ r = RSpringy
+ f = LFlappy
+ g = RFlappy
+
+ walls = """
+#############################
+## ##
+## g f ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## # ##
+## ##
+## ### ##
+## ##
+## ##### ##
+## ##
+## ####### ##
+## ##
+## ######### ##
+## sssrrr ##
+## ########### ##
+## ##
+## ##
+## ##
+## ##
+#############################
+""" #|# #|# #|# """
+
+class level09(boarddef.Level):
+ m = (LMonky, RMonky,)*5
+ g = LGhosty
+
+ walls = """
+####### #### #### #######
+####### #### #### #######
+## ## ## ##
+## ## ## ##
+## g ## ## g ##
+## ## ## ##
+## ## ## ##
+#### ##### ##### ####
+#### ##### ##### ####
+## ##
+## ## #### #### ## ##
+## ## #### #### ## ##
+## ## ## ##
+#### ## ## ####
+#### ## m ## ####
+## ## ### ## ##
+## ## ## ### ## ## ##
+## ## ## ### ## ## ##
+## ## ### ## ##
+#### ## ## ####
+#### ## ## ####
+## ## ## ##
+## ## #### #### ## ##
+####### #### #### #######
+""" #|# #|# #|# """
+
+class level10(boarddef.Level):
+ n = LNasty
+
+ fire = top = 1
+
+ walls = """
+## ##### ## ##
+## ##nn ## ##
+## ####### ##
+## ##
+## ##
+## ## ##### ##
+## ##nn ## ##
+## ####### ##### ## ##
+## ##nn ## ##
+## ####### ##
+## ##
+## ##
+## ## ##### ##
+## ##nn ## ##
+## ####### ##
+## ##
+## ##### ## ##
+## ##nn ## ##
+## ####### ##
+## ##
+## ##
+## ##
+## ##
+#############################
+""" #|# #|# #|# """
+
+ winds = """
+>> <<
+>>> >>v xxx <<<
+>>^ ^<<
+>>^ v<<<^<<
+>>^ ^<<
+>>^ ^<<
+>>^ xxx v<< ^<<
+>>^ ^<<
+>>^>>>v >>v xxx ^<<
+>>^ ^<<
+>>^ >v ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ xxx v<< ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ xxx ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+"""
+
+class level11(boarddef.Level):
+ o = LOrcy
+ p = ROrcy
+
+ letter = fire = water = lightning = top = 1
+
+ walls = """
+## ##
+## ##
+### ###
+## # # ##
+## # # ##
+## # # ##
+## # # # # ##
+## # ##
+## o p o # # #p o p ##
+####### #### # #### #######
+## # ##
+## # ##
+## # ##
+## # # # ##
+## # # # ##
+## # # # ##
+## # # # ##
+## # # # ##
+## # # # ##
+## # # # ##
+## # # # ##
+## # # # # # # ##
+## # # ##
+###### ###### ###### ######
+""" #|# #|# #|# """
+
+class level12(boarddef.Level):
+ o = LGramy
+
+ walls = """
+## #### ##
+## # # ##
+## #### ##
+## # # ##
+## # o ##
+## #################
+## # # ##
+## #### ##
+## o # # # ##
+## ######### #### ##
+## # # # # ##
+## #### # o ##
+## # # ############
+## #### # # ##
+## # # #### ##
+## o # # # ##
+############ #### ##
+## # # # # ##
+## #### o # ##
+## # # ######## ##
+## #### ##
+## # # ##
+## ##
+#############################
+""" #|# #|# #|# """
+
+class level13(boarddef.Level):
+ n = LNasty
+ m = LMonky
+ g = LGhosty
+ f = LFlappy
+ s = LSpringy
+ o = LOrcy
+ r = LGramy
+ b = LBlitzy
+
+ walls = """
+## # #### ##
+## f ## ## #### ##
+## #### ## ## ##
+## #### # ##
+## #### f ###### ##
+## ## ## f ###### ##
+## # #### #### #### ##
+## ###### ###### ##
+## ###### ###### ##
+## #### #### # ##
+## ## ## f ##
+## #### ##
+## f ##
+## #### ##
+## ###### ##
+## #### ###### ##
+## ## ## # #### ##
+## # #### f ## ## ##
+## ###### #### ##
+## ###### ##
+## #### ##
+## ##
+## #### ##
+###### ###### #####
+""" #|# #|# #|# """
+
+class level14(boarddef.Level):
+ o = LOrcy
+ r = LGramy
+
+ walls = """
+#############################
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ### ### ##
+## ## ## ##
+## # # ##
+## ### ### ##
+## ## ## ##
+## # # ##
+## ## ## ##
+## ## ## ##
+## ### ### ##
+## ## ## ##
+## ## ## ##
+## ##or or ## ##
+#############################
+""" #|# #|# #|# """
+
+class level15(boarddef.Level):
+ s = LSpringy
+ g = LGhosty
+
+ walls = """
+#############################
+## ##
+## ##
+## ##
+## s s ##
+## s s ##
+## s s ##
+## ###s #####s ### ##
+## # ## ####### ## # ##
+## # ### ### # ##
+## ## ### ### ## ##
+## # ### g ### # ##
+## # ### ### # ##
+## # ### ### # ##
+## # ## ####### ## # ##
+## ### ##### ### ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+#############################
+""" #|# #|# #|# """
+
+class level16(boarddef.Level):
+ l = LBlitzy, LGramy
+ r = RBlitzy, RGramy
+
+ letter = 1
+
+ walls = """
+#############################
+## l r ##
+## #### ## ##
+## #### ## #### ## ##
+## #### ## #### ## ##
+## l ## ## #### ## ##
+## #### ## ## l ##
+## #### ## ## #### ##
+## ## ## ## #### ##
+## #### ## ## ##
+## #### ## ## #### ## ##
+## ## #### ## ##
+## ## #### ## ##
+## ## #### #### l ## ## ##
+## ## #### ## ## ##
+## ## ## ## ##
+## ## ## r ## ##
+## #### ## ## ## ##
+## #### ## #### ## ## ##
+## ## #### ## ## ##
+## #### ## ## ##
+## #### ## ##
+## ##
+#############################
+""" #|# #|# #|# """
+
+class level17(boarddef.Level):
+ m = LMonky
+ j = RMonky
+ g = LGhosty
+ h = RGhosty
+ b = LBlitzy
+
+ walls = """
+############ ##
+## # ##
+## g # ##
+## ##### # ########### ##
+## # # # # # ##
+## # ## # # g # ##
+## # #j # # ##### # ##
+## # ##### # # # # ##
+## # # # ## # ##
+## #b # # #j # ##
+## ########### # ##### ##
+## # ##
+## # ##
+## ########### # h ##
+## # # # ##### ##
+## # h # # # # ##
+## # ##### # # ## # ##
+## # # # # # m # # ##
+## # ## # # ##### # ##
+## # m # # # # ##
+## ##### # # b # ##
+## # ########### ##
+## b # ##
+############ ##
+""" #|# #|# #|# """
+
+class level18(boarddef.Level):
+ o = (ROrcy,)*10
+
+ walls = """
+############# #############
+## ##
+## # ##
+## ##
+## # # ##
+## ##
+## ##
+## # # ##
+## ##
+## # ####### # ##
+## # # ##
+## ## ## ##
+## # ### ### # ##
+## ## ## ##
+## # # ##
+## # ####### # ##
+## # # ##
+## ## ## ##
+## ###### ###### ##
+## ### ### ##
+## ## o ## ##
+## ## ####### ## ##
+## ## ## ##
+############# #############
+""" #|# #|# #|# """
+
+class level19(boarddef.Level):
+ n = LNasty
+ g = RGhosty
+ f = LFlappy
+ s = LSpringy
+
+ walls = """
+## ### ### ### ##
+## # # # # # # ##
+## # #### #### # ##
+## #n # #n # #n # ##
+## ### ### ### ##
+## # # ##
+## # # ##
+## ### f ### ##
+## # # g # # ##
+## # # f # # ##
+## #n # g #n # ##
+## ### ### ##
+## # # ##
+## # # ##
+## ### ### ### ##
+## # #s # #s # # ##
+## # #### #### # ##
+## #n # #n # #n # ##
+## ### ### ### ##
+## ##
+## ##
+## ##
+## ##
+#############################
+""" #|# #|# #|# """
+
+class level20(boarddef.Level):
+ s = LSpringy, RSpringy
+
+ letter = fire = top = 1
+
+ walls = """
+### ###
+### ###
+## ##
+## ##
+##ss ss # # ss ss ##
+## # # # # # # ##
+## # # ##### # # ##
+## # # # # ##
+## # # # # ##
+## # # ##
+## # # ##
+## # # ##
+## ##
+## ##
+## ##
+## ##
+## # # ##
+## # # ##
+## # # ##
+## ##
+## ##
+## ##
+## ##
+## ######### ######### ##
+""" #|# #|# #|# """
+
+class level21(boarddef.Level):
+ n = (RNasty,)*12
+
+ letter = 1
+
+ walls = """
+#############################
+## ##
+##n ##
+######################### ##
+## ##
+## ##
+## #########################
+## ##
+## ##
+######################### ##
+## ##
+## ##
+## #########################
+## ##
+## ##
+######################### ##
+## ##
+## ##
+## #########################
+## ##
+## ##
+## ##
+## ##
+#############################
+""" #|# #|# #|# """
+
+class level22(boarddef.Level):
+ n = LNasty
+ m = LMonky
+ g = LGhosty
+ f = LFlappy
+ s = LSpringy
+ o = LOrcy
+ r = LGramy
+ b = LBlitzy
+
+ walls = """
+## ### ### ### ##
+## # # # # # # ##
+## # #### #### # ##
+## #o # #b # #r # ##
+## ### ### ### ##
+## # # ##
+## # # ##
+## ### ### ##
+## # # # # ##
+## # # # # ##
+## #g # #f # ##
+## ### ### ##
+## # # ##
+## # # ##
+## ### ### ### ##
+## # # # # # # ##
+## # #### #### # ##
+## #m # #s # #n # ##
+## ### ### ### ##
+## ##
+## ##
+## ##
+## ##
+#############################
+""" #|# #|# #|# """
+
+class level23(boarddef.Level):
+ m = LMonky
+
+ water = top = 1
+
+ walls = """
+###### # # ######
+###### # # ######
+## # m # ##
+## ######## ##### ######## ##
+## ######## ##### ######## ##
+## ######## ##### m ## ##
+## ######## ########### ## ##
+## m ######## ## ##
+## ########### ######## ## ##
+## ########### m ## ##
+## ################ ###### ##
+## ################ ###### ##
+## ######## m ##
+## ######## ############## ##
+## ######## ############## ##
+## ########m ########### ##
+## m ##### ##### m ##
+######## ##### ##### ########
+######## m m ########
+############## ##############
+#############################
+## ##
+## ##
+###### ######### ######
+""" #|# #|# #|# """
+
+class level24(boarddef.Level):
+ g = RGhosty
+ f = RFlappy
+ s = LSpringy
+ t = RSpringy
+
+ walls = """
+#############################
+## ##
+## ##
+## s t ##
+## s t ##
+## s t ##
+###### ######
+## ##
+## ##### ##### ##### ##
+## # # # ##
+## #g # #g ##
+## # #f # ##
+## # # # ##
+## ##
+## ##
+## ##### ##### ##### ##
+## # # # ##
+## # #g # ##
+## #f # #f ##
+## # # # ##
+## ##
+## ##
+## ##
+#############################
+""" #|# #|# #|# """
+
+class level25(boarddef.Level):
+ s = LSpringy
+ t = RSpringy
+
+ letter = lightning = 1
+
+ walls = """
+############# #############
+## ##
+## # ##
+## # ##
+## s t ##
+## # # # # ##
+## # # # # ##
+## ##
+## # ##
+## # ##
+## t s ##
+## # # # # ##
+## # # # # ##
+## ##
+## # ##
+## # ##
+## s t ##
+## # # # # ##
+## # # # # ##
+## ##
+## # ##
+## # ##
+## ##
+############# #############
+""" #|# #|# #|# """
+
+class level26(boarddef.Level):
+ s = LSpringy
+
+ fire = 1
+
+ walls = """
+####### #######
+## ##
+## ##
+##s s ##
+##s ####### ####### s ##
+##s s ##
+#### ####
+## ##
+## ## ## ##
+## ##
+## ## ## ##
+## ##
+## ### ##
+## ##
+##s s ##
+#### ## ## ####
+## ##
+## ##
+## ##
+## ##
+## s s ##
+## ## ## ##
+## ##
+#############################
+""" #|# #|# #|# """
+
+class level27(boarddef.Level):
+ s = LSpringy
+
+ fire = 1
+
+ walls = """
+## ##
+## ##
+## # s # ##
+##s # # s # # s ##
+## # #s # # ##
+## # # # # ##
+### # ###
+## ### ##
+### # ###
+## # # # # # ##
+## #s # # #s # ##
+## # # # # ##
+## # # ##
+## ### ### ##
+## # # ##
+## # # ##
+## s #s # s ##
+## # # # # # # ##
+## # # # # # ##
+## # ### # ##
+## ### # ### ##
+## # # ##
+## ##
+#############################
+""" #|# #|# #|# """
+
+class level28(boarddef.Level):
+ f = LFlappy
+
+ walls = """
+#############################
+######### ###########
+## ## f #### ##
+##f ## ff #### f ##
+##f f ## ### ####f f ##
+## f ## ### #### f ##
+##### ## ### ###### ##
+##### ## ### ###### ##
+##### ## #### ###### ##
+##### ## #### ###### ##
+## #### ##
+## ##
+## ##
+## ## ######## ### ##
+## ## ######## ### ##
+## ## ######## ### ##
+## ## ### ### ##
+## ## ### ### ##
+## ## ### ### ##
+## ##### ### ##### ##
+## ##### ### ##### ##
+## ### ##
+## ### ##
+#############################
+""" #|# #|# #|# """
+
+class level29(boarddef.Level):
+ f = LFlappy, RFlappy
+
+ top = water = 1
+
+ walls = """
+## ##################### ##
+## ##
+## f f ##
+## ##################### ##
+## ##################### ##
+## ##
+## f f ##
+## ##################### ##
+## ##################### ##
+## ##
+## f f ##
+## ##################### ##
+## ##################### ##
+## ##
+## f f ##
+## ##################### ##
+## ##################### ##
+## ##
+## f f ##
+## ##################### ##
+## ##################### ##
+## ##
+## f f ##
+## ##################### ##
+""" #|# #|# #|# """
+
+class level30(boarddef.Level):
+ g = RGhosty
+ h = RGhosty
+ i = LGhosty
+ j = LGhosty
+
+ walls = """
+#############################
+## ##
+## ##
+## ##
+## ####g # ##
+## ###### ##
+## j ###### ##
+## ####### ##
+## ###### ##
+## ######h ##
+## # #### ##
+## i ##
+## ##
+## ##
+## ####g # ####g # ##
+## ###### ###### ##
+## j ###### j ###### ##
+## ####### ####### ##
+## ###### ###### ##
+## ######h ######h ##
+## # #### # #### ##
+## i i ##
+## ##
+#############################
+""" #|# #|# #|# """
+
+class level31(boarddef.Level):
+ o = LGramy, RGramy
+ r = LOrcy
+ s = ROrcy
+
+ top = letter = lightning = fire = 1
+
+ walls = """
+## ######## ######## ##
+## ##
+## ##
+## r s ##
+## ######## ######## ##
+##o o ##
+###### ######
+## ##
+##o s # #r o ##
+############# #############
+## ##
+## ##
+## ##################### ##
+## ##
+## ##
+## ####### ####### ##
+## ##
+##o o ##
+########## # # ##########
+## # # ##
+## ## ## ##
+## ##
+## ##
+############# #############
+""" #|# #|# #|# """
+
+class level32(boarddef.Level):
+ n = LNasty, RNasty
+ f = (LFlappy, RFlappy) * 2
+
+ walls = """
+############# #############
+## ## ## ##
+## f ## ##f ##
+## ## ## ##
+## ## ## ##
+## ## ## ##
+## ## ## ##
+## ## ## ##
+##n ## ## n ##
+#############################
+#############################
+## ##
+## f ##
+##n n ##
+#############################
+#############################
+## ## ## ##
+## ## ## ##
+## ## ## ##
+## ## ## ##
+## ## ## ##
+## ## ## ##
+## ## ## ##
+############# #############
+""" #|# #|# #|# """
+
+ winds = """
+>> ^ <<
+>>>>>>>>>>>>> ^ <<<<<<<<<<<<<
+>>^ ^ ^<<
+>>^ ^ ^<<
+>>^ ^ ^<<
+>>^ ^ ^<<
+>>^ ^ ^<<
+>>^ ^ ^<<
+>>^ ^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ xxx ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+"""
+
+class level33(boarddef.Level):
+ monsters = [mnstrmap.Gramy(14, -2, 1),
+ mnstrmap.Gramy(13, -2, 0)] * 10
+ top = 1
+
+ walls = """
+## ## ## ##
+## ## ## ##
+## ## ## ##
+## ####### ####### ##
+## # # ##
+## # # ##
+## # ########### # ##
+## # # ##
+## # # ##
+## ####### ####### ##
+## # # ##
+## # # ##
+## # ########### # ##
+## # # ##
+## # # ##
+## ####### ####### ##
+## # # ##
+## # # ##
+## # ########### # ##
+## # # ##
+## # # ##
+## ####### ####### ##
+## ## ## ##
+############# #############
+""" #|# #|# #|# """
+
+ winds = """
+>> <<
+>>>xxxxxxxxxxxxxxxxxxxxxxx<<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+"""
+
+class level34(boarddef.Level):
+ n = LNasty
+ N = RNasty
+ m = LMonky
+ M = RMonky
+ g = LGhosty
+ G = RGhosty
+ o = LOrcy
+ O = ROrcy
+ r = LGramy
+ R = RGramy
+ b = LBlitzy, RBlitzy
+
+ top = water = letter = 1
+
+ walls = """
+##### # ############# # #####
+## # # # ##
+## # # # ##
+## # # # ##
+## r #R ##
+############# # #############
+## # ##
+##G # # # g ##
+## # # # ##
+## # # # ##
+## #O b o # ##
+##### # ############# # #####
+## # # ##
+## # # # ##
+## # # # ##
+## # # # ##
+##M # m ##
+############# # #############
+## # ##
+## # # # ##
+## # # # ##
+## # # # ##
+## # n #N # ##
+##### # ############# # #####
+""" #|# #|# #|# """
+
+class level35(boarddef.Level):
+ g = (RGhosty,)*3
+ h = (LGhosty,)*3
+
+ walls = """
+############# #############
+## h ##
+#############################
+##g ##
+#############################
+## h ##
+############# #############
+##g ##
+#############################
+## h ##
+#############################
+##g ##
+############# #############
+## h ##
+#############################
+##g ##
+#############################
+## h ##
+############# #############
+##g ##
+#############################
+## ##
+## ##
+############# #############
+""" #|# #|# #|# """
+
+class level36(boarddef.Level):
+ f = LFlappy
+ r = RSpringy
+
+ top = fire = letter = 1
+
+ walls = """
+## # # # # # # ##
+## f # # r # # f ##
+## # # #f r # # # ##
+## # # r # # ##
+## f # # # f # # # f ##
+## # # # # ##
+## f # # # f # # # f ##
+## # # f # # ##
+## # # # # # # ##
+## f # # f # # f ##
+## # # # f # # # ##
+## # # f # # ##
+## # # # # # # ##
+## # # f f # # ##
+## # # # # # # ##
+## # # # # ##
+## # # # f # # # ##
+## # # # # ##
+## # # # f # # # ##
+## # # f # # ##
+## # # # f # # # ##
+## # # # # ##
+## # # # f # # # ##
+#############################
+""" #|# #|# #|# """
+
+class level37(boarddef.Level):
+ n = LNasty
+ m = RMonky
+ o = (RFlappy,) * 5
+ s = LSpringy
+ t = RSpringy
+
+ walls = """
+####### ######### #######
+## ##
+## ##
+## ##
+## #s t s t # ##
+## ##################### ##
+## # # ##
+## # # ##
+## # o # ##
+## # # ##
+## # # ##
+## # #n m # # ##
+## # ####### # ##
+## # # # ##
+## # # # ##
+## # # # ##
+## # # # ##
+## # # # ##
+## ##################### ##
+## # # # ##
+## # ##
+## # ##
+## # ##
+####### ######### #######
+""" #|# #|# #|# """
+
+class level38(boarddef.Level):
+ f = LFlappy
+ b = (LBlitzy, RBlitzy) * 2
+
+ water = 1
+
+ walls = """
+############# #############
+## ##
+## ##
+## ##
+## b b b ##
+## ######### ######### ##
+## ######### ######### ##
+## ######### ######### ##
+## ### ### ##
+## ### f f f f ### ##
+## ### ### ##
+## ### ### ### ### ##
+## ### ### ### ### ##
+## ### ### ### ### ##
+## ### ### ### ### ##
+## ### ### ##
+## ### f f f f ### ##
+## ######### ######### ##
+## ######### ######### ##
+## ######### ######### ##
+## ##
+## ##
+## ##
+############# #############
+""" #|# #|# #|# """
+
+class level39(boarddef.Level):
+ monsters = []
+ mnstrclasses = ([mnstrmap.Nasty] * 10 +
+ [None] * 30 +
+ [mnstrmap.Monky] * 7 +
+ [None] * 30 +
+ [mnstrmap.Orcy] * 5)
+ for i in range(len(mnstrclasses)):
+ if mnstrclasses[i]:
+ left = random.randrange(2)
+ x = random.choice([7,14,21])
+ monsters.append(mnstrclasses[i](x-left, -2*i, left))
+
+ walls = """
+###### #### #### ######
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+###### ######### ######
+## ##
+## ##
+## ##
+## ##
+#############################
+""" #|# #|# #|# """
+
+class level40(boarddef.Level):
+ g = LGhosty
+
+ top = fire = 1
+
+ walls = """
+## ##
+## ##
+## # ## ##
+## ## ### # ##
+## # ## ##
+## ##
+## ##
+## # ##
+## # #### ##
+## ### ##
+## # # g ##
+## # ##
+## ### # ##
+## # # ### ##
+## ### g # ##
+## # ##
+## g ##
+## # ## ##
+## # # ### ##
+## ### # ##
+## # g ##
+## g # # ##
+## ## # ## ##
+## # ### ## ## ##
+""" #|# #|# #|# """
+
+ winds = """
+>>vvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>>>>>>>>>>>vvxvv<<<<<<<<<<<<
+>>>>>>>>>>>>vvxvv<<<<<<<<<<<<
+>>>>>>>>>>>>vxxxv<<<<<<<<<<<<
+>>>>>>>>>>>>vxxxv<<<<<<<<<<<<
+>>>>>>>>>>>>vxxxv<<<<<<<<<<<<
+>>>>>>>>>>>>xxxxx<<<<<<<<<<<<
+>>>>>>>>>>>>xxxxx<<<<<<<<<<<<
+>>>>>>>>>>>>xxxxx<<<<<<<<<<<<
+>>>>>>>>>>>>xxxxx<<<<<<<<<<<<
+>>>>>>>>>>>>xxxxx<<<<<<<<<<<<
+>>>>>>>>>>>>xxxxx<<<<<<<<<<<<
+>>>>>>>>>>>>xxxxx<<<<<<<<<<<<
+>>>>>>>>>>>>xxxxx<<<<<<<<<<<<
+>>>>>>>>>>>>xxxxx<<<<<<<<<<<<
+>>>>>>>>>>>>xxxxx<<<<<<<<<<<<
+>>>>>>>>>>>>xxxxx<<<<<<<<<<<<
+>>>>>>>>>>>>xxxxx<<<<<<<<<<<<
+>>>>>>>>>>>>xxxxx<<<<<<<<<<<<
+>>>>>>>>>>>>xxxxx<<<<<<<<<<<<
+>> <<
+"""
+
+class level41(boarddef.Level):
+ f = RFlappy
+ j = LFlappy
+ b = RBlitzy
+ d = LBlitzy
+
+ top = 1
+
+ walls = """
+############# #############
+##b d ##
+##### db #####
+## ############# ##
+## ## ## ##
+### ### jfjfjfjf ### ###
+## ## fjfjfjfj ## ##
+#### ##### ##### ####
+## ##
+### ###
+#### ####
+#b d ##
+###### ######
+### ###
+####### #######
+##### #####
+##b d ##
+########## ###########
+##### #####
+#### ####
+####### #######
+## ##
+## ##
+############# #############
+""" #|# #|# #|# """
+
+ winds = """
+>> <<
+>>v<<<<<<<<<<<<>>>>>>>>>>>v<<
+>>v>>>>>>>>vvvvvvv<<<<<<<<v<<
+>>v>>>>>>>>vvvvvvv<<<<<<<<v<<
+>>v>>>>>>>>xxxxxxx<<<<<<<<v<<
+>>v v<<
+>>v v<<
+>>v v<<
+>>v v<<
+>>v v<<
+>>v v<<
+>>v v<<
+>>v v<<
+>>v v<<
+>>v v<<
+>>v v<<
+>>v v<<
+>>v v<<
+>>v v<<
+>>v v<<
+>>v v<<
+>>v v<<
+>>> vvv <<<
+>>> vvv <<<
+"""
+
+class level42(boarddef.Level):
+ l = LOrcy, LOrcy
+ r = ROrcy, ROrcy
+ b = ROrcy, LOrcy, ROrcy, LOrcy
+
+ top = 1
+ water = 1
+ letter = 1
+
+ walls = """
+## # ##
+## l r ##
+## ## ## ##
+## ##
+## #### ### ##
+## # ##
+## # # ##
+## # # r ##
+## # ## ##
+## r # # ##
+## ## # # ##
+## # # ##
+## r # # ##
+## # # ## # ##
+## # # b # ##
+## # ## # ##
+## # r # ##
+## # ## # ##
+## # ##
+## # # ##
+## b ##
+## ## # ##
+## # ##
+## ### ## # ##
+""" #|# #|# #|# """
+
+ winds = """
+>>>vvvvvvvvvvvvvvvvvvvvvvv<<<
+>>>vvvvvvvvvvvvvvvvvvvvvvv<<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+"""
+
+class level43(boarddef.Level):
+ s = LSpringy, RSpringy, LSpringy, RSpringy
+ f = LFlappy
+ g = RFlappy
+
+ walls = """
+## # # ##
+## #g f # ##
+## ##
+## # # ##
+## # # ##
+##s # # # # s ##
+## # # # # ##
+## # # # ##
+## # ##
+## # s #s # ##
+## # # # # # ##
+## # # # # # # # # # ##
+## # # # # # # # # # # # ##
+## # # # # # # # # # # # ##
+## # # # # # # # # ##
+## # # # # # # ##
+## # # # # # # ##
+## # # ##
+## # # ##
+## # # ##
+## # # # # ##
+## # # # # ##
+## # # # # # # # ##
+## # # # # # # # # # # # # ##
+""" #|# #|# #|# """
+
+class level44(boarddef.Level):
+ o = LGramy
+ p = RGramy
+ g = RFlappy
+ h = LFlappy
+
+ top = 1
+
+ walls = """
+## ##
+## ##
+## ##
+######## ## ##
+## h ## ## ##
+## g ## ## ###### ##
+## h ## ## ## ## ##
+## g ## ## ## ## ##
+## h ## ## ## ## ##
+## g ## ## ## ## ##
+## ## ## ## ## ##
+## ## ## ## ## h ##
+## ## ## ## ## g ##
+## ## ## ## ## h ##
+## ## ## ## ## g ##
+## ###### ## ## h ##
+## ## ## g ##
+## ## ########
+## ##
+## ##
+## ##
+## ##
+## p o p o p o ##
+#############################
+""" #|# #|# #|# """
+
+ winds = """
+>>>vvvvvvvvvvvvvvvvvvvvvvv<<<
+>>>>>>>>vv<<<<<vv<<<>>>vvv<<<
+>>^ vv ^<<
+>>^ vv ^<<
+>>^ vv ^<<
+>>^ vv ^<<
+>>^ vv >< ^<<
+>>^ vv ^^ ^<<
+>>^ vv ^^ ^<<
+>>^ vv ^^ ^<<
+>>^ vv ^^ ^<<
+>>^ vv ^^ ^<<
+>>^ vv ^^ ^<<
+>>^ vv ^^ ^<<
+>>^ >< ^^ ^<<
+>>^ ^<<<>>>^ ^^ ^<<
+>>^ ^^ ^<<
+>>^ ^^ ^<<
+>>^ >>>>>^^<<<<<^<<
+>>^ ^<<
+>>^ ^<<
+>>^vvvvvvvvvvvvvvvvvvvvvvv^<<
+>>^vvvvvvvvvvvvvvvvvvvvvvv^<<
+>>^vvvvvvvvvvvvvvvvvvvvvvv^<<
+"""
+
+class level45(boarddef.Level):
+ b = LBlitzy, RBlitzy
+
+ top = 1
+ water = 1
+
+ walls = """
+## ##
+## ## ## ##
+## ## ## # # ## ## ##
+## # # # # # # ##
+## # # #bb # # # ##
+## #bb # ## ## #bb # ##
+## ## ## ## ## ##
+## ## ## ##
+## ## ## # # ## ## ##
+## # # # # # # ##
+## # # # # # # ##
+## #bb # # # #bb # ##
+## ## ## #bbbb # ## ## ##
+## ## ## ##
+## ## ## ## ## ##
+## # # ## ## # # ##
+## # # # # # # ##
+## #bb # # # #bb # ##
+## ## ## #bb # ## ## ##
+## ## ## ##
+## ##
+## ##
+## ##
+### ##################### ###
+""" #|# #|# #|# """
+
+ winds = """
+>>>vvvvvvvvvvvvvvvvvvvvvvv<<<
+>>>>>>>>>>>>>xxx<<<<<<<<<<<<<
+>>xxxxxxxxxxxxxxxxxxxxxxxxx<<
+>>xxxxxxxxxxxxxxxxxxxxxxxxx<<
+>>xxxxxxxxxxxxxxxxxxxxxxxxx<<
+>>xxxxxxxxxxxxxxxxxxxxxxxxx<<
+>>xxxxxxxxxxxxxxxxxxxxxxxxx<<
+>>xxxxxxxxxxxxxxxxxxxxxxxxx<<
+>>xxxxxxxxxxxxxxxxxxxxxxxxx<<
+>>xxxxxxxxxxxxxxxxxxxxxxxxx<<
+>>xxxxxxxxxxxxxxxxxxxxxxxxx<<
+>>xxxxxxxxxxxxxxxxxxxxxxxxx<<
+>>xxxxxxxxxxxxxxxxxxxxxxxxx<<
+>>xxxxxxxxxxxxxxxxxxxxxxxxx<<
+>>xxxxxxxxxxxxxxxxxxxxxxxxx<<
+>>xxxxxxxxxxxxxxxxxxxxxxxxx<<
+>>xxxxxxxxxxxxxxxxxxxxxxxxx<<
+>>xxxxxxxxxxxxxxxxxxxxxxxxx<<
+>>xxxxxxxxxxxxxxxxxxxxxxxxx<<
+>>xxxxxxxxxxxxxxxxxxxxxxxxx<<
+>>xxxxxxxxxxxxxxxxxxxxxxxxx<<
+>>xxxxxxxxxxxxxxxxxxxxxxxxx<<
+>>xxxxxxxxxxxxxxxxxxxxxxxxx<<
+>>xxxxxxxxxxxxxxxxxxxxxxxxx<<
+"""
+
+class level46(boarddef.Level):
+ m = LMonky, RGramy
+ n = LGramy, RMonky
+ g = [RGhosty] * 4
+ h = [LGhosty] * 4
+
+ letter = 1
+ fire = 1
+
+ walls = """
+############# #############
+## ##
+## ##
+## ##
+############# #############
+## ## ## ##
+## g ## ## h ##
+## ## mnmnm ## ##
+## ########### ##
+## ##
+## ##
+## ##
+######## ### ########
+## ## # ## ##
+## ## # ## ##
+##mnm ## # ##mnm ##
+######## ##### ########
+## # ##
+## # ##
+## ####### ##
+## ##
+## ##
+## # # # ##
+############ ############
+""" #|# #|# #|# """
+
+ winds = """
+>>> ^ <<<
+>>>>>>>>>>>>>>^<<<<<<<<<<<<<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ x ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+"""
+
+class level47(boarddef.Level):
+ l = LOrcy
+ r = ROrcy
+ n = LNasty
+ m = RNasty
+
+ top = 1
+ lightning = 1
+
+ walls = """
+## ################### ##
+## ################### ##
+## # ##
+## # ##
+## ##
+## ##
+## ##
+###### #### #### ######
+## # # ##
+## # # ##
+## # # ##
+## ##### ####### ##### ##
+## ##
+## ##
+## ##
+##### ###### ###### #####
+## # # ##
+## # # ##
+##rmr # lr # lnl ##
+######### ####### #########
+## # # # ##
+## # # # ##
+## #rmrmrmr #lnlnlnl # ##
+## ################### ##
+""" #|# #|# #|# """
+
+ winds = """
+>>vvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>>>>>>vvvvvvvvvvvvvvv<<<<<<<
+>>>>>>>>^^^^^^^^^^^^^<<<<<<<<
+>>>>>>>>^^^^^^^^^^^^^<<<<<<<<
+>>>>>>>>^^^^^^^^^^^^^<<<<<<<<
+>>^ ^<<
+"""
+
+class levelFinal(boarddef.Level):
+
+ walls = """
+############# #############
+## ##
+## ##
+## ##
+## ##
+## ##
+#### ################# ####
+## ### ### ##
+## ## ## ##
+####### ##### #######
+## ##
+## ##
+## ##
+## ##
+######### #########
+## ##
+## ##
+## ##
+########## ##########
+## ##
+## ##
+## ##
+## ##
+############# #############
+""" #|# #|# #|# """
+# nb.: the previous line has no purpose
+# other than helping with wall alignment
diff --git a/bubbob/levels/HouseOfFun.bin b/bubbob/levels/HouseOfFun.bin
new file mode 100644
index 0000000..838344f
--- /dev/null
+++ b/bubbob/levels/HouseOfFun.bin
Binary files differ
diff --git a/bubbob/levels/Levels.bin b/bubbob/levels/Levels.bin
new file mode 100644
index 0000000..fd176d1
--- /dev/null
+++ b/bubbob/levels/Levels.bin
Binary files differ
diff --git a/bubbob/levels/LostLevels.bin b/bubbob/levels/LostLevels.bin
new file mode 100644
index 0000000..a09e365
--- /dev/null
+++ b/bubbob/levels/LostLevels.bin
Binary files differ
diff --git a/bubbob/levels/README.txt b/bubbob/levels/README.txt
new file mode 100644
index 0000000..544b571
--- /dev/null
+++ b/bubbob/levels/README.txt
@@ -0,0 +1,23 @@
+===============================
+How to make your own level sets
+===============================
+
+The .bin files are MacBinary files from the original MacOS9 game. The
+.py files are the levels that we did ourselves. To make new levels, you
+need to make a new .py file in the current directory (bubbob/levels/).
+
+For an example, open CompactLevels.py with any text editor. The
+structure should be fairly simple to understand even without knowledge
+about Python. (Just don't try to look at RandomLevels.py first: it is a
+full Python program that generates the levels completely randomly.)
+
+To get started, copy CompactLevels.py to a new name, like MyLevels.py,
+and start editing it. You can remove or change levels, but the last one
+should always be called LevelFinal and have no monster in it. All other
+levels should have monsters, otherwise they'll be considered as the
+final level.
+
+Also note that all levels need to have the same size (width and height),
+but different level sets can have different sizes. For example, the
+levels in CompactLevels.py are a bit smaller than the ones in the .bin
+files, and the levels in scratch.py are a bit larger.
diff --git a/bubbob/levels/RandomLevels.py b/bubbob/levels/RandomLevels.py
new file mode 100644
index 0000000..5c0f77d
--- /dev/null
+++ b/bubbob/levels/RandomLevels.py
@@ -0,0 +1,362 @@
+#
+# Second try at automatically generating levels that are
+# a bit more related to each other instead of being completely independent.
+#
+
+from __future__ import generators
+import sys, random, math
+from random import uniform, choice, randrange
+
+
+class Parameter(object):
+ def __init__(self, name, rng):
+ self.name = name
+ self.rng = rng
+ def __get__(self, instance, cls):
+ assert self.name not in instance.__dict__
+ value = self.rng()
+ setattr(instance, self.name, value)
+ return value
+
+class ChoiceParameter(Parameter):
+ def __init__(self, name, list):
+ Parameter.__init__(self, name, lambda : choice(list))
+
+class BoolParameter(Parameter):
+ def __init__(self, name, prob=0.5):
+ Parameter.__init__(self, name, lambda : random.random() < prob)
+
+def flat(mean,var):
+ return randrange(mean-var,mean+var+1)
+
+def dice(n,sides,orig=1):
+ result = 0
+ for i in range(n):
+ result += orig+randrange(sides)
+ return result
+
+def fork(choice1, prob, choice2):
+ if random.random() < prob:
+ return choice1()
+ else:
+ return choice2()
+
+MnstrCategory = {
+ "Nasty": 0,
+ "Monky": 0,
+ "Ghosty": 1,
+ "Flappy": 1,
+ "Springy": 2,
+ "Orcy": 0,
+ "Gramy": 0,
+ "Blitzy": 2}
+MnstrNames = MnstrCategory.keys()
+Bonuses = ['letter', 'fire', 'lightning', 'water', 'top']
+
+def mnstrclslist(name):
+ import boarddef
+ classname1 = 'L' + name
+ classname2 = 'R' + name
+ return [getattr(boarddef, classname1), getattr(boarddef, classname2)]
+
+class Shape:
+ basemnstr = ChoiceParameter('basemnstr', MnstrNames)
+ extramnstr = ChoiceParameter('extramnstr', range(4))
+ samemnstr = BoolParameter('samemnstr')
+ baseshape = ChoiceParameter('baseshape', ' ODBGMPRWZS')
+ rooms = BoolParameter('rooms')
+ holes = BoolParameter('holes')
+ lines = ChoiceParameter('lines', ' -/|')
+ platforms = BoolParameter('platforms')
+ platholes = BoolParameter('platholes')
+ platfull = BoolParameter('platfull')
+ mess = ChoiceParameter('mess', ' ....!')
+ closed = BoolParameter('closed', 0.95)
+ bonuses = ChoiceParameter('bonuses', xrange(3**len(Bonuses)))
+ smooth = ChoiceParameter('smooth', range(4))
+ startplats = BoolParameter('startplats', 0.98)
+ makespace = BoolParameter('makespace', 0.8)
+ straightfall = BoolParameter('straightfall', 0.8)
+ mirrored = BoolParameter('mirrored', 0.4)
+ enlargeholes = BoolParameter('enlargeholes', 0.9)
+
+ all_parameters = [name for name in locals().keys()
+ if not name.startswith('_')]
+
+ def __init__(self, shape=None):
+ if shape:
+ self.__dict__.update(shape.__dict__)
+ self.modified = 0
+
+ def set_gens(self, rooms=0, platforms=0, holes=0, smooth=0, mess=' ', lines=' '):
+ self.rooms = rooms
+ self.platforms = platforms
+ self.holes = holes
+ self.smooth = smooth
+ self.mess = mess
+ self.lines = lines
+
+ def reset(self, attrname=None):
+ if attrname:
+ try:
+ del self.__dict__[attrname]
+ except KeyError:
+ pass
+ else:
+ self.__dict__.clear()
+ self.modified = 1
+
+ def __eq__(self, other):
+ return (self.__class__ is other.__class__ and
+ self.__dict__ == other.__dict__)
+
+ def test_similar_parameters(self, prevlist):
+ similarity = 0
+ rprevlist = prevlist[:]
+ rprevlist.reverse()
+ for param in Shape.all_parameters:
+ accum = 0
+ for prev in rprevlist:
+ if getattr(self, param) != getattr(prev, param):
+ break
+ else:
+ accum += 1
+ similarity += accum
+ minimum = min(4*len(prevlist), 7)
+ if not (minimum <= similarity <= 17):
+ self.reset()
+
+ def test_not_too_often(self, prevlist):
+ for param, bad_value, delay in [
+ ('mess', '.', 2),
+ ('mess', '!', 11),
+ ('holes', 1, 1),
+ ]:
+ if getattr(self, param) == bad_value:
+ for prev in prevlist[-delay:]:
+ if getattr(prev, param) == bad_value:
+ self.reset(param)
+
+ def test_mess_hole(self, prevlist):
+ if self.mess == '!':
+ self.holes = 1
+
+ all_tests = [value for (name, value) in locals().items()
+ if name.startswith('test_')]
+
+ def accept(self, lvl):
+ f = lambda d=self.difficulty : randrange(3, 4+int(9*d))
+ lvl.mlist = [(mnstrclslist(self.basemnstr), f)]
+ repeat = choice([2,2,3]) - self.extramnstr
+ if repeat > 1:
+ lvl.mlist *= repeat
+ if self.extramnstr:
+ othermnstr = [name for name in MnstrNames if name!=self.basemnstr]
+ if self.samemnstr:
+ othermnstr = [name for name in othermnstr
+ if MnstrCategory[name]==MnstrCategory[self.basemnstr]]
+ random.shuffle(othermnstr)
+ for name in othermnstr[:self.extramnstr]:
+ lvl.mlist.append((mnstrclslist(name), f))
+
+ lvl.genwalls = []
+
+ if self.baseshape == 'G':
+ lvl.genwalls.append((RandomLevel.grids,
+ uniform(0.7,0.8),
+ uniform(0.7,0.8)))
+ self.set_gens()
+ if self.baseshape == 'P':
+ lvl.genwalls.append((RandomLevel.pegs,
+ uniform(0.1,0.2),
+ uniform(0.45,0.7),
+ choice([0,1,1,1])))
+ self.set_gens(smooth=3)
+ self.closed = random.random() < 0.80
+ if self.baseshape == 'B':
+ nr = choice([0,0,1])
+ lvl.genwalls.append((RandomLevel.bouncers,
+ dice(1, 100) + 250 - nr*200, # length
+ uniform(0.7, 1.7),
+ nr))
+ self.set_gens(smooth=3)
+ if self.baseshape == 'W':
+ nr = dice(1, 3) + 2
+ lvl.genwalls.append((RandomLevel.walkers,
+ dice(2, 100) + 100, # length
+ nr, nr + dice(2, 3),
+ choice([0,1])))
+ self.set_gens()
+ if self.baseshape == 'R':
+ lvl.genwalls.append((RandomLevel.rivers,
+ randrange(3,(lvl.WIDTH-4)/4), # the number of rivers
+ uniform(0.3, 1.4), # the side stepping threshold
+ 10)) # the max side stepping size
+ self.set_gens()
+ if self.baseshape == 'Z':
+ lvl.genwalls.append((RandomLevel.zigzag,))
+ self.set_gens()
+ if self.baseshape == 'M':
+ lvl.genwalls.append((RandomLevel.mondrian,))
+ self.set_gens()
+ if self.baseshape == 'O':
+ lvl.genwalls.append((RandomLevel.remove_joined_blocks,))
+ self.set_gens()
+ if self.baseshape == 'S':
+ lvl.genwalls.append((RandomLevel.platforms_reg,))
+ self.set_gens()
+ self.makespace = random.random() < 0.1
+ if self.closed:
+ self.startplats = 0
+ if self.baseshape == 'D':
+ lvl.genwalls.append((RandomLevel.discrete_blocks,))
+ self.set_gens()
+ self.makespace = random.random() < 0.1
+ if self.closed:
+ self.startplats = 0
+
+ if self.rooms:
+ nr = dice(2, 6)
+ lvl.genwalls.append((RandomLevel.rooms,
+ lambda : flat(9-nr,2), # the half size of the room
+ lambda : uniform(0.8,1.2), # the excentricity of the room
+ nr)) # the number of rooms
+ if self.lines != ' ':
+ rng_angle = {
+ '-': lambda : 0,
+ '/': None, # default
+ '|': lambda : math.pi/2,
+ }
+ lvl.genwalls.append((RandomLevel.lines,
+ lambda : dice(8,3), # line length
+ dice(2,4), # number of lines
+ rng_angle[self.lines]))
+ if self.platforms:
+ nplat = dice(2,4,0)
+ if nplat: space = flat((lvl.HEIGHT-1)/nplat/2,(lvl.HEIGHT-1)/nplat/2-1)
+ else: space = 1
+ if self.platholes:
+ nholes = lambda : dice(1,3)
+ else:
+ nholes = lambda : 0
+ wholes = lambda : dice(2,3)
+ full = self.platfull
+ lvl.genwalls.append((RandomLevel.platforms,
+ (nplat,space), # number of platform and spacing
+ (nholes,wholes), # number of holes and width
+ full)) # full width platform
+ if self.mess != ' ':
+ threshold = {
+ '.': 0.02 + 0.08*random.random(), # normal
+ '!': 0.25 + 0.2 *random.random(), # super-filled
+ }
+ lvl.genwalls.append((RandomLevel.mess, threshold[self.mess]))
+ if self.holes:
+ nh = choice([1,1,2,2,2,3,3,3,4,5])
+ lvl.genwalls.append((RandomLevel.holes,
+ lambda : flat(9-nh,2), # radius of the holes
+ lambda : uniform(0.9,1.1), # excentricity
+ nh, # number of holes
+ lambda : choice([0,0,0,1]))) # circle or rectangle
+ if self.closed:
+ lvl.genwalls.append((RandomLevel.close,))
+ if self.smooth > 0:
+ # smooth away all lone empty spaces
+ lvl.genwalls.append((RandomLevel.smooth, 1.0, 1))
+ # possibly smooth away some lone bricks
+ if self.smooth == 2:
+ lvl.genwalls.append((RandomLevel.smooth, 0.25, 0))
+ elif self.smooth == 3:
+ lvl.genwalls.append((RandomLevel.smooth, 0.75, 0))
+ if self.startplats:
+ lvl.genwalls.append((RandomLevel.startplatform, ))
+ lvl.genwalls.append((RandomLevel.openstartway, ))
+
+ if self.makespace:
+ lvl.genwalls.append((RandomLevel.make_space, ))
+
+ if self.straightfall:
+ lvl.genwalls.append((RandomLevel.prevent_straight_fall, ))
+
+ if self.mirrored:
+ lvl.genwalls.append((RandomLevel.mirror, ))
+
+ if self.enlargeholes:
+ lvl.genwalls.append((RandomLevel.enlarge_tiny_holes, ))
+
+ lvl.genwalls.append((RandomLevel.generate_wind, ))
+ b = self.bonuses
+ for name in Bonuses:
+ setattr(lvl, name, (b % 3) == 1)
+ b = b // 3
+ lvl.autogen_shape = self
+
+
+def generate_shape(prevlist):
+ tests = Shape.all_tests
+ s = Shape()
+ for i in range(100):
+ s1 = Shape(s)
+ random.shuffle(tests)
+ for test in tests:
+ test(s1, prevlist)
+ if not s1.modified and s1 == s:
+ break
+ s = s1
+# else:
+# sys.stdout.write('*')
+ del s.modified
+ return s
+
+def makeshapes(nblevels=25):
+ shapelist = []
+ for i in range(nblevels):
+ s = generate_shape(shapelist)
+ s.difficulty = float(i+1)/nblevels
+ yield s
+ shapelist.append(s)
+ if len(shapelist) == 10:
+ del shapelist[:]
+
+def GenerateLevels():
+# print 'generating levels',
+ Levels = []
+ for s in makeshapes():
+ class level(RandomLevel):
+ WIDTH = 28
+ HEIGHT = 23
+ def enter(self, *args, **kw):
+ result = RandomLevel.enter(self, *args, **kw)
+ params = self.autogen_shape.__dict__.items()
+ params.sort()
+# for keyvalue in params:
+# print '%20s: %s' % keyvalue
+ return result
+ s.accept(level)
+ Levels.append(level)
+# sys.stdout.write('.')
+# sys.stdout.flush()
+# print
+ class levelfinal(RandomLevel):
+ WIDTH = level.WIDTH
+ HEIGHT = level.HEIGHT
+ genwalls = [(RandomLevel.platforms,(5,3),(lambda:flat(2,1),lambda:flat(6,2)),1),
+ (RandomLevel.close,)]
+ Levels.append(levelfinal)
+ return Levels
+
+def GenerateSingleLevel(width, height):
+ [s] = makeshapes(1)
+ class level(RandomLevel):
+ WIDTH = width
+ HEIGHT = height
+ s.accept(level)
+ return level
+
+if __name__ == '__main__':
+ for s in makeshapes():
+ print s.__dict__
+else:
+ rnglevel = {}
+ execfile('levels/rnglevel', rnglevel)
+ RandomLevel = rnglevel['RandomLevel']
diff --git a/bubbob/levels/rnglevel b/bubbob/levels/rnglevel
new file mode 100644
index 0000000..cfa6ec0
--- /dev/null
+++ b/bubbob/levels/rnglevel
@@ -0,0 +1,1276 @@
+from random import *
+from math import *
+
+import boarddef
+from boarddef import LNasty, LMonky, LGhosty, LFlappy
+from boarddef import LSpringy, LOrcy, LGramy, LBlitzy
+from boarddef import RNasty, RMonky, RGhosty, RFlappy
+from boarddef import RSpringy, ROrcy, RGramy, RBlitzy
+
+def flat(mean,var):
+ return randrange(mean-var,mean+var+1)
+
+def dice(n,sides,orig=1):
+ result = 0
+ for i in range(n):
+ result += orig+randrange(sides)
+ return result
+
+def fish(mu):
+ def fact(n):
+ r = 1.
+ for i in range(1,n+1):
+ r *= i
+ return r
+ scale = fact(0)/exp(-mu)
+ dens = []
+ while 1:
+ x = len(dens)
+ dens.append(int(scale*exp(-mu)*pow(mu,x)/fact(x)+0.5))
+ if x > mu and dens[-1] == 0:
+ break
+ table = []
+ x = 0
+ for d in dens:
+ for i in range(d):
+ table.append(x)
+ x += 1
+ return choice(table)
+
+
+class RandomLevel(boarddef.Level):
+ WIDTH = 32
+ HEIGHT = 28
+ MAXTRY = 1000
+ # parameters of the 'mess generator'
+ # mess_prob : the probability that a cell turn into a wall
+
+ def __init__(self,num):
+ if hasattr(self.__class__, 'walls'):
+ #print 'Reusing previously generated level'
+ #print self.__class__.walls
+ self.walls = self.__class__.walls
+ boarddef.Level.__init__(self,num)
+ return
+
+ #print 'Generating a new level'
+ self.reset(fill=False)
+
+ self.windmap = [ [' ' for x in range(self.WIDTH)] for y in range(self.HEIGHT) ]
+
+ if hasattr(self, 'auto'):
+ self.generate()
+ self.do_bonuses()
+
+ for gw in self.genwalls:
+ gw[0](self,*gw[1:])
+
+ if hasattr(self, 'mlist'):
+ self.do_monsters()
+
+ self.dig_vertical_walls()
+ self.do_walls()
+ self.walls = self.__class__.walls
+ #print self.walls
+
+ self.do_winds()
+ self.winds = self.__class__.winds
+
+ boarddef.Level.__init__(self,num)
+
+ def reset(self, fill=False):
+ if fill:
+ w = '#'
+ f = 0
+ else:
+ w = ' '
+ f = 1
+ # map for the walls
+ self.wmap = [ [w for x in range(self.WIDTH)] for y in range(self.HEIGHT) ]
+ # map of the free cells
+ self.fmap = [ [f for x in range(self.WIDTH)] for y in range(self.HEIGHT) ]
+
+ def setw(self,x,y,c='#'):
+ if x > self.WIDTH-1 or x < 0 or y > self.HEIGHT-1 or y < 0:
+ return
+ if self.fmap[y][x]:
+ self.wmap[y][x] = c
+ self.fmap[y][x] = 0
+
+ def getw(self,x,y):
+ if x > self.WIDTH-1 or x < 0 or y > self.HEIGHT-1 or y < 0:
+ return '#'
+ return self.wmap[y][x]
+
+ def clrw(self,x,y):
+ if x > self.WIDTH-1 or x < 0 or y > self.HEIGHT-1 or y < 0:
+ return
+ self.wmap[y][x] = ' '
+ self.fmap[y][x] = 1
+
+ def lockw(self,x,y,c=0):
+ if x > self.WIDTH-1 or x < 0 or y > self.HEIGHT-1 or y < 0:
+ return
+ self.fmap[y][x] = c
+
+ def setwind(self,x,y,c=' '):
+ if x > self.WIDTH-1 or x < 0 or y > self.HEIGHT-1 or y < 0:
+ return
+ self.windmap[y][x] = c
+
+ def getwind(self,x,y):
+ if x > self.WIDTH-1 or x < 0 or y > self.HEIGHT-1 or y < 0:
+ return ' '
+ return self.windmap[y][x]
+
+ def wind_rect(self,x,y,w,h,ccw=0):
+ "Set a wind in a rectangle which will move the bubbles cw or ccw"
+ if w < 1 or h < 1:
+ return
+ if ccw == 1:
+ for dx in range(w):
+ self.setwind(x+dx+1, y, '<')
+ self.setwind(x+dx, y+h, '>')
+ for dy in range(h):
+ self.setwind(x, y+dy, 'v')
+ self.setwind(x+w, y+dy+1, '^')
+ else:
+ for dx in range(w):
+ self.setwind(x+dx, y, '>')
+ self.setwind(x+dx+1, y+h, '<')
+ for dy in range(h):
+ self.setwind(x, y+dy+1, '^')
+ self.setwind(x+w, y+dy, 'v')
+
+ def mirror(self):
+ "Mirror the level vertically."
+ for y in range(self.HEIGHT):
+ for x in range(self.WIDTH/2):
+ self.wmap[y][x] = self.wmap[y][self.WIDTH-x-1]
+
+ def dig_well_until_space(self, x=1, yadj=1):
+ "Digs a well either up or down and stops when it encounters first empty wall space."
+ if yadj == 1:
+ y = 0
+ else:
+ yadj = -1
+ y = self.HEIGHT-1
+ while (y < self.HEIGHT) and (y >= 0):
+ self.clrw(x,y)
+ self.clrw(x+1,y)
+ y += yadj
+ if ((self.getw(x,y) == ' ') and (self.getw(x+1,y) == ' ')):
+ break
+
+ def enlarge_tiny_holes(self):
+ "Makes one-block size holes wider."
+ for x in range(self.WIDTH):
+ for y in range(self.HEIGHT):
+ if self.wmap[y][x] == ' ':
+ single = 0
+ for dx in range(x-1,x+2):
+ for dy in range(y-1,y+2):
+ if self.getw(dy,dx) == '#':
+ single = single + 1
+ if single == 8:
+ if x > (self.WIDTH / 2):
+ self.clrw(x-1,y)
+ else:
+ self.clrw(x+1,y)
+
+ def make_space(self, gens=-1):
+ "Removes walls from a level, to make it more playable."
+ if gens == -1:
+ gens = randint(0,62)+1
+ if gens & 1: # top
+ for x in range(self.WIDTH):
+ self.clrw(x,1)
+ if random() < 0.5:
+ self.clrw(x,2)
+ if gens & 2: # bottom
+ for x in range(self.WIDTH):
+ self.clrw(x,self.HEIGHT-1)
+ if random() < 0.5:
+ self.clrw(x,self.HEIGHT-2)
+ if gens & 4: # middle
+ y = randint(0,self.HEIGHT/10) + (self.HEIGHT/2)
+ for x in range(self.WIDTH):
+ self.clrw(x,y)
+ if random() < 0.5:
+ self.clrw(x,y-1)
+ if random() < 0.5:
+ self.clrw(x,y+1)
+ if gens & 8: # left
+ x = randint(0,self.WIDTH/4)
+ self.dig_well_until_space(x, 1)
+ self.dig_well_until_space(x, -1)
+ if gens & 16: # right
+ x = randint(0,self.WIDTH/4)
+ self.dig_well_until_space(self.WIDTH-x-2, 1)
+ self.dig_well_until_space(self.WIDTH-x-2, -1)
+ if gens & 32: # center
+ self.dig_well_until_space(self.WIDTH/2, 1)
+ self.dig_well_until_space(self.WIDTH/2, -1)
+
+ def generate_wind1(self, rndchoice=1, choices=[' ',' ',' ','x','>','<','^','^','v'], xsize=-1,ysize=-1):
+ """Makes a random wind pattern. Parameters:
+ 0: if 1=randomly select from choices, else select in order
+ 1: a list of the choices that are allowed.
+ 2: horizontal size of wind blocks
+ 3: vertical size of wind blocks
+ """
+ choicenum = 0
+ if xsize == -1:
+ xsize = randint(1, self.WIDTH)
+ if ysize == -1:
+ ysize = randint(1, self.HEIGHT)
+ if xsize < 1:
+ xsize = 1
+ elif xsize > self.WIDTH:
+ xsize = self.WIDTH
+ if ysize < 1:
+ ysize = 1
+ elif ysize > self.HEIGHT:
+ ysize = self.HEIGHT
+ for x in range((self.WIDTH/xsize)+1):
+ for y in range((self.HEIGHT/ysize)+1):
+ if rndchoice == 1:
+ wdir = choice(choices)
+ else:
+ wdir = choices[choicenum]
+ choicenum = (choicenum + 1) % len(choices)
+ for dx in range(xsize+1):
+ for dy in range(ysize+1):
+ self.setwind(x*xsize+dx,y*ysize+dy,wdir)
+ # make sure that the special bubbles can come into screen
+ for x in range(self.WIDTH):
+ self.setwind(x, 0, ' ')
+ self.setwind(x, self.HEIGHT-1, ' ')
+
+ def wind_sidewalls(self):
+ """Make sure the left and side walls have updraft next to them
+ """
+ for y in range(self.HEIGHT):
+ self.setwind(0,y,'^')
+ self.setwind(self.WIDTH-1,y,'^')
+
+ def wind_wallblocking(self, winddirs):
+ """Sets up wind depending on the number of walls around each place.
+ winddirs is an array of 16 wind chars.
+ directions with walls count as: 1=N, 2=E, 4=S, 8=W
+ 16th place is used if there is wall at the position.
+ """
+ for x in range(self.WIDTH):
+ for y in range(self.HEIGHT):
+ walld = 0
+ if self.getw(x,y) == '#':
+ walld = 16
+ else:
+ if self.getw(x,y-1) == '#':
+ walld = walld + 1
+ if self.getw(x+1,y) == '#':
+ walld = walld + 2
+ if self.getw(x,y+1) == '#':
+ walld = walld + 4
+ if self.getw(x-1,y) == '#':
+ walld = walld + 8
+ wnd = winddirs[walld]
+ self.setwind(x,y,wnd)
+
+ def wind_wallblocking256(self, winddirs):
+ """Sets up wind depending on the number of walls around each position.
+ winddirs is an array of 257 wind chars (one of ' x<>^v-'), where '-' means
+ to use '>' or '<', pointing towards center of level.
+ directions with walls count as: 1=N, 2=NE, 4=E, 8=SE, 16=S, 32=SW, 64=W, 128=NW
+ 257th place is use if there is wall at the position.
+ """
+ mdirs = [(0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1)]
+ for x in range(self.WIDTH):
+ for y in range(self.HEIGHT):
+ windd = 0
+ if self.getw(x,y) == '#':
+ windd = 256
+ else:
+ for d in range(8):
+ dx = x + mdirs[d][0]
+ dy = y + mdirs[d][1]
+ if self.getw(dx, dy) == '#':
+ windd = (1 << d)
+ wd = choice(winddirs[windd])
+ if wd == '-':
+ if x < self.WIDTH / 2:
+ wd = '>'
+ else:
+ wd = '<'
+ self.setwind(x,y, wd)
+
+ def generate_wind(self, gens = -1):
+ """Chooses one of the wind pattern generators and uses that to generate the winds.
+ 0: choose what generator to use.
+ """
+ if gens == -1:
+ gens = choice([1,1,2,3,4,4,4,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19])
+ if gens == 1: # totally random pattern
+ self.generate_wind1()
+ elif gens == 2: # wind "layers"
+ self.generate_wind1(0, ['x','^','^','^'],self.WIDTH,1)
+ self.wind_sidewalls()
+ elif gens == 3: # "wiggly" winds
+ self.generate_wind1(1, ['^','<','^','>'],1,1)
+ self.wind_sidewalls()
+ elif gens == 4: # "normal" wind pattern
+ self.wind_sidewalls()
+ dx = (self.WIDTH/2)
+ if random() < 0.7:
+ dy = 1 # usual height where bubbles collect
+ else:
+ dy = randint(1, self.HEIGHT-1)
+ for x in range(dx-3, dx+3):
+ self.setwind(x,dy,'x')
+ for x in range(dx-2):
+ self.setwind(x,dy,'>')
+ self.setwind(self.WIDTH-x-1,dy,'<')
+ elif gens == 5: # bubbles are stopped by horizontal walls
+ self.wind_sidewalls()
+ for x in range(self.WIDTH):
+ for y in range(self.HEIGHT-2):
+ if self.getw(x,y) == '#':
+ if self.getw(x,y+1) == ' ':
+ self.setwind(x,y+1,'x')
+ elif gens == 6: # bubbles move next the walls, rotating cw or ccw
+ if random() < 0.5: #clockwise
+ winddirs = [' ','>','v','v','<',' ','<','<','^','>',' ','v','^','>','^','x','x']
+ else:
+ winddirs = [' ','<','^','<','>',' ','^','<','v','v',' ','v','>','>','^','x','x']
+ self.wind_wallblocking(winddirs)
+ elif gens == 7: # bubbles move up in column(s) that zig-zag left and right
+ wid = choice([self.WIDTH, randint(2, self.WIDTH), randint(2, self.WIDTH)])
+ xofs = (self.WIDTH % wid) / 2
+ ofs = choice([0,1])
+ for dx in range(0, self.WIDTH-wid+1, wid):
+ for x in range(wid):
+ for y in range(self.HEIGHT):
+ if (y+ofs) & 1:
+ self.setwind(x+dx+xofs,y,'<')
+ if x == 0:
+ self.setwind(x+dx+xofs,y,'^')
+ else:
+ self.setwind(x+dx+xofs,y,'>')
+ if x == wid-1:
+ self.setwind(x+dx+xofs,y,'^')
+ elif gens == 8: # bubbles move towards the map centerline at random height
+ for x in range(self.WIDTH):
+ y = randint(1, self.HEIGHT-1)
+ if x < (self.WIDTH/2):
+ self.setwind(x,y,'>')
+ else:
+ self.setwind(x,y,'<')
+ self.setwind(x,0,'v')
+ self.setwind(x,self.HEIGHT-1,'^')
+ for y in range(self.HEIGHT):
+ self.setwind(self.WIDTH/2,y,'x')
+ elif gens == 9: # bubbles move towards the side walls at random height
+ for x in range(self.WIDTH):
+ y = randint(1, self.HEIGHT-1)
+ if y & 1:
+ self.setwind(x,y,'>')
+ else:
+ self.setwind(x,y,'<')
+ self.setwind(x,0,'v')
+ self.setwind(x,self.HEIGHT-1,'^')
+ for y in range(self.HEIGHT):
+ self.setwind(0,y,'x')
+ self.setwind(self.WIDTH-1,y,'x')
+ elif gens == 10: # bubbles move up-down
+ ofs = choice([0,1])
+ dir_l = choice(['>', '>', '<'])
+ dir_r = choice(['<', '<', '>'])
+ for x in range(self.WIDTH):
+ if x < (self.WIDTH / 2):
+ self.setwind(x, 0, dir_r)
+ self.setwind(x,self.HEIGHT-1,dir_l)
+ else:
+ self.setwind(x, 0, dir_l)
+ self.setwind(x,self.HEIGHT-1,dir_r)
+ for x in range(self.WIDTH):
+ for y in range(self.HEIGHT):
+ if (x+ofs) & 1:
+ self.setwind(x,y+1,'^')
+ else:
+ self.setwind(x,y-1,'v')
+ elif gens == 11: # bubbles rotate
+ self.wind_sidewalls()
+ for z in range(20):
+ wid = randint(2,self.WIDTH/2)
+ hei = randint(2,self.HEIGHT/2)
+ y = randint(1, self.HEIGHT - hei - 1)
+ x = randint(1, self.WIDTH - wid - 1)
+ ok = 1
+ for dx in range(wid):
+ if self.getwind(x+dx+1, y) != ' ':
+ ok = 0
+ if self.getwind(x+dx, y+hei) != ' ':
+ ok = 0
+ for dy in range(hei):
+ if self.getwind(x, y+dy) != ' ':
+ ok = 0
+ if self.getwind(x+wid, y+dy+1) != ' ':
+ ok = 0
+ if ok == 1:
+ self.wind_rect(x,y,wid,hei, random() < 0.5)
+ elif gens == 12: # bubbles gravitate towards a certain spot
+ dx = randint(1,self.WIDTH-1)
+ dy = randint(1,self.HEIGHT-1)
+ for x in range(self.WIDTH):
+ for y in range(self.HEIGHT):
+ ax = abs(dx - x)
+ ay = abs(dy - y)
+ sx = cmp(dx - x, 0)
+ sy = cmp(dy - y, 0)
+ winds = [' ',' ',' ']
+ if ax < 2 and ay < 2:
+ winds = ['x']
+ else:
+ if sx < 0:
+ winds += ['<']
+ elif sx > 0:
+ winds += ['>']
+ else:
+ if sy > 0:
+ winds = ['v']
+ elif sy < 0:
+ winds = ['^']
+ else:
+ winds = ['x']
+ if sy < 0:
+ winds += ['^']
+ elif sy > 0:
+ winds += ['v']
+ else:
+ if sx > 0:
+ winds = ['>']
+ elif sx < 0:
+ winds = ['<']
+ else:
+ winds = ['x']
+ self.setwind(x,y,choice(winds))
+ elif gens == 13: # bubbles stop at some random positions
+ self.generate_wind1(1, [' ',' ',' ',' ',' ',' ',' ','x'],1,1)
+ self.wind_sidewalls()
+ elif gens == 14: # bubbles move cw and ccw in alternating rectangles
+ m = max(self.WIDTH / 2, self.HEIGHT / 2)
+ cwofs = choice([0,1])
+ thk = choice([1,1,2,2,3,4,randint(1,m/2)])
+ for dx in range(m):
+ cw = ((dx / thk) + cwofs) % 2
+ self.wind_rect(dx,dx, self.WIDTH-(dx*2), self.HEIGHT-(dx*2), cw)
+ elif gens == 15: # bubbles move cw or ccw in rectangles
+ m = max(self.WIDTH / 2, self.HEIGHT / 2)
+ cw = choice([0,1])
+ for dx in range(m):
+ self.wind_rect(dx,dx, self.WIDTH-(dx*2), self.HEIGHT-(dx*2), cw)
+ elif gens == 16:
+ xs = randint(2, (self.WIDTH/2)-1)
+ ys = randint(2, (self.HEIGHT/2)-1)
+ rx = (self.WIDTH / xs) + 1
+ ry = (self.HEIGHT / ys) + 1
+ cwchanges = choice([0,0,0,0,0,0,0,0,1,1,1,1,2,2,3])
+ if cwchanges == 0:
+ cw = random() < 0.5
+ for x in range(rx):
+ if cwchanges == 1:
+ cw = random() < 0.5
+ for y in range(ry):
+ if cwchanges == 2:
+ cw = random() < 0.5
+ maxd = max((xs / 2), (ys / 2))
+ for d in range(maxd):
+ if cwchanges == 3:
+ cw = random() < 0.5
+ self.wind_rect(xs*x+d, ys*y+d, xs-2*d-1, ys-2*d-1, cw)
+ elif gens == 17: # bubbles bounce between walls
+ if random() < 0.5: # horizontal
+ winddirs = [' ',' ','<','<',' ',' ','<','<','>','>',' ',' ','>','>',' ',' ','x']
+ else: # vertical
+ winddirs = [' ','v',' ','v','^',' ','^',' ',' ','v',' ','v','^',' ','^',' ','x']
+ self.wind_wallblocking(winddirs)
+ elif gens == 18: # generate winds based on a random 3x3 matrix ruleset
+ winddirs = []
+ for z in range(257):
+ winddirs.append(choice([' ',' ',' ','x','^','v','-']))
+ winddirs[0] = ' '
+ winddirs[256] = choice(['x',' '])
+ self.wind_wallblocking256(winddirs)
+ elif gens == 19: # bubbles will move downwards in a zig-zag pattern
+ y = 0
+ x1 = randint(0, self.WIDTH-1)
+ while y < self.HEIGHT:
+ x2 = randint(0, self.WIDTH-1)
+ if x1 < x2:
+ self.setwind(x1,y, '>')
+ else:
+ self.setwind(x1,y, '<')
+ dy = choice([1,1,1,2])
+ self.setwind(x2,y, 'v')
+ y += dy
+ x1 = x2
+
+ def smooth(self, threshold, rev):
+ """Remove wall blocks that are surrounded by 4 empty places.
+ 0: probability which a wall cell is turned into space
+ 1: smooth away walls or smooth away empty spaces?
+ """
+ # make a copy of self.wmap and adds '#' at the end of line, for
+ # the overflowing indexing below: [x-1] and [x+1]
+ tmpwmap = [ line + ['#'] for line in self.wmap ]
+ if rev == 0:
+ chr = ' '
+ else:
+ chr = '#'
+ for x in range(self.WIDTH):
+ for y in range(1,self.HEIGHT-1):
+ count = 0
+ if tmpwmap[y+1][x] == chr:
+ count = count + 1
+ if tmpwmap[y-1][x] == chr:
+ count = count + 1
+ if tmpwmap[y][x+1] == chr:
+ count = count + 1
+ if tmpwmap[y][x-1] == chr:
+ count = count + 1
+ if (count >= 4) and (random() < threshold):
+ if rev == 0:
+ self.clrw(x,y)
+ else:
+ self.setw(x,y)
+
+ def mess(self, threshold):
+ """Random fill of the board with walls.
+ Only one argument, the probability that
+ a cell turns out to be a wall.
+ """
+ for x in range(self.WIDTH):
+ for y in range(self.HEIGHT):
+ if random() < threshold:
+ self.setw(x,y)
+
+ def zigzag_lr(self):
+ """Generate the level with random left-right zig-zags.
+ """
+ first = 1
+ self.reset(fill=False)
+ y = choice([0,0,2,3,3,4])
+ while y < (self.HEIGHT-2):
+ if first == 1:
+ first = 0
+ x1 = x2 = randint(2, self.WIDTH-3)
+ else:
+ x2 = randint(2, self.WIDTH-3)
+ while (x2 > (x1-3)) and (x2 < (x1+3)):
+ x2 = randint(2, self.WIDTH-3)
+ for dx in range(min(x1,x2+1), max(x1,x2+1)):
+ self.setw(dx,y)
+ dy = choice([2,2,3,3,3,4])
+ for dy in range(dy+1):
+ self.setw(x2,y+dy)
+ y = y + dy
+ x1 = x2
+
+ def zigzag_ud(self):
+ """Generate the level with random up-down zig-zags.
+ """
+ first = 1
+ self.reset(fill=False)
+ x = -1
+ while x < self.WIDTH:
+ if first == 1:
+ first = 0
+ y1 = y2 = randint(2, self.HEIGHT-1)
+ else:
+ y2 = randint(2, self.HEIGHT-1)
+ while (y2 > (y1-2)) and (y2 < (y1+2)):
+ y2 = randint(2, self.HEIGHT-1)
+ for dy in range(min(y1,y2+1), max(y1,y2+1)):
+ self.setw(x,dy)
+ dx = choice([3,4,4,4,5,6])
+ for dx in range(dx+1):
+ self.setw(x+dx,y2)
+ x = x + dx
+ y1 = y2
+
+ def zigzag(self):
+ """Generate a level with a random zig-zag form.
+ """
+ if random() < 0.5:
+ self.zigzag_lr()
+ else:
+ self.zigzag_ud()
+
+ def platforms(self, (nplat, space), (rng_holes, rng_width), full=1):
+ """Place random platforms.
+ args is a tuple with the following fields:
+ 0: a tuple containing the number of platforms and
+ the minum space between two platforms,
+ 1: a tuple indicating in order:
+ - the rng for the number of holes per platform
+ - the rng for the width of the holes,
+ 2: a flag indicating whether the platform should cross
+ the whole level or not.
+ """
+ plat = []
+ for i in range(nplat):
+ ntry = 100
+ while ntry:
+ y = randint(0,self.HEIGHT-1)
+ found = 0
+ for old in plat:
+ if abs(old-y) <= space:
+ found = 1
+ break
+ if not found:
+ plat.append(y)
+ break
+ ntry -= 1
+ if not ntry:
+ continue # ignore platform
+ if full:
+ x = 0
+ w = self.WIDTH
+ else:
+ x = randint(0,self.WIDTH-1)
+ w = randint(0,self.WIDTH-1)
+ s = choice([-1,1])
+ if s == -1:
+ w = min(w,x)
+ x -= w
+ else:
+ w = min(w,self.WIDTH-x)
+ for x1 in range(x,x+w):
+ self.setw(x1,y)
+ for i in range(rng_holes()):
+ hx = randint(x,x+w)
+ hw = rng_width()
+ for h in range(hx-hw/2,hx+hw/2):
+ self.clrw(h,y)
+
+ def remove_joined_blocks(self):
+ """Removes randomly placed, random sized blocks of walls.
+ The blocks are usually joined, or if not, they're offset so that
+ it's possible to move from one block to another by jumping.
+ """
+ self.reset(fill=True)
+ nrooms = randint(1, 4)
+ while nrooms:
+ nrooms -= 1;
+ x = 0
+ while x < self.WIDTH:
+ wid = randint(2,8)
+ hei = randint(2,6)
+ y = randint(2, self.HEIGHT - hei - 1)
+ for dx in range(wid):
+ for dy in range(hei):
+ self.clrw(x+dx, y+dy)
+ x += wid + choice([-2,-2,-1,-1,0]);
+
+ def discrete_blocks(self, blocks = -1):
+ """Put certain size blocks randomly, but so that they don't touch each other.
+ """
+ self.reset(fill=False)
+ if blocks == -1:
+ if random() < 0.75:
+ blocks = [(4,2),(2,4)] # CompactLevels, level 16
+ if random() < 0.30:
+ blocks.append((2,2))
+ if random() < 0.20:
+ blocks.append((6,2))
+ if random() < 0.10:
+ blocks.append((8,2))
+ else:
+ blocks = []
+ while len(blocks) == 0:
+ for bz in range(10):
+ if random() < 0.3:
+ blocks.append((bz+1,1))
+ ntry = 300
+ while ntry:
+ ntry -= 1
+ doput = 1
+ block = choice(blocks)
+ wid = block[0]
+ hei = block[1]
+ x = randint(0,self.WIDTH-wid-2)
+ y = randint(1,self.HEIGHT-hei-3)
+ for dx in range(x,x+wid+2):
+ for dy in range(y,y+hei+2):
+ if self.getw(dx,dy) == '#':
+ doput = 0
+ if doput:
+ for dx in range(x+1,x+wid+1):
+ for dy in range(y+1,y+hei+1):
+ self.setw(dx,dy)
+
+ def lines(self, rng_len, nlines, rng_angle=None):
+ """Generate a set of lines in any direction. It takes three
+ arguments, a rng for the length the lines, the number of lines,
+ and a rng for the angle.
+ """
+ if rng_angle is None:
+ rng_angle = lambda : choice([0]+[pi/i for i in range(3,21)]+[-pi/i for i in range(3,21)])
+ for i in range(nlines):
+ len = rng_len()
+ angle = rng_angle()
+ ntry = self.MAXTRY
+ while ntry:
+ sx = randint(0,self.WIDTH-1)
+ sy = randint(0,self.HEIGHT-1)
+ dx = int(sx + len*cos(angle) + 0.5)
+ dy = int(sy + len*sin(angle) + 0.5)
+ if dx < self.WIDTH and dy < self.HEIGHT and dx >= 0 and dy >= 0:
+ break
+ ntry -= 1
+ if ntry == 0:
+ break
+ if abs(dx-sx) > abs(dy-sy):
+ for x in range(dx-sx+1):
+ y = (2*(dy-sy)*x/(dx-sx)+1)/2
+ self.setw(sx+x,sy+y)
+ else:
+ for y in range(dy-sy+1):
+ x = (2*(dx-sx)*y/(dy-sy)+1)/2
+ self.setw(sx+x,sy+y)
+
+ def rooms(self, rng_radius, rng_e, n_rooms):
+ """Generate rooms. It takes the following arguments:
+ 0: the rng for the radius of the room
+ 1: the rng for the excentricity of the room
+ 2: the number of rooms
+ """
+ for i in range(n_rooms):
+ cx = randint(0,self.WIDTH-1)
+ cy = randint(0,self.HEIGHT-1)
+ r = rng_radius()
+ e = rng_e()*1.0
+ left = cx-int(r*e+0.5)
+ right = cx+int(r*e+0.5)
+ top = cy-int(r/e+0.5)
+ bottom = cy+int(r/e+0.5)
+ for x in range(left,right+1):
+ self.setw(x,top)
+ self.setw(x,bottom)
+ for y in range(top,bottom+1):
+ self.setw(left,y)
+ self.setw(right,y)
+ for x in range(left+1,right):
+ for y in range(top+1,bottom):
+ self.lockw(x,y)
+
+ def holes(self, rng_radius, rng_e, n_holes, rng_rect):
+ """Generate a set of holes in the level. It takes four args:
+ 0: the rng for the radius of the holes
+ 1: the rng for the excentricity of the holes
+ 2: the number of holes
+ 3: the rng for the shape of the hole 0 for circular, 1 for rectangular
+ """
+ for i in range(n_holes):
+ cx = randint(0,self.WIDTH-1)
+ cy = randint(0,self.HEIGHT-1)
+ r = rng_radius()
+ e = rng_e()*1.0
+ rect = rng_rect()
+ for x in range(cx-int(r*e+0.5),cx+int(r*e+0.5)+1):
+ for y in range(cy-int(r/e+0.5),cy+int(r/e+0.5)+1):
+ if not rect and (((x-cx)/e)**2+((y-cy)*e)**2) > r**2:
+ continue
+ self.clrw(x,y)
+
+ def grids(self, horizchance, vertchance):
+ """Generate a level with a grid of horizontal and vertical lines
+ 0: gaussian chance of each horizontal line part
+ 1: gaussian chance of each vertical line part
+ """
+ self.reset(fill=False)
+ xsize = choice([3,3,3,4,4,4,4,5,6])
+ ysize = choice([2,3,3,4,4,4,4,5])
+ xofs = choice([-1,0,1])
+ yofs = choice([-1,0,1])
+ for x in range((self.WIDTH/xsize)+1):
+ for y in range((self.HEIGHT/ysize)+1):
+ dx = x*xsize + xofs
+ dy = y*ysize + yofs
+ if gauss(0,1) > horizchance:
+ for i in range(0,xsize+1):
+ self.setw(dx+i,dy)
+ if gauss(0,1) > vertchance:
+ for i in range(0,ysize+1):
+ self.setw(dx,dy+i)
+
+ def pegs(self, pegchance, posadj, thick):
+ """Generate a level by putting pegs
+ 0: gaussian level of a peg appearance
+ 1: gaussian level of peg position adjustment
+ """
+ self.reset(fill=False)
+ xdist = choice([3,3,3,4,4,5]) # distance between pegs
+ ydist = choice([2,3,3,3,4,5]) # distance between pegs
+ if not thick:
+ xdist = xdist - randint(0,1)
+ ydist = ydist - randint(0,1)
+ xadj = randint(0,4) - 2
+ yadj = randint(0,4) - 2
+ for x in range(self.WIDTH / xdist):
+ for y in range(self.HEIGHT / ydist):
+ if gauss(0,1) > pegchance:
+ dx = x * xdist + xadj
+ dy = y * ydist + yadj
+ if gauss(0,1) > posadj:
+ dx = dx + randint(0,2) - 1
+ dy = dy + randint(0,2) - 1
+ self.setw(dx,dy)
+ if thick:
+ self.setw(dx+1,dy)
+ self.setw(dx,dy+1)
+ self.setw(dx+1,dy+1)
+
+ def mondrian(self, x1=2,y1=2,x2=-1,y2=-1, horiz=-1, mindepth=3):
+ """Generate a level that looks a bit like a Piet Mondrian painting, or
+ different sized rectangles stacked on top of each other.
+ 0-3: the size of the area to be split
+ 4: whether the first split is horizontal or vertical
+ 5: minimum number of splits to do
+ """
+ if horiz == -1:
+ horiz = choice([0,1])
+ if x2 == -1:
+ x2 = self.WIDTH-2
+ if y2 == -1:
+ y2 = self.HEIGHT-2
+ if (abs(x2-x1) < 6) or (abs(y2-y1) < 5):
+ return
+ mindepth = mindepth - 1
+ if horiz == 1:
+ horiz = 0
+ dy = randint(y1+2,y2-2)
+ for dx in range(min(x1,x2),max(x1,x2)):
+ self.setw(dx,dy)
+ if (random() < 0.75) or (mindepth > 0):
+ self.mondrian(x1,y1,x2,dy, horiz, mindepth)
+ if (random() < 0.75) or (mindepth > 0):
+ self.mondrian(x1,dy,x2,y2, horiz, mindepth)
+ else:
+ horiz = 1
+ dx = randint(x1+3,x2-3)
+ for dy in range(min(y1,y2),max(y1,y2)):
+ self.setw(dx,dy)
+ if (random() < 0.75) or (mindepth > 0):
+ self.mondrian(x1,y1,dx,y2, horiz, mindepth)
+ if (random() < 0.75) or (mindepth > 0):
+ self.mondrian(dx,y1,x2,y2, horiz, mindepth)
+
+ def bouncers(self, length, diradj, rev):
+ """Generate a level using a down and left or right moving walker
+ 0: how many steps does the walker take
+ 1: gaussian level, how often to change moving from left to right
+ 2: fill empty level with wall or reverse?
+ """
+ if rev == 0:
+ self.reset(fill=True)
+ else:
+ self.reset(fill=False)
+ x = randint(0,self.WIDTH-2)
+ y = randint(0,self.HEIGHT-2)
+ lorr = choice([1, -1]) # move left or right
+ for i in range(length):
+ if rev == 0:
+ self.clrw(x,y)
+ self.clrw(x+1,y)
+ self.clrw(x,y+1)
+ self.clrw(x+1,y+1)
+ else:
+ self.setw(x,y)
+ self.setw(x+1,y)
+ x = x + lorr
+ y = y + 1
+ if y > self.HEIGHT:
+ y = 0
+ if x > self.WIDTH - 2:
+ x = self.WIDTH - 2
+ lorr = -lorr
+ elif x < 0:
+ x = 0
+ lorr = -lorr
+ if gauss(0,1) > diradj:
+ lorr = -lorr
+
+
+ def walkers(self, length, minturn, maxturn, isbig):
+ """Generate a level with a walker
+ 0: length of the walker: how many steps it walks
+ 1: minimum length it walks straight, before turning
+ 2: maximum length it walks straight, before turning
+ 3: is the trail is 1 or 2 blocks high
+ """
+ # We start from a full wall
+ self.reset(fill=True)
+ x = randint(0,self.WIDTH-2)
+ y = randint(0,self.HEIGHT-2)
+ dir = randint(0,4)
+ dlen = 0
+ for i in range(length):
+ self.clrw(x,y)
+ self.clrw(x+1,y)
+ if isbig == 1:
+ self.clrw(x,y+1)
+ self.clrw(x+1,y+1)
+ dlen = dlen + 1
+ if dir == 0:
+ x = x - 2
+ if x < 0:
+ x = self.WIDTH-2
+ elif dir == 1:
+ y = y - 1
+ if y < 0:
+ y = self.HEIGHT
+ elif dir == 2:
+ x = x + 2
+ if x > (self.WIDTH - 2):
+ x = 0
+ else:
+ y = y + 1
+ if y > self.HEIGHT:
+ y = 0
+ if dlen > randint(minturn, maxturn):
+ # turn 90 degrees
+ dir = (dir + choice([1,3])) % 4
+ dlen = 0
+
+ def rivers(self, n_flow, side_threshold, side_shift):
+ """Generate flow paths by digging a big wall. The arguments are:
+ 0: the number of parallel flow to dig in the wall
+ 1: side_threshold is a gausian level for doing a step aside
+ 2: side_shift is the maximal size of the side step.
+ """
+ # We start from a full wall
+ self.reset(fill=True)
+ for x in [0, self.WIDTH-2]+[randint(3,self.WIDTH-5) for f in range(max(0, n_flow-2))]:
+ for y in range(self.HEIGHT):
+ self.clrw(x,y)
+ self.clrw(x+1,y)
+ g = gauss(0,1)
+ if abs(g) > side_threshold:
+ # We want to move aside, let's find which side is the best:
+ if self.WIDTH/4 < x < 3*self.WIDTH/4:
+ side = random() > 0.5
+ t = random()
+ if t > x*4/self.WIDTH:
+ side = 1
+ elif t > (self.WIDTH-x)*4/self.WIDTH:
+ side = -1
+ side_step = randint(1,side_shift)
+ if side > 0:
+ for i in range(x+2, min(x+2+side_step,self.WIDTH-1)):
+ self.clrw(i,y)
+ x = max(0,min(x+side_step, self.WIDTH-2))
+ else:
+ for i in range(max(x-side_step,0),x):
+ self.clrw(i,y)
+ x = max(x-side_step, 0)
+
+ def platforms_reg(self):
+ """Generate random platforms at regular y-intervals.
+ """
+ self.reset(fill=False)
+ yadjs = [-2,-1,0,0,0,0,0,0,0,0,1,2]
+ y = randint(2,4)
+ yinc = randint(2,6)
+ yincadj = choice(yadjs)
+ ymax = self.HEIGHT-choice([1,1,1,1,1,2,2,2,3,3,4])-1
+ while y < ymax:
+ holes = randint(choice([0,1,1,1,1]),7)
+ for x in range(0, self.WIDTH):
+ self.setw(x,y)
+ for i in range(holes):
+ x = randint(0, self.WIDTH-2)
+ self.clrw(x,y)
+ self.clrw(x+1,y)
+ y = y + yinc
+ yinc = yinc + yincadj
+ if yinc < 2:
+ yinc = 2
+ yincadj = choice(yadjs)
+ if yinc > 6:
+ yinc = 6
+ yincadj = choice(yadjs)
+
+ def startplatform(self):
+ "Make sure there's free space with wall underneath for dragon start positions"
+ hei = choice([1,1,1,2,2,3])
+ lft = choice([0,1])
+ wid = choice([3,3,3,4,5])
+ for x in range(lft, wid):
+ self.setw(x,self.HEIGHT-1)
+ self.setw(self.WIDTH-x-1,self.HEIGHT-1)
+ for y in range(hei+1):
+ self.clrw(x,self.HEIGHT-2-y)
+ self.clrw(self.WIDTH-x-1,self.HEIGHT-2-y)
+
+ def openstartway(self):
+ "Make sure there is a way from the starting position to the center of the level. Reduces player frustrations."
+ gen = choice([0,0,0,1])
+ if gen == 0: # horizontal open space to middle of level
+ ypos = choice([1,1,1,1,1,2,2,3,4,randint(1,self.HEIGHT/2)])
+ hei = choice([1,1,1,2])
+ for x in range(self.WIDTH/2):
+ for y in range(hei):
+ self.clrw(x, self.HEIGHT-1-ypos-y)
+ ypos = choice([1,1,1,1,1,2,2,3,4,randint(1,self.HEIGHT/2)])
+ hei = choice([1,1,1,2])
+ for x in range(self.WIDTH/2):
+ for y in range(hei):
+ self.clrw(self.WIDTH-x-1, self.HEIGHT-1-ypos-y)
+ elif gen == 1: # open way diagonally to NW or NS, third of a way to the level width
+ ypos = choice([1,1,1,1,1,2,2,3,4])
+ wid = choice([2,2,2,2,2,3,3,4])
+ for x in range(self.WIDTH/3):
+ for z in range(wid):
+ self.clrw(x+z, self.HEIGHT-1-x-ypos)
+ ypos = choice([1,1,1,1,1,2,2,3,4])
+ wid = choice([2,2,2,2,2,3,3,4])
+ for x in range(self.WIDTH/2):
+ for z in range(wid):
+ self.clrw(self.WIDTH-x-1-z, self.HEIGHT-1-x-ypos)
+
+ def close(self):
+ "Just close the level with floor and roof"
+ for x in range(self.WIDTH):
+ self.setw(x,0)
+ self.setw(x,self.HEIGHT)
+
+ def largest_vertical_hole(self, x):
+ "Returns the (start, stop) of the largest range of holes in column x."
+ if not (0 <= x < self.WIDTH):
+ return (0, 0)
+ ranges = []
+ best = 0
+ length = 0
+ for y in range(self.HEIGHT+1):
+ if y < self.HEIGHT and self.getw(x,y) == ' ':
+ length += 1
+ elif length > 0:
+ if length > best:
+ del ranges[:]
+ best = length
+ if length == best:
+ ranges.append((y-length, y))
+ length = 0
+ return choice(ranges or [(0, 0)])
+
+ def dig_vertical_walls(self):
+ "Check that no vertical wall spans the whole height of the level"
+ vwall = []
+ for x in range(self.WIDTH):
+ spaces = 0
+ for y in range(self.HEIGHT-1): # ignore bottom line spaces
+ spaces += self.getw(x,y) == ' '
+ if spaces == 0 or (random() < 0.4**spaces):
+ vwall.append(x)
+ shuffle(vwall)
+ for x in vwall:
+ # look for the longest continuous space in each of the two
+ # adjacent columns, and extend these to the current column
+ def dig(y1, y2):
+ for y in range(y1, y2):
+ self.clrw(x, y)
+ return y1 < y2 and y1 < self.HEIGHT-1
+ progress = False
+ for col in [x-1, x+1]:
+ y1, y2 = self.largest_vertical_hole(col)
+ progress |= dig(y1, y2)
+ while not progress:
+ progress |= dig(randint(0, self.HEIGHT-1),
+ randint(0, self.HEIGHT-1))
+
+ def prevent_straight_fall(self):
+ """Make platforms that prevent falling straight from top to bottom, but
+ still leave space for moving.
+ """
+ falls = []
+ for x in range(self.WIDTH):
+ for y in range(self.HEIGHT):
+ if self.getw(x,y) == '#':
+ break
+ else:
+ falls = falls + [x]
+ y = oldy = -10
+ for x in falls:
+ while (y < oldy+2) and (y > oldy-2):
+ y = randint(2, self.HEIGHT-2)
+ for dy in range(y-1,y+2):
+ for dx in range(x-3, x+4):
+ self.clrw(dx,dy)
+ self.setw(x-1,y)
+ self.setw(x+1,y)
+ self.setw(x,y)
+ oldy = y
+
+ def do_monsters(self):
+ """Create monsters based on the requested settings.
+ mlist is a list of monster setting. Each item is a tuple with:
+ 0: the list of monster to uses (each item might be a tuple)
+ 1: the rng for the number of monsters to pick in the list.
+ """
+ current = 'a'
+ for ms in self.mlist:
+ n_monsters = ms[1]()
+ for idx in range(n_monsters):
+ self.__class__.__dict__[current] = choice(ms[0])
+ ntry = self.MAXTRY
+ while ntry:
+ x = randint(0,self.WIDTH-2)
+ y = randint(0,self.HEIGHT-1)
+
+ if self.getw(x,y) == self.getw(x+1,y) == ' ':
+ self.wmap[y][x] = current
+ break
+ ntry -= 1
+ current = chr(ord(current)+1)
+
+ def do_walls(self):
+ "Build the actual walls map for the game."
+ self.__class__.walls = ''
+ for y in range(self.HEIGHT-1):
+ self.__class__.walls += '##'
+ for x in range(self.WIDTH):
+ self.__class__.walls += self.wmap[y][x]
+ self.__class__.walls += '##\n'
+ self.__class__.walls += '##'
+ for x in range(self.WIDTH):
+ if self.getw(x,0) == '#' or self.getw(x,self.HEIGHT-1) == '#':
+ self.__class__.walls += '#'
+ else:
+ self.__class__.walls += ' '
+ self.__class__.walls += '##\n'
+
+ def do_winds(self):
+ "Build the actual wind map for the game."
+ self.__class__.winds = ''
+ for y in range(self.HEIGHT):
+ self.__class__.winds += '>>'
+ for x in range(self.WIDTH):
+ self.__class__.winds += self.windmap[y][x]
+ self.__class__.winds += '<<' + '\n'
+
+ def do_bonuses(self):
+ self.__class__.letter = choice([0,1])
+ self.__class__.fire = choice([0,1])
+ self.__class__.lightning = choice([0,1])
+ self.__class__.water = choice([0,1])
+ self.__class__.top = choice([0,1])
+
+ def generate(self):
+ "Generate random level settings."
+ assert 0, "--- THIS IS NO LONGER REALLY USED ---"
+ self.mlist = [([
+ LNasty, LMonky, LGhosty, LFlappy, LSpringy, LOrcy, LGramy, LBlitzy,
+ RNasty, RMonky, RGhosty, RFlappy, RSpringy, ROrcy, RGramy, RBlitzy,
+ ],lambda : flat(12,4))]
+ gens = choice([512,512,256,256,128,128,64,64,32,32,16,16,16,16,16,16,20,20,8,8,8,8,4,4,4,4,2,2,2,2,1,1,3,5,6,7])
+ self.genwalls = []
+ if gens & 512:
+ print 'Using grids generator'
+ self.genwalls.append((RandomLevel.grids,
+ uniform(0.0, 0.1),
+ uniform(0.0, 0.1)))
+ if gens & 256:
+ # generate using pegs
+ print 'Using the pegs generator'
+ self.genwalls.append((RandomLevel.pegs,
+ uniform(0.1,0.2),
+ uniform(0.45,0.7),
+ choice([0,1,1,1])))
+ if gens & 128:
+ # generate using a bouncer
+ nr = choice([0,0,1])
+ print 'Using the bouncer generator'
+ self.genwalls.append((RandomLevel.bouncers,
+ dice(1, 100) + 250 - nr*200, # length
+ uniform(0.7, 1.7),
+ nr))
+ if gens & 64:
+ # generate using a walker
+ print 'Using the walker generator'
+ nr = dice(1, 3) + 2
+ self.genwalls.append((RandomLevel.walkers,
+ dice(2, 100) + 100, # length
+ nr, nr + dice(2, 3), # straight walk min, max len
+ choice([0,1])))
+ if gens & 32:
+ # generate rivers
+ print 'Using the rivers generator'
+ self.genwalls.append((RandomLevel.rivers,
+ randrange(2,(self.WIDTH-4)/5), # the number of rivers
+ uniform(0.7, 1.7), # the side stepping threshold
+ 6)) # the max side stepping size
+ if gens & 16:
+ # generate rooms
+ print 'Using the romms generator'
+ nr = choice([1,2,2,2,3,3,4,5])
+ self.genwalls.append((RandomLevel.rooms,
+ lambda : flat(9-nr,2), # the half size of the room
+ lambda : uniform(0.8,1.2), # the excentricity of the room
+ nr)) # the number of rooms
+ if gens & 8:
+ # generate a holes generator
+ # as this is interesting only if the level is filled somehow
+ print 'Using the holes generator'
+ self.genwalls.append((RandomLevel.mess,1-uniform(0.2,0.5)))
+ nh = choice([1,1,2,2,2,3,3,3,4,5])
+ self.genwalls.append((RandomLevel.holes,
+ lambda : flat(9-nh,2), # radius of the holes
+ lambda : uniform(0.9,1.1), # excentricity
+ nh, # number of holes
+ lambda : choice([0,0,0,1]))) # circle or rectangle
+ if gens & 4:
+ # generate a lines generator
+ print 'Using the lines generator'
+ self.genwalls.append((RandomLevel.lines,
+ lambda : dice(7,3), # line length
+ dice(2,3))) # number of lines
+ if gens & 2:
+ # generate a platforms generator
+ print 'Using the platforms generator'
+ nplat = dice(2,4,0)
+ if nplat: space = flat((self.HEIGHT-1)/nplat/2,(self.HEIGHT-1)/nplat/2-1)
+ else: space = 1
+ nholes = lambda : dice(1,3,0)
+ wholes = lambda : dice(2,3)
+ full = randrange(2)
+ self.genwalls.append((RandomLevel.platforms,
+ (nplat,space), # number of platform and spacing
+ (nholes,wholes), # number of holes and width
+ full)) # full width platform
+ if gens & 1:
+ # generate a mess generator
+ print 'Using the mess generator'
+ if gens & ~2:
+ offset = 0
+ scale = 0.05
+ else:
+ offset = 0.05
+ scale = 0.10
+ self.genwalls.append((RandomLevel.mess,offset+random()*scale))
+ if random() < 0.2:
+ self.genwalls.append((RandomLevel.close,))
+ if random() < 0.90:
+ self.genwalls.append((RandomLevel.startplatform,))
+ self.genwalls.append((RandomLevel.generate_wind, ))
+
+
+Levels = []
+for i in range(25):
+ class level(RandomLevel):
+ auto = 1
+ Levels.append(level)
+
+class levelfinal(RandomLevel):
+ genwalls = [(RandomLevel.platforms,(4,3),(lambda:flat(1,1),lambda:flat(4,2)),1)]
+Levels.append(levelfinal)
diff --git a/bubbob/levels/scratch.py b/bubbob/levels/scratch.py
new file mode 100644
index 0000000..a958a28
--- /dev/null
+++ b/bubbob/levels/scratch.py
@@ -0,0 +1,2301 @@
+#
+# An experimental level set.
+#
+
+import boarddef
+from boarddef import LNasty, LMonky, LGhosty, LFlappy
+from boarddef import LSpringy, LOrcy, LGramy, LBlitzy
+from boarddef import RNasty, RMonky, RGhosty, RFlappy
+from boarddef import RSpringy, ROrcy, RGramy, RBlitzy
+
+
+class level01(boarddef.Level):
+ a = LNasty
+ b = RNasty
+
+ walls = """
+####################################
+## ##
+## ##
+## ##
+## ##
+## ##
+## a a b b ##
+#### ###################### ####
+## ##
+## ##
+## ##
+## a a b b ##
+#### ###################### ####
+## ##
+## ##
+## ##
+## a a b b ##
+#### ###################### ####
+## ##
+## ##
+## ##
+## a a b b ##
+#### ###################### ####
+## ##
+## ##
+## ##
+## ##
+####################################
+"""
+
+ winds = """
+>> <<
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>> <<
+"""
+
+
+class level02(boarddef.Level):
+ letter = 1
+ lightning = 1
+ top = 0
+
+ a = LNasty
+ b = RNasty
+
+ walls = """
+## ##
+## ##
+##b ##
+############## ##############
+## a ##
+############# #############
+##b ##
+############ ############
+## a ##
+########### ###########
+##b ##
+########## ##########
+## a ##
+######### #########
+##b ##
+######## ########
+## a ##
+####### #######
+##b ##
+###### ## ######
+## a ##
+##### ### ### #####
+## ##
+#### #### #### #### ####
+## ##
+### ######## ###
+## ##
+############ ############
+""" # [] [] [] # """
+
+ winds = """
+>> <<
+>>x<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>x<<
+>> <<
+>> <<
+>>>>>>>>>>>>>>^ ^<<<<<<<<<<<<<<
+>> <<
+>>>>>>>>>>>>>^ ^<<<<<<<<<<<<<
+>> <<
+>>>>>>>>>>>>^ ^<<<<<<<<<<<<
+>> <<
+>>>>>>>>>>>^ ^<<<<<<<<<<<
+>> <<
+>>>>>>>>>>^ ^<<<<<<<<<<
+>> <<
+>>>>>>>>>^ ^<<<<<<<<<
+>> <<
+>>>>>>>>^ ^<<<<<<<<
+>> <<
+>>>>>>>^ ^<<<<<<<
+>> <<
+>>>>>>^ ^<<<<<<
+>> <<
+>>>>>^ ^<<<<<
+>> <<
+>>>>^ ^<<<<
+>> <<
+>>>^ ^<<<
+>> <<
+"""
+
+
+class level03(boarddef.Level):
+ letter = 1
+ fire = 0
+ lightning = 0
+ water = 1
+ top = 1
+
+ a = LNasty, RNasty
+ b = ()
+ c = LMonky, RMonky
+
+ walls = """
+## ##
+## ##
+## # # ##
+## b ##
+## # ##
+## a # # ##
+## # # ##
+## ##
+## b a b ##
+## # # # ##
+## c ##
+## # # c ##
+## # b # ##
+## ##
+## # # # ##
+## ##
+## # # ##
+## # a # ##
+## # b # ##
+## ##
+## c # ##
+## # # b # ##
+## # ##
+## # ##
+## # ##
+## # # ##
+## # ##
+##### # #####
+""" # [] [] [] # """
+
+ winds = """
+>> <<
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>> <<
+"""
+
+
+class level04(level03):
+ a = LNasty, RNasty
+ b = LGhosty, RGhosty
+ c = ()
+
+class level05(level03):
+ a = LSpringy, RSpringy
+ b = LSpringy, RSpringy
+ c = ()
+
+
+
+class level06(boarddef.Level):
+ letter = 1
+ fire = 0
+ lightning = 1
+ water = 0
+ top = 0
+
+ a = LSpringy
+ b = RSpringy
+ c = LBlitzy
+
+ walls = """
+## ############## ##
+## ##
+## ##
+## b bcbcbcb bb a a acacaca a ##
+################ ################
+## # # ##
+## # # ##
+## # # ##
+## # # ##
+## # # ##
+## # # ##
+## # # ##
+## # # ##
+## # # ##
+## # # ##
+## ########### ########### ##
+## bababa ##
+## ########################## ##
+## ##
+## ##
+## ##
+########## ##########
+## ##
+## ##
+## ##
+## ##
+## ##
+###### ############## ######
+""" # [] [] [] # """
+
+ winds = """
+>> <<
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>>>>>>>>>>>>>>>^^^^<<<<<<<<<<<<<<<<
+>>^ ^<<
+>>^ ^<<<<<<<<<<<<<>>>>>>>>>>>>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>> <<
+"""
+
+
+class level07(boarddef.Level):
+ letter = 0
+ fire = 0
+ lightning = 0
+ water = 0
+ top = 1
+
+ a = LSpringy
+ b = RSpringy
+
+ walls = """
+############ ############# ####
+### ##
+### b ##
+## b ##
+#### b ##
+## # b ##
+## #b ##
+## # ###### ##
+## # b ##
+## # b ##### ##
+## # b ##
+## # b ##
+## # b ##
+## ### ##
+## # ##
+## # b ##
+## # b ##
+## # b ##
+## # b ##
+## #b ##
+## # ##
+## # ##
+## # ##
+## # ##
+## # ##
+## # ##
+## # ##
+############ ############# ####
+""" # [] [] [] # """
+
+ winds = """
+>> <<
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>v v<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvv v<<<<<<<<<<<<<<<<<<<<<<<<<<<
+>>vvvvv v<<<<<<<<<<<<<<<<<<<<<<<<<<<
+>>>>>>v v<<<<<<<<<<<<<<<<<<<<<<<<<<<
+>>> v v<<<<<<<<<<<<<<<<<<<<<<<<<<<
+>>> v v<<<<<<<<<<<<<<<<<<<<<<<<<<<
+>>> v v<<<<<<<<<<<<<<<<<<<<<<<<<<<
+>>> xxx v<<<<<<<<<<<<<<<<<<<<<<<<<<<
+>>> v<<<<<<<<<<<<<<<<<<<<<<<<<<<
+>>> <<<<<<<<<<<<<<<<<<<<<<<<<<<
+>>> <<<<<<<<<<<<<<<<<<<<<<<<<<<
+>>> <<<<<<<<<<<<<<<<<<<<<<<<<<<
+>>> ^<<
+>vv ^<<
+>vv ^<<
+>vv ^<<
+>vv ^<<
+>vv ^<<
+>vv ^<<
+>vv ^<<
+>vv ^<<
+>vvvvvvvvvvvvvvvv ^<<
+>vvvvvvvvvvvvvvvv ^<<
+>vvvvvvvvvvvvvvvv ^<<
+>vvvvvvvvvvvvvvvv ^<<
+"""
+
+
+class level08(boarddef.Level):
+ letter = 1
+ fire = 1
+ lightning = 1
+ water = 0
+ top = 0
+
+ a = b = c = d = e = LFlappy
+
+ walls = """
+## ##
+## ##
+#### ## ## ## ## ## ####
+## ##
+## # # # # # # ##
+## # # # # # # ##
+## a b c d e ##
+#### ## ## ## ## ## ####
+## ##
+## # # # # # # ##
+## # # # # # # ##
+## e d c b a ##
+#### ## ## ## ## ## ####
+## ##
+## # # # # # # ##
+## # # # # # # ##
+## a b c d e ##
+#### ## ## ## ## ## ####
+## ##
+## # # # # # # ##
+## # # # # # # ##
+## e d c b a ##
+#### ## ## ## ## ## ####
+## ##
+## # # # # # # ##
+## # # # # # # ##
+## a b c d e ##
+#### ## ## ## ## ## ####
+""" # [] [] [] # """
+
+ winds = """
+>> <<
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>> <<
+"""
+
+
+class level09(level08):
+
+ a = ()
+ b = RGhosty
+ c = RBlitzy
+ d = ()
+ e = LBlitzy
+
+
+class level10(boarddef.Level):
+ top = 1
+
+ a = RSpringy
+ b = LSpringy
+ c = LSpringy
+ d = RSpringy
+ e = LSpringy
+
+ walls = """
+## # # # # # # ##
+#### ## ## ## ## ## ####
+## ##
+## # # # # # # ##
+## a b c d e ##
+#### ## ## ## ## ## ####
+## ##
+## # # # # # # ##
+## # # # # # # ##
+## e d c b a ##
+#### ## ## ## ## ## ####
+## ##
+## # # # # # # ##
+## a b c d e ##
+#### ## ## ## ## ## ####
+## ##
+## # # # # # # ##
+## e d c b a ##
+#### ## ## ## ## ## ####
+## ##
+## # # # # # # ##
+## # # # # # # ##
+## ##
+#### ## ## ## ## ## ####
+## ##
+## # # # # # # ##
+## ##
+#### ## ## ## ## ## ####
+""" # [] [] [] # """
+
+ winds = """
+>> <<
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>> <<
+"""
+
+
+
+class level11(boarddef.Level):
+ letter = 0
+ fire = 0
+ lightning = 1
+ water = 0
+ top = 0
+
+ a = LSpringy
+ b = RFlappy
+ c = RSpringy
+
+ walls = """
+####################################
+## ##
+## ##
+## ##
+## ##
+## ##
+## b b b b b ##
+## ##
+## b b b b b b ##
+## ##
+## ##
+## ##
+## ##
+####################################
+## ##
+## ##
+## ##
+## ##
+## ##
+## a ##
+## c ##
+## a ##
+## c ##
+## a ##
+## c ##
+## a ##
+## c ##
+####################################
+""" # [] [] [] # """
+
+ winds = """
+>> <<
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>> <<
+"""
+
+
+
+class level12(level11):
+ letter = 0
+ fire = 1
+ lightning = 0
+ water = 0
+ top = 1
+
+ a = LGhosty
+ b = RFlappy
+ c = RGhosty
+
+
+class level13(boarddef.Level):
+ letter = 1
+ fire = 0
+ lightning = 1
+ water = 0
+ top = 1
+
+ a = LFlappy
+ b = RFlappy
+
+ walls = """
+########## ###### ##########
+## ###### ##
+## ###### ##
+## ###### ##
+## ###### ##
+## a ###### b ##
+## ###### ##
+## b ###### a ##
+## ###### ##
+## a ###### b ##
+## ###### ##
+## b ###### a ##
+## ###### ##
+## a ###### b ##
+## ##
+## b ###### a ##
+## ###### ##
+## a ###### b ##
+## ###### ##
+## ###### ##
+## ###### ##
+## ###### ##
+## ###### ##
+## ###### ##
+## ###### ##
+## ###### ##
+## ###### ##
+########## ###### ##########
+""" # [] [] [] # """
+
+ winds = """
+>> <<
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>> <<<
+>>> <<<
+>>> <<<
+>>> <<<
+>>> <<<
+>>> <<<
+>>> <<<
+>>> <<<
+>>> <<<
+>>> <<<
+>>> <<<
+>>> <<<
+>>> xxxx <<<
+>>> <<<
+>>> <<<
+>>> <<<
+>>> <<<
+>>> <<<
+>>> <<<
+>>>>>>>>>>>>>>> <<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>> <<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>> <<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>> <<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>> <<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>> <<<<<<<<<<<<<<<<
+>> <<
+"""
+
+
+class level14(boarddef.Level):
+ letter = 0
+ fire = 0
+ lightning = 0
+ water = 0
+ top = 0
+
+ a = RSpringy
+ b = ()
+
+ walls = """
+####################################
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+###a # # #a # ##
+## # # #a # #a # # # # ##
+## # # # # # # # # # ##
+## ## ## ## ## ###
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+####################################
+""" # [] [] [] # """
+
+ winds = """
+>> <<
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>> <<
+"""
+
+
+class level15(boarddef.Level):
+ letter = 0
+ fire = 0
+ lightning = 0
+ water = 1
+ top = 0
+
+ f = LFlappy
+ b = RBlitzy
+ a = LOrcy, RMonky
+ g = RGramy
+ h = LGhosty
+ n = LNasty
+ s = LSpringy
+
+ walls = """
+############# ###### f f #########
+## a #b f f f ##
+## # ###### #### #######
+## # a a # # ##
+## ####### ## # # g ##
+## # # # # ##
+## # # ##
+## # # ##
+## # # ##
+## # # ##
+## # n n # n # ##
+## # #### ### ##
+## # # ##
+## s # # ##
+## s # h # ##
+## # # s # h # ##
+## # s # # ##
+## # #### ## # ##
+## # # ##### ### ## ##
+## # # ##
+## # b # ##
+## #### ##
+## ##
+## ##
+## ###### ##
+## # # ##
+## # # ##
+############# ###### f #########
+""" # [] [] [] # """
+
+ winds = """
+>> <<
+>>>>>>>>>>>>>>>>>>vvvv<<<<<<<<<<<<<<
+>>^ vvvv x<<
+>>^ >vvvv< ^<<
+>>^ > xxxx < ^<<
+>>^ >>>>vvv<<< ^<<
+>>^ vv< >>>>>>>>>^<<
+>>^ xx ^ ^<<
+>>^ ^ ^<<
+>>^ ^ ^<<
+>>^ >^ ^<<
+>>^ ^< >^ ^<<
+>>^ ^<<<<<<<>>>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^< ^<<
+>>^ ^< xx ^<<
+>>^ ^ ^^<<<<<<<<<<<< ^<<
+>>^ ^ ^ ^<<
+>>^ ^< >^ ^<<
+>>^ ^<<>>^ ^<<
+>>^ ^<<
+>>^ ^^^^^^ ^<<
+>>^ ^<>vvvv<>^ ^<<
+>>^ ^<>vvvv<>^ ^<<
+>> ^<>vvvv<>^ <<
+"""
+
+
+class level16(boarddef.Level):
+ letter = 1
+ fire = 1
+ lightning = 1
+ water = 0
+ top = 1
+
+ a = LGhosty, RGhosty
+
+ walls = """
+## # ##
+## ## ## ##
+## a a ###### ##
+## ###### # ##
+## ######### ##
+## ############ ##
+## # # ########## a ##
+## a ### ######## ##
+## #### ### ####### ##
+## ######### ### a ##
+## ######### ### # ##
+## ############## ##
+## ############## ##
+## ####### ###### ##
+## ###### ###### a ##
+## ##### ##### ##
+## #### #### a ##
+## ##### ### ##
+## ########### a ##
+## a ### #### ##
+## ## ###### ##
+## ## ##### ##
+## #### ## ##
+## ####### ##
+## ##### ##
+## # #### ##
+## ##
+####################################
+""" # [] [] [] # """
+
+ winds = """
+>> <<
+>>>>>>>>vvvvvvvvvvvvvvvvvvvv<<<<<<<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>> <<
+"""
+
+
+class level17(boarddef.Level):
+ letter = 1
+ fire = 1
+ lightning = 0
+ water = 1
+ top = 0
+
+ a = LGhosty, RFlappy
+
+ walls = """
+## ##### # ##
+## ######## # ##
+## ####### a # # ##
+## ####### # # ##
+## ###### a ## # ##
+## ##### ## # ##
+## ###### # a ### # ####
+## ##### # # ####
+## ########### ####### # #####
+## #### ##### # ## #### # #####
+## #### ###### ### ### # # ###
+## ## # ## # ## ###### # ####
+## ### # ### ## # ####
+## ### ## #### # ## ####
+## ### a ## ### # # ###
+## ### ## ### ## # ###
+## ### ## ### a ## # ###
+## #### # ## ### # ###
+## #### # ### ### # ##
+## ### ### #### # ##
+## #### a ## # #### ##
+## ### ####### ###
+### ### ############ ## # ###
+### ## ########### #### ##
+### ### ### ########## ##
+### ### ################ ##
+## ## ############# ##
+### # ####### ##
+"""
+
+
+class level18(boarddef.Level):
+ letter = 1
+ fire = 0
+ lightning = 0
+ water = 1
+ top = 0
+
+ b = LBlitzy, RBlitzy
+ a = LOrcy
+
+ walls = """
+################# #################
+## # ## ## # ##
+## # ## ## # ##
+## # b b ## ## b b # ##
+## # ####### ####### # ##
+## #b # a #b # ##
+## ## #### ## ##
+## # a # ##
+## # # ##
+## a ##
+## #### ##
+## #a # ##
+## # # ##
+## a ##
+## #### ##
+## # a # ##
+## # # ##
+## a ##
+## #### ##
+## #a # ##
+## # # ##
+## a ##
+## #### ##
+## # a # ##
+## ## ## ##
+## ## ## ##
+## ## ## ##
+################# #################
+""" # [] [] [] # """
+
+ winds = """
+>> <<
+>>>>>>>>>>>>>>>>>^^<<<<<<<<<<<<<<<<<
+>>^ ^<<
+>>^ ^<<
+>>^ > ^^ < ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>> <<
+"""
+
+
+class level19(boarddef.Level):
+ letter = 0
+ fire = 0
+ lightning = 0
+ water = 0
+ top = 0
+
+ a = LNasty
+ b = LMonky
+ c = LOrcy
+ d = LGramy
+
+ walls = """
+####################################
+####################################
+####################################
+####################################
+####################################
+####################################
+####################################
+####################################
+## ##
+## # # ##
+## # # ##
+## # # # # ##
+## # # # # ##
+## # # # # ##
+## # # # # # # ##
+## # # # # # # ##
+## # # # # # # ##
+## # # # # # # ##
+## # # # # # # # # ##
+## # # # # # # # # ##
+## # # # # # # # # ##
+## # # # # # # # # ##
+## # # # # # # # # ##
+## # # # # # # # # ##
+## # # # # # # # # ##
+## # # # # # # # # ##
+## #a #b #c #d #c #b #a # ##
+####################################
+""" # [] [] [] # """
+
+ winds = """
+>>xxxxxxxxx>>>>xxxxxx<<<<xxxxxxxxx<<
+>>^ >>> ^ ^<<
+>>^ ^ <<< ^<<
+>>^ ^<<
+>>^ >> << ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>> <<
+"""
+
+
+class level20(boarddef.Level):
+ letter = 1
+ fire = 0
+ lightning = 0
+ water = 0
+ top = 1
+
+ a = RGhosty
+ b = RNasty
+ c = LSpringy
+ d = LFlappy
+
+ walls = """
+## ## # # # ##
+## # # ##
+## # # ##
+## # # ##
+## # # ##
+## ### # # ## ##
+## # # ##
+## # # ##
+## # # ##
+## # # ##
+## ## # # # ### ##
+## # # ##
+## # a d d d # ##
+## # a # ##
+## # a c # ##
+## # c # ##
+## # b b b c # ##
+## ### # # # ## ##
+## # # ##
+## # # ##
+## # # ##
+## # # ##
+## ## # # ### ##
+## # # ##
+## # # ##
+## # # ##
+## # # ##
+################ # # ###############
+""" # [] [] [] # """
+
+ winds = """
+>>v v< v v v>v v<<
+>>v v<v>vxvv>v v<<
+>>v v<vvvvvv>v v<<
+>>v v<vvvvvv>v v<<
+>>v v<>v>v<v>v v<<
+>>v v<< v x <>>v v<<
+>>v v<vvvvvvvv>v v<<
+>>v v<vvvvvvvv>v v<<
+>>v v<vvvvvvvv>v v<<
+>>v v<v<v<v<v<>v v<<
+>>v v<<v x x v >>v v<<
+>>v v<vvvvvvvvvv>v v<<
+>>v v<vvvvvvvvvv>v v<<
+>>v v<vvvvvvvvvv>v v<<
+>>v v<vvvvvvvvvv>v v<<
+>>v v<vvvvvvvvvv>v v<<
+>>v v<>>v<v<v<<<>v v<<
+>>v v<< v x x v>>v v<<
+>>v v<v>>vvvvv>v v<<
+>>v v<vvvvvvvv>v v<<
+>>v v<vvvvvvvv>v v<<
+>>v v<>v>v<v<<>v v<<
+>>v v<<v x < >>v v<<
+>>v v<vvvvvv>v v<<
+>>v v<vvvvvv>v v<<
+>>v v<vvvvvv>v v<<
+>>v v<>v>v<v>v v<<
+>> v< v v v>v <<
+"""
+
+
+class level21(boarddef.Level):
+ letter = 0
+ fire = 0
+ lightning = 0
+ water = 0
+ top = 0
+
+ s = LSpringy, LSpringy
+ f = LFlappy, LFlappy
+ g = RGhosty, RGhosty
+
+ walls = """
+########### ## ##########
+## ## s ## ##
+## ##f ## ## ##
+## ## ## ## ##
+## ## ## s ## ##
+## # ## ##
+## ## s ## ##
+## ## #### ##
+## ## ## ## ##
+## ## s ## ## ##
+## ## ## ## ##
+## ## ## ## ##
+## ## s ## ## ##
+## ## ## ## ##
+## ## ## ## ##
+## ##s g ## ##
+## ## ## ## ##
+## ## ## ## ##
+## ## ## f ## ##
+## # ## ##
+## ## g ## ##
+## ## #### ##
+## ## ## ## ##
+## ## f ## ## ##
+## ## ## ## ##
+## ## ## ## ##
+## ## g ## ## ##
+########### ## ##########
+""" # [] [] [] # """
+
+
+class level22(level21):
+ water = 1
+ letter = 1
+
+ s = RBlitzy, RBlitzy
+ f = LOrcy, LOrcy
+ g = LGramy, LGramy
+
+
+class level23(boarddef.Level):
+ letter = 1
+ fire = 1
+ lightning = 1
+ water = 1
+ top = 0
+
+ a = LNasty
+ b = RMonky
+ c = LGramy
+ d = ROrcy
+ s = LSpringy
+
+ walls = """
+## ##
+## ### ####### ############## # ##
+## # # a # b # # # ##
+## # ### # ### # ##### # ### # ##
+## #d # # # # # # c # # # ##
+## ### # # # ### # ##### ### # ##
+## # # # # s # # d # # ##
+## ### # # ### #### ####### ### ##
+## #c # # # # a # # ##
+## ### # ### # ########## ### # ##
+## # # # # # # # b # c # # ##
+## # ### # ### # ### ### ### # ##
+## # b # #s # # # ##
+## # ####### # ######## # ##### ##
+## # # a # # # a # d # ##
+## ### ##### ### # ##### ### # ##
+## # d # # # b # # # ##
+## ### # ####### # ##### # ### ##
+## # # # cs # # # # ##
+## ### ### ######## # ### ### # ##
+## # # # b # # #c # # ##
+## # ### # ##### ##### # ### # ##
+## # d # # # # # # ##
+## ### # # # # ########## ### # ##
+## # # # # # a # # # ##
+## # ####### ################ # ##
+## ##
+####################################
+"""
+
+
+class level24(boarddef.Level):
+ letter = 0
+ fire = 0
+ lightning = 0
+ water = 0
+ top = 0
+
+ n = LOrcy
+ a = RGhosty, LFlappy
+
+ walls = """
+####################################
+## ## ## # # # # # # # ### ##
+## ## # # # # # # # ## # # # ##
+## ## ### # #a # n # # # ##
+## ## # # a #a # ## # ### ##
+## ## # a # ### ##
+###### # # #a # ########
+###### n #a # # ######
+##### # # # #######
+####### # n #####
+#### # # #######
+##### #a ####
+##### # # # #######
+##### # # ####
+#### # # #####
+###### # # #####
+#### # # ### ####
+####### # ######
+##### # # # # #####
+##### ## # # ######
+####### # # # # # #######
+####### # # # # ######
+######## # # # # # # ########
+######## ## # # # # #########
+########## # # # # # ## ##########
+## ###### # # # # # ###### ##
+## ##
+####################################
+"""
+
+ winds = """
+>> <<
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>^ > xx < ^<<
+>>^ > xx < ^<<
+>>^ > xxx x < ^<<
+>>^ > xxx xxx < ^<<
+>>^ > x x< ^<<
+>>^ >x ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>> <<
+"""
+
+
+class level25(boarddef.Level):
+ letter = 0
+ fire = 0
+ lightning = 0
+ water = 0
+ top = 0
+
+ b = RGhosty
+ a = LGhosty
+
+ walls = """
+####################################
+## ##
+## ##
+## ####################### ##
+## # ##
+## a a a a a a # ##
+## ################## # ##
+## # # # ##
+## # a a a a a a #b # ##
+## # ########## # ## ##
+## # # # # # ##
+## #b #a a a #b # # ##
+## ## # ##### ## # ##
+## # # # # ##
+## # # b b b b #b # ##
+## # ########### ## ##
+## # # # ##
+## #b b b b b b # ##
+## ###### b ###### ##
+## ####### ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+####################################
+""" # [] [] [] # """
+
+
+class level26(boarddef.Level):
+ letter = 0
+ fire = 0
+ lightning = 0
+ water = 0
+ top = 0
+
+ n = LNasty
+ m = RNasty
+ a = LGramy
+ g = LGhosty
+
+ walls = """
+## ####### ####### ##
+## ### ########## ### ##
+## g ## ## g ##
+##n m n m n ########## m n m n m ##
+############### ###############
+## ################ ##
+## #### #### ##
+## ############ ##
+## ###### ## ## ###### ##
+## ### ######### ### ##
+## ### # ## ### ##
+##nmnmnm ### ########## ###mnmnmn ##
+###############nmnmn ###############
+##### ################ #####
+############# #############
+## ## ## ## ## ##
+## ####### ####### ##
+## # ################ # ##
+## ########### ########### ##
+## # # ############ # # ##
+## ####### ## ## ####### ##
+## # m n # ######## # n m # ##
+## ####### # # ####### ##
+## ######## ##
+## # m # ##
+## ######## ##
+## ##
+####################################
+""" # [] [] [] # """
+
+
+class level27(boarddef.Level):
+ letter = 1
+ fire = 1
+ lightning = 1
+ water = 0
+ top = 0
+
+ n = LNasty
+ m = RMonky
+
+ walls = """
+########### ###### # ##
+## # # ##
+## ######### # # n ##
+## # ###########
+## # nnn ##
+## ######### ##
+## ##
+## nnn # ##
+## ####### ## # ##
+## # # ##
+## # # ##
+## #nnn mmmm # # m ##
+## ##### ###### ###########
+## ##
+## ##
+## ####### ##
+## ### # ##
+## # # ##
+## nnn # # n ##
+## ###### ###########
+## ##
+## #mmmmmm ##
+## ######## ##
+## ##
+## ######## ### # ##
+## # # ##
+## nnn # # ##
+########### ###### ###########
+""" # [] [] [] # """
+
+
+class level28(boarddef.Level):
+ letter = 0
+ fire = 0
+ lightning = 0
+ water = 1
+ top = 1
+
+ m = LMonky
+ g = LGramy
+ h = LGhosty
+ f = LFlappy
+ b = LSpringy
+ a = RSpringy
+
+ walls = """
+## # # ##
+## b # #a ##
+## b # #a ##
+## ##### ##### ##
+## # # ##
+## # # ##
+## # # ##
+## # # ##
+## ##
+## ##
+## ##
+## # # ##
+###### ##### # # # # # ##
+## m m m # # ##
+## ######### # # h f h f ##
+## g g g # # ##
+####### ###### # h f ##
+## m m m # # f ##
+## ########### # # h h h ##
+## g g g # # f f ##
+######## ####### # # # # # # # ##
+## ##
+## ##
+## ##
+####### ##### ## ##### #######
+## # ## # ##
+## # # ##
+################# #################
+""" # [] [] [] # """
+
+ winds = """
+>>v v<<
+>>v v<<
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>> <<
+"""
+
+class level29(boarddef.Level):
+ letter = 0
+ fire = 0
+ lightning = 0
+ water = 0
+ top = 0
+
+ m = LMonky, RMonky
+ g = LGramy, RGramy
+ o = LOrcy, ROrcy
+ n = LNasty, RNasty
+
+ walls = """
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+################# #################
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## # m g o nn o g m # ##
+## ######################## ##
+## ##
+""" # [] [] [] # """
+
+ winds = """
+vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
+vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
+vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
+vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
+>> <<
+>> <<
+>> <<
+>> <<
+>> <<
+>> <<
+>> <<
+>> <<
+>>xxxxxxxxxxxxxxx xxxxxxxxxxxxxxx<<
+>>>>>>v>>>>v>>>>v<>v<<<<v<<<<v<<<<<<
+>>>>>>v>>>>v>>>>v<>v<<<<v<<<<v<<<<<<
+>>>>>>v>>>>v>>>>v<>v<<<<v<<<<v<<<<<<
+>>>>>>v>>>>v>>>>v<>v<<<<v<<<<v<<<<<<
+>>>>>>v>>>>v>>>>v<>v<<<<v<<<<v<<<<<<
+>>>>>>v>>>>v>>>>v<>v<<<<v<<<<v<<<<<<
+>>>>>>v>>>>v>>>>v<>v<<<<v<<<<v<<<<<<
+>>>>>>v>>>>v>>>>v<>v<<<<v<<<<v<<<<<<
+>>>>>>v>>>>v>>>>v<>v<<<<v<<<<v<<<<<<
+>>>>>>v>>>>v>>>>v<>v<<<<v<<<<v<<<<<<
+>>>>>>v>>>>v>>>>v<>v<<<<v<<<<v<<<<<<
+>>>>>>v>>>>v>>>>v<>v<<<<v<<<<v<<<<<<
+>>>>>>x>>>>v>>>>v<>v<<<<v<<<<x<<<<<<
+vvvvvvxxxxxxxxxxxxxxxxxxxxxxxxvvvvvv
+vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
+"""
+
+class level29(boarddef.Level):
+ letter = 0
+ fire = 0
+ lightning = 0
+ water = 0
+ top = 1
+
+ a = LNasty
+ b = RNasty
+ g = RGhosty
+ r = RGramy
+ m = LMonky
+
+ walls = """
+## ############################
+## ##
+##### ##
+##### ##
+##### ##
+##### ##
+##### # ##
+##### a b a ba ab a ba # ##
+################################ ##
+## ##
+## ##
+## ## g ##
+## ## ##
+## ## ##
+## ## ##
+## ## ##
+## ## br a rb a rb a r a ##
+## #############################
+## ## ##
+## ## ## ## ##
+##### ## mm ##mm ## ##
+############################## ##
+##### ## ## ## ##
+##### ## ## ##
+##### ##
+## ##
+## ##
+## ################################
+""" # [] [] [] # """
+
+ winds = """
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv ^<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv ^<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv ^<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv ^<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv ^<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv ^<<
+>>vv>>>>>>>>>>>>>>>>>>>>>>>>>>>> v<<
+>>vv <<
+>>vv <<
+>>vv vvvvvvvvvvvvvvvvvvvvvv <<
+>>vv vvvvvvvvvvvvvvvvvvvvvv <<
+>>vv vvvvvvvvvvvvvvvvvvvvvv <<
+>>vv vvvvvvvvvvvvvvvvvvvvvv <<
+>>vv vvvvvvvvvvvvvvvvvvvvvv <<
+>>vv vvvvvvvvvvvvvvvvvvvvvv <<
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>> <<
+"""
+
+
+class level30(boarddef.Level):
+ letter = 1
+ fire = 1
+ lightning = 1
+ water = 0
+ top = 1
+
+ a = RNasty
+
+ walls = """
+####################################
+####################################
+## ##### ## ##### ##
+## a ##### a ## a ##### a ##
+####################################
+####################################
+####################################
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+###### ######
+## ##
+## ##
+## ##
+## ##
+####################################
+""" # [] [] [] # """
+
+ winds = """
+>> <<
+>> <<
+>>>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx<<<
+>>>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx<<<
+>>>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx<<<
+>>>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx<<<
+>>> <<<
+>>>>>>>>>>>>>>>>>vv<<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>>vv<<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>>vv<<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>>vv<<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>>vv<<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>>vv<<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>>vv<<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>>vv<<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>>vv<<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>>vv<<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>>vv<<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>>vv<<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>>vv<<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>>vv<<<<<<<<<<<<<<<<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>> <<
+"""
+
+
+class level31(level30):
+ fire = 0
+ lightning = 0
+
+
+class level32(boarddef.Level):
+ n = LNasty
+ m = LMonky
+ o = LOrcy
+ g = LGramy
+ h = LGhosty
+ f = LFlappy
+ s = LSpringy
+ b = LBlitzy
+
+ walls = """
+#################### ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ########
+## ##
+## ##
+## # ##
+## ####### ##
+## ##
+## ##
+## # ##
+## ####### ##
+## ##
+## ##
+## ##
+## # # ##
+## # # # ##
+## # # # n ##
+## # # ######
+## # # # ##
+## # # # n n ##
+## # #########
+## # ##
+## # ##
+#################### ##
+""" # [] [] [] # """
+
+ winds = """
+>>>>>>>>>>>>>>>>xxxx<<vvvvvvvvvvvv>v
+>>^ ^<<vvvvvvvvvv>v
+>>^ ^<<vvvvvvvv>v
+>>^ ^<<vvvvvv>v
+>>^ ^<<vvvv>v
+>>^ ^<<<<<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ vvvvvvvvvvvvvv>v
+>>^ vvvvvvvvvvvvvv>v
+>> vvvvvvvvvvvvvv>v
+"""
+
+
+class level33(boarddef.Level):
+ m = LMonky, RMonky
+ g = LGramy, RGramy
+ o = LOrcy, ROrcy
+ n = LNasty, RNasty
+
+ walls = """
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+################# #################
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ##
+## ## m g o nn o g m ## ##
+## ######################## ##
+## ##
+""" # [] [] [] # """
+
+ winds = """
+v<vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv>v
+v<vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv>v
+v<vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv>v
+v<vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv>v
+>> <<
+>> <<
+>> <<
+>> <<
+>> <<
+>> <<
+>> <<
+>> <<
+>>xxxxxxxxxxxxxxx xxxxxxxxxxxxxxx<<
+>>>>>>v>>>>v>>>>v<>v<<<<v<<<<v<<<<<<
+>>>>>>v>>>>v>>>>v<>v<<<<v<<<<v<<<<<<
+>>>>>>v>>>>v>>>>v<>v<<<<v<<<<v<<<<<<
+>>>>>>v>>>>v>>>>v<>v<<<<v<<<<v<<<<<<
+>>>>>>v>>>>v>>>>v<>v<<<<v<<<<v<<<<<<
+>>>>>>v>>>>v>>>>v<>v<<<<v<<<<v<<<<<<
+>>>>>>v>>>>v>>>>v<>v<<<<v<<<<v<<<<<<
+>>>>>>v>>>>v>>>>v<>v<<<<v<<<<v<<<<<<
+>>>>>>v>>>>v>>>>v<>v<<<<v<<<<v<<<<<<
+>>>>>>v>>>>v>>>>v<>v<<<<v<<<<v<<<<<<
+>>>>>>v>>>>v>>>>v<>v<<<<v<<<<v<<<<<<
+>>>>>>v>>>>v>>>>v<>v<<<<v<<<<v<<<<<<
+>>>>>>x>>>>v>>>>v<>v<<<<v<<<<x<<<<<<
+v<vvvvxxxxxxxxxxxxxxxxxxxxxxxxvvvv>v
+v<vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv>v
+"""
+
+
+class level34(boarddef.Level):
+ letter = 0
+ fire = 0
+ lightning = 0
+ water = 0
+ top = 1
+
+ a = RBlitzy
+ b = LBlitzy
+
+ walls = """
+## # ## # ## # ## # ##
+## ## # ba # ## ##
+## ### b#### #### ### ##
+## # # # # b # # ##
+## #### # # #### ##
+## #######b # # ####### ##
+## # # #b # ##
+## # # # # ##
+## # # # # ##
+## #b # # # ##
+## # # #b # ##
+## # # ##
+## # # # # ##
+## #b # # # ##
+## # # #b # ##
+## # # # # ##
+## # # # # ##
+## #b # # # ##
+## # # #b # ##
+## # # # # ##
+## # # # # ##
+## #b # # # ##
+## # # #b # ##
+## ####### # # ####### ##
+## #### # # #### ##
+## #### #### #### #### ##
+## #### b #### ##
+## ## # # ## ##
+""" # [] [] [] # """
+
+ winds = """
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>xxxxxvvvvvvvvvvvvvvvvvvvvvvxxxxx<<
+"""
+
+
+class level35(boarddef.Level):
+ s = LNasty
+ o = LNasty, LNasty, RNasty, RNasty
+ a = RBlitzy
+ b = LBlitzy
+
+ walls = """
+####################################
+## ###s ### ##
+##s ## b ######## a ##s ##
+####################################
+## ##
+## ##
+## ##
+## ## ##
+## #### ##
+## ######## ##
+## ############ ##
+## ###### ##
+## ########## ##
+## ################ ##
+## ########## ##
+## ############## ##
+## ################## ##
+## ## ##
+## ## ##
+## #### ##
+## ###### ##
+## ## ## ## ##
+## ## ## ## ##
+## ##
+## ##
+## ##
+## #o o o o o o o o o o o o # ##
+####################################
+""" # [] [] [] # """
+
+ winds = """
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>>>>>>>>>>>>>>> <<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>> <<<<<<<<<<<<<<<
+>>>>>>>>>>>>>> <<<<<<<<<<<<<<
+>>>>>>>>>>>>> <<<<<<<<<<<<<
+>>>>>>>>>>>> <<<<<<<<<<<<
+>>>>>>>>>>> <<<<<<<<<<<
+>>>>>>>>>> <<<<<<<<<<
+>>>>>>>>> <<<<<<<<<
+>>>>>>>> <<<<<<<<
+>>>>>>> <<<<<<<
+>>>>>> <<<<<<
+>>>>> <<<<<
+>>>> <<<<
+>>> <<<
+>>vv<<<<<<<<<<<<<<>>>>>>>>>>>>>>vv<<
+>>vv<<<<<<<<<<<<<<>>>>>>>>>>>>>>vv<<
+>>vv<<<<<<<<<<<<<<>>>>>>>>>>>>>>vv<<
+>>vv<<<<<<<<<<<<<<>>>>>>>>>>>>>>vv<<
+>>vv<<<<<<<<<<<<<<>>>>>>>>>>>>>>vv<<
+>>vv<<<<<<<<<<<<<<>>>>>>>>>>>>>>vv<<
+>>vv<<<<<<^^^^^^^^^^^^^^^^>>>>>>vv<<
+>>vv<<<<^^^^^^^^^^^^^^^^^^^^>>>>vv<<
+>>xx<<^^^^^^^^^^^^^^^^^^^^^^^^>>xx<<
+>>xx<<<<<<<<<<<<<<>>>>>>>>>>>>>>xx<<
+"""
+
+
+class level36(boarddef.Level):
+ s = LSpringy
+ b = LBlitzy
+
+ walls = """
+## b b ##
+## s ##
+## b b ##
+## s ##
+## b b ##
+## s ##
+## b b ##
+## s ##
+## b b ##
+## s ##
+## b b ##
+## s ##
+## b b ##
+## s ##
+## b b ##
+## s ##
+## b b ##
+## s ##
+## b b ##
+## s ##
+## b b ##
+## s ##
+## b b ##
+## s ##
+## b b ##
+## s ##
+## b b ##
+## s ##
+""" # [] [] [] # """
+
+ winds = """
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>>>>>>>>>>>>>>>xxxx<<<<<<<<<<<<<<<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>>^ ^<<
+>> <<
+"""
+
+
+class level37(boarddef.Level):
+ n = LNasty
+ m = LMonky
+
+ walls = """
+################# #################
+## m m m m m # ## #m m m m m ##
+## #n n n n n # #n n n n n # ##
+################# #################
+## ####### ####### ##
+## ################ ##
+## ############################ ##
+## # #### #### # ##
+## #### ##### ##### #### ##
+## #### ################ #### ##
+## #### ################ #### ##
+## ######### ######### ##
+## ### ############## ### ##
+## #### # ############ # #### ##
+## #### ############ #### ##
+############ #### ############
+############ ## ############
+## # ## ## # ##
+## ############ ############ ##
+## # ##### ##### # ##
+## ############################ ##
+## ############################ ##
+## ######## ######## ##
+######## ## ## ########
+######## # #### # ########
+## # #### # ##
+## ## ### ### ## ##
+###### ################ ######
+""" # [] [] [] # """
+
+ winds = """
+>>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<<
+>>xx<<<<<<<<<<<<<<>>>>>>>>>>>>>>xx<<
+>>xx^^^^^^^^^^^^^^^^^^^^^^^^^^^^xx<<
+v<vv>>>>vvvvvvvvvvvvvvvvvvvv<<<<vv>v
+v<vv>>>>vvvvvvvvvvvvvvvvvvvv<<<<vv>v
+v<vv>>>>vvvvvvvvvvvvvvvvvvvv<<<<vv>v
+v<vv>>>>vvvvvvvvvvvvvvvvvvvv<<<<vv>v
+v<vv>>>>vvvvvvvvvvvvvvvvvvvv<<<<vv>v
+v<vv>>>>vvvvvvvvvvvvvvvvvvvv<<<<vv>v
+v<vv>>>>vvvvvvvvvvvvvvvvvvvv<<<<vv>v
+v<vv>>>>vvvvvvvvvvvvvvvvvvvv<<<<vv>v
+v<vv>>>>vvvvvvvvvvvvvvvvvvvv<<<<vv>v
+v<vv>>>>vvvvvvvvvvvvvvvvvvvv<<<<vv>v
+v<vvvv<>vvvvvvvvvvvvvvvvvvvv<>vvvv>v
+v<vvvv>>vvvvvvvvvvvvvvvvvvvv<<vvvv>v
+v<vv>vvvvvvvvvvvvvvvvvvvvvvvvvv>vv>v
+v<vv>vvvvvvvvvvvvvvvvvvvvvvvvvv>vv>v
+v>vv<vvvvvvvvvvvvvvvvvvvvvvvvvv>vv<v
+v>vv<vvvvvvvvvvvvvvvvvvvvvvvvvv>vv<v
+v>vv<vvvvvvvvvvvvvvvvvvvvvvvvvv>vv<v
+v>vv<vvvvvvvvvvvvvvvvvvvvvvvvvv>vv<v
+v>vv<vvvvvvvvvvvvvvvvvvvvvvvvvv>vv<v
+v>vv<vvvvvvvvvvvvvvvvvvvvvvvvvv>vv<v
+v>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<v
+v>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<v
+v>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<v
+v>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<v
+v>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv<v
+"""
+
+class level38(boarddef.Level):
+ g = RGhosty
+
+ walls = """
+####################################
+## # ##
+## g # g ##
+## # ##
+## # g ##
+## # ##
+## ### ##### ## ##
+## ### ######## ## ##
+## #### ### ###### ##
+## ### ### ####### ### ##
+## ## ########## ##
+## # ######## ##
+## # g ##
+## g ########## ##
+## ########### ##
+## ########## ## ##
+## ############ ## ##
+## ### ############ ## ##
+## #### ########## ### ##
+## ##### ######## ## ##
+## #### ###### ##
+## ##### ##
+## ######## ##
+## ##
+## ##
+## ##
+## ##
+####################################
+"""
+
+
+class levelFinal(boarddef.Level):
+
+ walls = """
+################ ################
+## ##
+## ##
+## ### ### ##
+## ##
+## ### ### ##
+## ##
+## ### ### ##
+## ##
+## ### ### ##
+## ##
+##### #####
+## ##
+## ### ### ##
+## ##
+## ### ### ##
+## ##
+## ### ### ##
+## ##
+## ### ### ##
+## ##
+####################################
+## ##
+## ##
+## ##
+## ##
+## ##
+################ ################
+""" # [] [] [] # """
+# nb.: the previous line has no purpose
+# other than helping with wall alignment
+
+ winds = """
+>> ^^^^ <<
+>> ^^^^ <<
+>> ^^^^ <<
+>> ^^^^ <<
+>> ^^^^ <<
+>> ^^^^ <<
+>> ^^^^ <<
+>> ^^^^ <<
+>> ^^^^ <<
+>> ^^^^ <<
+>> ^^^^ <<
+>> ^^^^ <<
+>> ^^^^ <<
+>> ^^^^ <<
+>> ^^^^ <<
+>> ^^^^ <<
+>> ^^^^ <<
+>> ^^^^ <<
+>> ^^^^ <<
+>> ^^^^ <<
+>> ^^^^ <<
+>> ^^^^ <<
+>>>>>>>>>>>>>>>>^^^^<<<<<<<<<<<<<<<<
+>> ^^^^ <<
+>> ^^^^ <<
+>> ^^^^ <<
+>> ^^^^ <<
+>> ^^^^ <<
+"""
diff --git a/bubbob/macbinary.py b/bubbob/macbinary.py
new file mode 100644
index 0000000..616bce0
--- /dev/null
+++ b/bubbob/macbinary.py
@@ -0,0 +1,260 @@
+import struct
+
+
+def padto(n, m):
+ return (n+m-1) & ~(m-1)
+
+def resourceclass(rtype):
+ return globals().get(rtype.strip() + 'Resource', Resource)
+
+
+class TypeList:
+
+ def __init__(self, type, fmap, fdata, namebase, start, count):
+ self.type = type
+ self.fmap = fmap
+ self.fdata = fdata
+ self.namebase = namebase
+ self.start = start
+ self.count = count
+ self.ids = None
+
+ def resources(self):
+ if self.ids is None:
+ ResourceCls = resourceclass(self.type)
+ d = {}
+ self.fmap.seek(self.start)
+ for resid, resname, resattr, resofshi, resofslo in [
+ struct.unpack(">HHBBHxxxx", self.fmap.read(12))
+ for i in range(self.count)]:
+ if resname == 0xffff:
+ name = None
+ else:
+ self.fmap.seek(self.namebase + resname)
+ namelen, = struct.unpack(">B", self.fmap.read(1))
+ name = self.fmap.read(namelen)
+ assert resid not in d
+ d[resid] = ResourceCls(self.type, resid, name, resattr,
+ self.fdata, resofslo + (resofshi<<16))
+ self.ids = d
+ return self.ids
+
+ def __getitem__(self, id):
+ return self.resources()[id]
+
+ def keys(self):
+ return self.resources().keys()
+
+ def values(self):
+ return self.resources().values()
+
+ def items(self):
+ return self.resources().items()
+
+ def namedict(self):
+ return dict([(r.name, r) for r in self.resources().values() if r.name is not None])
+
+
+class MacBinary:
+
+ def __init__(self, f):
+ if type(f) is type(''):
+ f = open(f, 'rb')
+ self.f = f
+ self.f.seek(0x53)
+ self.dataforksize, self.resforksize = struct.unpack(">ll", self.f.read(8))
+ self.loadresources()
+
+ def getdata(self):
+ self.f.seek(0x80)
+ return self.f.read(self.dataforksize)
+
+ def loadresources(self):
+ f = Subfile(self.f, padto(0x80 + self.dataforksize, 0x80), self.resforksize)
+ ofsdata, ofsmap, lendata, lenmap = struct.unpack(">llll", f.read(16))
+ fdata = Subfile(f, ofsdata, lendata)
+ fmap = Subfile(f, ofsmap, lenmap)
+ fmap.seek(24)
+ ofstype, ofsname = struct.unpack(">HH", fmap.read(4))
+ self.dtypes = {}
+ fmap.seek(ofstype)
+ numtypes, = struct.unpack(">H", fmap.read(2))
+ numtypes = numtypes + 1
+ for rtype, num, ofsref in [struct.unpack(">4sHH", fmap.read(8))
+ for i in range(numtypes)]:
+ assert rtype not in self.dtypes
+ self.dtypes[rtype] = TypeList(rtype, fmap, fdata, ofsname,
+ ofstype + ofsref, num + 1)
+
+ def __getitem__(self, rtype):
+ return self.dtypes[rtype]
+
+ def types(self):
+ return self.dtypes
+
+ def keys(self):
+ return self.dtypes.keys()
+
+ def values(self):
+ return self.dtypes.values()
+
+ def items(self):
+ return self.dtypes.items()
+
+
+class Subfile:
+ def __init__(self, f, start, length):
+ if start < 0:
+ raise ValueError, 'negative position'
+ if isinstance(f, Subfile):
+ if start + length > f.length:
+ raise ValueError, 'subfile out of bounds'
+ f, start = f.f, f.start+start
+ self.f = f
+ self.start = start
+ self.length = length
+ self.position = 0
+ def read(self, size=None):
+ if size is None or self.position + size > self.length:
+ size = self.length - self.position
+ if size <= 0:
+ return ''
+ self.f.seek(self.start + self.position)
+ self.position = self.position + size
+ return self.f.read(size)
+ def seek(self, npos):
+ if npos < 0:
+ raise ValueError, 'negative position'
+ self.position = npos
+
+
+class Resource:
+
+ def __init__(self, type, id, name, attr, srcfile, srcofs):
+ self.type = type
+ self.id = id
+ self.name = name
+ self.attr = attr
+ self.srcfile = srcfile
+ self.srcofs = srcofs
+
+ def subfile(self):
+ self.srcfile.seek(self.srcofs)
+ length, = struct.unpack(">l", self.srcfile.read(4))
+ return Subfile(self.srcfile, self.srcofs + 4, length)
+
+ def load(self):
+ return self.subfile().read()
+
+
+class RGBImage:
+ def __init__(self, w, h, data):
+ assert len(data) == 3*w*h
+ self.w = w
+ self.h = h
+ self.data = data
+
+
+def loadcolormap(f):
+ size, = struct.unpack(">xxxxxxH", f.read(8))
+ size = size + 1
+ d = {}
+ for index, r, g, b in [struct.unpack(">HHHH", f.read(8)) for i in range(size)]:
+ assert index not in d, 'duplicate color index'
+ d[index] = r/256.0, g/256.0, b/256.0
+ return d
+
+def image2rgb(image):
+ # returns (w, h, data)
+ h = len(image)
+ result1 = []
+ for line in image:
+ for r, g, b in line:
+ result1.append(chr(int(r)) + chr(int(g)) + chr(int(b)))
+ return len(image[0]), len(image), ''.join(result1)
+
+
+class clutResource(Resource):
+ # a color table
+ def gettable(self):
+ return loadcolormap(self.subfile())
+
+
+class ppatResource(Resource):
+ # a pattern
+ def getimage(self):
+ f = self.subfile()
+ pattype, patmap, patdata = struct.unpack(">Hll", f.read(10))
+ if pattype != 1:
+ raise ValueError, 'Pattern type not supported'
+ f.seek(patmap)
+ (rowBytes, h, w, packType, packSize,
+ pixelType, pixelSize, cmpCount, cmpSize, pmTable) = (
+ struct.unpack(">xxxxHxxxxHHxxHlxxxxxxxxHHHHxxxxlxxxx", f.read(50)))
+ isBitmap = (rowBytes & 0x8000) != 0
+ rowBytes &= 0x3FFF
+ if packType != 0:
+ raise ValueError, 'packed image not supported'
+ if pixelType != 0 or cmpCount != 1:
+ raise ValueError, 'direct RGB image not supported'
+ assert cmpSize == pixelSize and pixelSize in [1,2,4,8]
+ f.seek(pmTable)
+ colormap = loadcolormap(f)
+ bits_per_pixel = pixelSize
+ pixels_per_byte = 8 // bits_per_pixel
+ image = []
+ f.seek(patdata)
+ for y in range(h):
+ line = f.read(rowBytes)
+ imgline = []
+ for x in range(w):
+ n = x//pixels_per_byte
+ idx = ((ord(line[n]) >> ((pixels_per_byte - 1 - x%pixels_per_byte) * bits_per_pixel))
+ & ((1<<bits_per_pixel)-1))
+ imgline.append(colormap[idx])
+ image.append(imgline)
+ return image
+
+
+class LEVLResource(Resource):
+ # bub & bob level
+ WIDTH = 32
+ HEIGHT = 25
+ MONSTERS = 30
+ WALLS = { 1:'#', 0:' '}
+ WINDS = { 0:' ', 1:'>', 2:'<', 3:'v', 4:'^', 5:'x', 0x66:' '}
+ FLAGS = ['flag0', 'letter', 'fire', 'lightning', 'water', 'top', 'flag6', 'flag7']
+
+ def getlevel(self, mnstrlist):
+ f = self.subfile()
+ result = {}
+
+ walls = []
+ for y in range(self.HEIGHT):
+ line = f.read(self.WIDTH//8)
+ line = [self.WALLS[(ord(line[x//8]) >> (x%8)) & 1]
+ for x in range(self.WIDTH)]
+ walls.append(''.join(line))
+ result['walls'] = '\n'.join(walls)
+
+ winds = []
+ for y in range(self.HEIGHT):
+ line = f.read(self.WIDTH)
+ line = [self.WINDS[ord(v)] for v in line]
+ winds.append(''.join(line))
+ result['winds'] = '\n'.join(winds)
+
+ monsters = []
+ for i in range(self.MONSTERS):
+ x,y,monster_type,f1,f2,f3 = struct.unpack(">BBBBBB", f.read(6))
+ if monster_type != 0:
+ assert f1 == 0, f1
+ cls = mnstrlist[monster_type-1]
+ monsters.append(cls(x=x, y=y, dir=f2, player=f3))
+ result['monsters'] = monsters
+
+ result['level'], = struct.unpack('>H', f.read(2))
+ for i in range(8):
+ result[self.FLAGS[i]] = ord(f.read(1))
+
+ return result
diff --git a/bubbob/mnstrmap.py b/bubbob/mnstrmap.py
new file mode 100644
index 0000000..88e531d
--- /dev/null
+++ b/bubbob/mnstrmap.py
@@ -0,0 +1,397 @@
+
+class Monster1:
+ def __init__(self, x, y, dir, player=0):
+ self.x = x
+ self.y = y
+ self.dir = (1, -1)[dir]
+ self.player = player
+
+def nrange(start,n):
+ return range(start,start+n)
+
+class Nasty(Monster1):
+ right = nrange(239,4)
+ left = nrange(243,4)
+ jailed = nrange(247,3)
+ dead = nrange(253,4)
+ right_angry = nrange(800,4)
+ left_angry = nrange(804,4)
+
+class Monky(Monster1):
+ right = nrange(265,4)
+ left = nrange(269,4)
+ jailed = nrange(273,3)
+ dead = nrange(279,4)
+ left_weapon = right_weapon = nrange(451,1) # FIXME
+ decay_weapon = nrange(452,4) # FIXME
+ right_angry = nrange(808,4)
+ left_angry = nrange(812,4)
+
+class Ghosty(Monster1):
+ right = nrange(291,4)
+ left = nrange(295,4)
+ jailed = nrange(299,3)
+ dead = nrange(305,4)
+ right_angry = nrange(816,4)
+ left_angry = nrange(820,4)
+
+class Flappy(Monster1):
+ right = nrange(317,4)
+ left = nrange(321,4)
+ jailed = nrange(325,3)
+ dead = nrange(331,4)
+ right_angry = nrange(824,4)
+ left_angry = nrange(828,4)
+
+class Springy(Monster1):
+ right = nrange(343,4)
+ left = nrange(347,4)
+ jailed = nrange(351,3)
+ dead = nrange(357,4)
+ right_jump = nrange(369,2)
+ left_jump = nrange(371,2)
+ right_angry = nrange(832,4)
+ left_angry = nrange(836,4)
+ right_jump_angry = nrange(840,2)
+ left_jump_angry = nrange(842,2)
+
+class Orcy(Monster1):
+ right = nrange(373,4)
+ left = nrange(377,4)
+ jailed = nrange(381,3)
+ dead = nrange(387,4)
+ left_weapon = nrange(456,4)
+ right_weapon = nrange(460,4)
+ decay_weapon = nrange(456,0) # FIXME
+ right_angry = nrange(844,4)
+ left_angry = nrange(848,4)
+
+class Gramy(Monster1):
+ right = nrange(399,4)
+ left = nrange(403,4)
+ jailed = nrange(407,3)
+ dead = nrange(413,4)
+ left_weapon = right_weapon = nrange(472,4) # FIXME
+ right_angry = nrange(852,4)
+ left_angry = nrange(856,4)
+
+class Blitzy(Monster1):
+ right = left = nrange(425,4)
+ jailed = nrange(429,3)
+ dead = nrange(435,4)
+ right_angry = left_angry = nrange(860,4)
+ left_weapon = right_weapon = nrange(476,1) # FIXME
+
+class Ghost:
+ left = nrange(443,4)
+ right = nrange(447,4)
+
+class PlayerBubbles:
+ appearing = nrange(952,5)
+ bubble = nrange(910,3)
+ explosion = nrange(913,3)
+ left_weapon = nrange(464,4)
+ right_weapon = nrange(468,4)
+ decay_weapon = []
+
+class LetterBubbles:
+ Extend = nrange(128,3) # FIXME
+ eXtend = nrange(136,3)
+ exTend = nrange(144,3)
+ extEnd = nrange(152,3)
+ exteNd = nrange(160,3)
+ extenD = nrange(168,3)
+
+class DyingBubble:
+ first = nrange(163,3)
+ medium = nrange(171,3)
+ last = nrange(155,3)
+
+class Fire:
+ ground = nrange(490,4)
+ drop = 489
+
+class Lightning:
+ fired = 488
+
+class Water:
+ h_flow = 900
+ v_flow = 901
+ start_right = 902
+ start_left = 904
+ bottom = 903
+ top = 905
+ tl_corner = 906
+ bl_corner = 907
+ br_corner = 908
+ tr_corner = 909
+
+class Flood:
+ waves = nrange(140,4)
+ fill = 495
+
+class MiscPoints:
+ pink_100 = 496
+
+class DigitsMisc:
+ digits_mask = nrange(519,10)
+ digits_border = nrange(920,10)
+ digits_white = nrange(930,10)
+
+class PotionBonuses:
+ coin = 477
+ flower = 478
+ trefle = 479
+ rainbow = 480
+ green_note = 481
+ blue_note = 692
+
+
+class Bonuses:
+ monster_bonuses = [
+ (593,1000), # banana
+ (594,2000), # peach
+ (595,3000), # quince
+ (596,4000), # pastec
+ (597,5000), # wine
+ (598,6000), # ananas
+ (599,8000) # diamond
+ ]
+
+ door = 139 # Lots of diamonds
+
+ red_potion = 637 #\ .
+ green_potion = 638 # > Clean the level and fill the top 5 lines with one of the PotionBonuses.
+ yellow_potion = 639 #/
+
+ kirsh = 600
+ icecream1 = 601 # NOT_USED
+ erdbeer = 602
+ fish1 = 603
+ tomato = 604
+ donut = 605
+ apple = 606
+ corn = 607
+ icecream2 = 608 # NOT_USED
+ radish = 609
+
+ cyan_ice = 610 #\ .
+ violet_ice = 611 #|
+ peach2 = 612 # > Produced from the bubbles after a wand.
+ pastec2 = 613 #|
+ cream_pie = 614 #|
+ sugar_pie = 615 #/
+
+ brown_wand = 620 #\ .
+ yellow_wand = 621 #|
+ green_wand = 622 # > Bubbles turn into bonus of the previous set after
+ violet_wand = 623 # > the death of the last enemy plus a mega-bonus.
+ blue_wand = 624 #|
+ red_wand = 625 #/
+
+ violet_chest = 626 #\ .
+ blue_chest = 627 # > Bubbles turn into diamonds plus after the death
+ red_chest = 628 # > of the last enemy plus a mega-diamond
+ yellow_chest = 629 #/
+
+ shoe = 631 # speed player movements
+ grenade = 632 # put fire everywhere
+
+ brown_umbrella = 633 # fire rain
+ grey_umbrella = 634 # water rain
+ violet_umbrella = 635 # spinning balls rain
+
+ clock = 636 # time travel
+ coffee = 641 # Speed player's movements and fire rate.
+ book = 642 # Produces stars the middle-top going in any direction which kill the enemy upon contact.
+ heart_poison = 643 # Froze the enemy and they are now killed on contact.
+ gold_crux = 644 # become a bubble
+ red_crux = 645 # become a monster
+ blue_crux = 646 # become a monster
+ extend = 647 # Give 100'000 Points to the player and finish the level.
+
+ ring = 640 # lord of the ring
+ green_pepper = 648 # hot stuff!
+ orange_thing = 649 # slippy
+ aubergine = 650 # rear gear
+ carrot = 651 # angry monsters
+ rape = 652 # auto-fire
+ white_carrot = 653 # fly
+ chickpea = 654 # shield
+ mushroom = 655 # pinball mode
+ egg = 656 # players permutation
+ chestnut = 657 # variation of frames per second
+ green_thing = 658 # sugar bomb
+ icecream3 = 659 # \ each icecream becomes two of the
+ icecream4 = 660 # \ next kind, which scores more points
+ icecream5 = 661 # / that's a lot of points in total
+ icecream6 = 662 # /
+ softice1 = 663 # shoot farther
+ softice2 = 665 # shoot nearer1
+ french_fries = 664 # shoot 10 lightning bubbles
+ custard_pie = 666 # shoot faster
+ lollipop = 667 # invert left and right
+ cocktail = 668 # short-lived bubbles
+ ham = 669 # wall builder
+ bomb = 670 # explodes the structure of the level
+ beer = 671 # shoot 10 water bubbles
+ emerald = 672 # mega points
+ fish2 = 673 # mega blitz
+ sapphire = 681 # mega points
+ ruby = 682 # mega points
+ tin = 674 # angry (double-speed) player
+ hamburger = 675 # shoot 10 fire bubbles
+ insect = 676 # walls fall down
+ blue_necklace = 677 # player ubiquity
+ violet_necklace = 679 # monster ubiquity
+ butterfly = 678 # lunar gravity
+ conch = 680 # complete water flood
+ yellow_sugar = 630 # from a bonbon bomb
+ blue_sugar = 691 # from a bonbon bomb
+
+class Diamonds:
+ # Produced from the bubbles after last enemy is killed and a chest or wand has been caught.
+ violet = 616
+ blue = 617
+ red = 618
+ yellow = 619
+
+class Stars:
+ # Effect of the book. Kill monsters on contact.
+ blue = nrange(940,2)
+ yellow = nrange(942,2)
+ red = nrange(944,2)
+ green = nrange(946,2)
+ magenta = nrange(948,2)
+ cyan = nrange(950,2)
+ COLORS = ['blue', 'yellow', 'red', 'green', 'magenta', 'cyan']
+
+class SpinningBalls:
+ free = nrange(482,4)
+ bubbled = nrange(486,2) # NOT_USED
+
+class BigImages:
+ cyan_ice = 10 # Megabonus produced after a wand
+ violet_ice = 11
+ peach2 = 12
+ pastec2 = 13
+ cream_pie = 14
+ sugar_pie = 15
+ violet = 16
+ blue = 17
+ red = 18
+ yellow = 19
+ blitz = 30
+ hurryup = nrange(31,2)
+
+class birange:
+ def __init__(self, a,b,n):
+ self.a = a
+ self.n = n
+ def __getitem__(self, pn):
+ return range(self.a + 1000*pn, self.a + 1000*pn + self.n)
+
+class bidict:
+ def __init__(self, a,b):
+ self.a = a.items()
+ def __getitem__(self, pn):
+ pn *= 1000
+ d = {}
+ for key, value in self.a:
+ d[key] = value + pn
+ return d
+
+class GreenAndBlue:
+ water_bubbles = birange(182,185,3)
+ fire_bubbles = birange(176,554,3)
+ light_bubbles = birange(179,557,3)
+ normal_bubbles = birange(188,195,3)
+ new_bubbles = birange(191,203,4)
+ players = birange(210,226,13)
+ jumping_players = birange(683,687,4)
+ new_players = birange(693,696,3)
+ numbers = birange(499,509,10) # FIXME: already seen below
+ comming = birange(693,696,3)
+ points = bidict({
+ 100: 529,
+ 150: 530,
+ 200: 531,
+ 250: 532,
+ 300: 533,
+ 350: 534,
+ 500: 535,
+ 550: 536,
+ 600: 537,
+ 650: 538,
+ 700: 539,
+ 750: 540,
+ 800: 541,
+ 850: 542,
+ 900: 543,
+ 950: 544,
+ 1000: 545,
+ 2000: 546,
+ 3000: 547,
+ 4000: 548,
+ 5000: 549,
+ 6000: 550,
+ 7000: 551,
+ 8000: 552,
+ 9000: 553,
+ 10000: 20,
+ 20000: 21,
+ 30000: 22,
+ 40000: 23,
+ 50000: 24,
+ 60000: 25,
+ 70000: 26,
+ },{
+ 100: 561,
+ 150: 562,
+ 200: 563,
+ 250: 564,
+ 300: 565,
+ 350: 566,
+ 500: 567,
+ 550: 568,
+ 600: 569,
+ 650: 570,
+ 700: 571,
+ 750: 572,
+ 800: 573,
+ 850: 574,
+ 900: 575,
+ 950: 576,
+ 1000: 577,
+ 2000: 578,
+ 3000: 579,
+ 4000: 580,
+ 5000: 581,
+ 6000: 582,
+ 7000: 583,
+ 8000: 584,
+ 9000: 585,
+ 10000: 90,
+ 20000: 91,
+ 30000: 92,
+ 40000: 93,
+ 50000: 94,
+ 60000: 95,
+ 70000: 96,
+ })
+ gameover = birange(497,498,1)
+ digits = birange(499,509,10)
+ fish = birange(700,707,7)
+
+class Butterfly(Monster1):
+ right = [('butterfly', 'fly', n) for n in range(2)]
+ left = [Bonuses.insect, Bonuses.butterfly]
+ jailed = [('butterfly', 'jailed', n) for n in range(3)]
+ dead = [('butterfly', 'dead', n) for n in range(4)]
+
+class Sheep(Monster1):
+ right = [('sheep', 1, n) for n in range(4)]
+ left = [('sheep',-1, n) for n in range(4)]
+ right_angry = right
+ left_angry = left
+
diff --git a/bubbob/monsters.py b/bubbob/monsters.py
new file mode 100644
index 0000000..88983d0
--- /dev/null
+++ b/bubbob/monsters.py
@@ -0,0 +1,937 @@
+from __future__ import generators
+import random
+import gamesrv
+import images
+import boards
+from boards import *
+from images import ActiveSprite
+from mnstrmap import GreenAndBlue, Bonuses, Ghost
+from player import BubPlayer
+import bonuses
+
+
+class Monster(ActiveSprite):
+ touchable = 1
+ special_prob = 0.2
+ shootcls = None
+ vx = 2
+ vy = 0
+ vdir = -1
+ is_ghost = 0
+ MonsterBonus = bonuses.MonsterBonus
+
+ def __init__(self, mnstrdef, x=None, y=None, dir=None, in_list=None):
+ self.mdef = mnstrdef
+ self.ptag = None
+ if dir is None: dir = mnstrdef.dir
+ if x is None: x = mnstrdef.x*CELL
+ if y is None: y = mnstrdef.y*CELL
+ self.dir = dir
+ ActiveSprite.__init__(self, images.sprget(self.imgrange()[0]), x, y)
+ self.gen.append(self.waiting())
+ if in_list is None:
+ in_list = BubPlayer.MonsterList
+ self.in_list = in_list
+ self.in_list.append(self)
+ self.no_shoot_before = 0
+ #images.ActiveSprites.remove(self)
+
+ def unlist(self):
+ try:
+ self.in_list.remove(self)
+ return 1
+ except ValueError:
+ return 0
+
+ def kill(self):
+ self.unlist()
+ ActiveSprite.kill(self)
+
+ def tagdragon(self):
+ lst = bonuses.getvisibledragonlist()
+ if lst:
+ return random.choice(lst)
+ else:
+ return None
+
+ def imgrange(self):
+ if self.is_ghost:
+ if self.dir > 0:
+ return Ghost.right
+ else:
+ return Ghost.left
+ elif self.angry:
+ if self.dir > 0:
+ return self.mdef.right_angry
+ else:
+ return self.mdef.left_angry
+ else:
+ if self.dir > 0:
+ return self.mdef.right
+ else:
+ return self.mdef.left
+
+ def imgrange1(self):
+ # normally this is self.imgrange()[1]
+ lst = self.imgrange()
+ return lst[len(lst) > 1]
+
+ def resetimages(self, is_ghost=0):
+ self.is_ghost = is_ghost
+ if self.gen:
+ self.setimages(self.cyclic(self.imgrange(), 3))
+ else: # frozen monster
+ self.seticon(images.sprget(self.imgrange()[0]))
+
+ def blocked(self):
+ if self.dir < 0:
+ x0 = (self.x-1)//16
+ else:
+ x0 = (self.x+33)//16
+ y0 = self.y // 16 + 1
+ y1 = (self.y + 31) // 16
+ return bget(x0,y0) == '#' or bget(x0,y1) == '#'
+
+ def tryhstep(self):
+ if self.blocked():
+ self.dir = -self.dir
+ self.resetimages()
+ return 0
+ else:
+ self.step(self.vx*self.dir, 0)
+ return 1
+
+ def vblocked(self):
+ if self.vdir < 0:
+ y0 = self.y//16
+ else:
+ y0 = (self.y+1)//16 + 2
+ x0 = self.x // 16
+ x1 = self.x // 16 + 1
+ x2 = (self.x+31) // 16
+ return bget(x0,y0) == '#' or bget(x1,y0) == '#' or bget(x2,y0) == '#'
+
+ def tryvstep(self):
+ if self.vblocked():
+ self.vdir = -self.vdir
+ return 0
+ else:
+ self.step(0, self.vy*self.vdir)
+ self.vertical_warp()
+ return 1
+
+ def waiting(self, delay=20):
+ for i in range(delay):
+ yield None
+ self.resetimages()
+ self.gen.append(self.default_mode())
+
+ def overlapping(self):
+ if self.in_list is BubPlayer.MonsterList:
+ for s in self.in_list:
+ if (-6 <= s.x-self.x <= 6 and -6 <= s.y-self.y < 6 and
+ #s.dir == self.dir and s.vdir == self.vdir and
+ s.vx == self.vx and s.vy == self.vy and
+ (not s.angry) == (not self.angry)):
+ return s is not self
+ return 0
+
+ def walking(self):
+ while onground(self.x, self.y):
+ yield None
+ if random.random() < 0.2 and self.overlapping():
+ yield None
+ x1 = self.x
+ if self.dir > 0:
+ x1 += self.vx
+ if (x1 & 15) < self.vx and random.random() < self.special_prob:
+ self.move(x1 & -16, self.y)
+ if self.special():
+ return
+ self.tryhstep()
+ if self.seedragon():
+ self.gen.append(self.hjumping())
+ else:
+ self.gen.append(self.falling())
+
+ def seedragon(self, dragon=None):
+ dragon = dragon or self.tagdragon()
+ if dragon is None:
+ return False
+ return abs(dragon.y - self.y) < 16 and self.dir*(dragon.x-self.x) > 0
+
+ def special(self):
+ dragon = self.tagdragon()
+ if dragon is None:
+ return 0
+ if self.seedragon(dragon) and self.shoot():
+ return 1
+ if dragon.y < self.y-CELL:
+ #and abs(dragon.x-self.x) < 2*(self.y-dragon.y):
+ for testy in range(self.y-2*CELL, self.y-6*CELL, -CELL):
+ if onground(self.x, testy):
+ if random.random() < 0.5:
+ ndir = self.dir
+ elif dragon.x < self.x:
+ ndir = -1
+ else:
+ ndir = 1
+ self.gen.append(self.vjumping(testy, ndir))
+ return 1
+ return 0
+
+ def shooting(self, pause):
+ for i in range(pause):
+ yield None
+ self.shootcls(self)
+ yield None
+ self.gen.append(self.default_mode())
+
+ def shoot(self, pause=10):
+ if (self.shootcls is None or
+ self.no_shoot_before > BubPlayer.FrameCounter):
+ return 0
+ else:
+ self.gen.append(self.shooting(pause))
+ self.no_shoot_before = BubPlayer.FrameCounter + 29
+ return 1
+
+ def falling(self):
+ bubber = getattr(self, 'bubber', None)
+ while not onground(self.x, self.y):
+ yield None
+ ny = self.y + 3
+ if (ny & 15) > 14:
+ ny = (ny//16+1)*16
+ elif (ny & 15) < 3:
+ ny = (ny//16)*16
+ nx = self.x
+ if nx < 32:
+ nx += 1 + (self.vx-1) * (bubber is not None)
+ elif nx > boards.bwidth - 64:
+ nx -= 1 + (self.vx-1) * (bubber is not None)
+ elif bubber:
+ dx = bubber.wannago(self.dcap)
+ if dx and dx != self.dir:
+ self.dir = dx
+ self.resetimages()
+ self.setimages(None)
+ if dx and not self.blocked():
+ nx += self.vx*dx
+ self.seticon(images.sprget(self.imgrange1()))
+ self.move(nx, ny)
+ if self.y >= boards.bheight:
+ self.vertical_warp()
+ if bubber:
+ nextgen = self.playing_monster
+ else:
+ nextgen = self.walking
+ self.gen.append(nextgen())
+
+## def moebius(self):
+## self.dir = -self.dir
+## self.resetimages()
+## if hasattr(self, 'dcap'):
+## self.dcap['left2right'] *= -1
+
+ def hjumping(self):
+ y0 = self.y
+ vspeed = -2.2
+ ny = y0-1
+ while ny <= y0 and not self.blocked():
+ self.move(self.x+2*self.dir, int(ny))
+ yield None
+ vspeed += 0.19
+ ny = self.y + vspeed
+ self.gen.append(self.default_mode())
+
+ def vjumping(self, limity, ndir):
+ self.setimages(None)
+ yield None
+ self.dir = -self.dir
+ self.seticon(images.sprget(self.imgrange()[0]))
+ for i in range(9):
+ yield None
+ self.dir = -self.dir
+ self.seticon(images.sprget(self.imgrange()[0]))
+ for i in range(4):
+ yield None
+ self.dir = ndir
+ self.seticon(images.sprget(self.imgrange1()))
+ for ny in range(self.y-4, limity-4, -4):
+ self.move(self.x, ny)
+ if ny < -32:
+ self.vertical_warp()
+ yield None
+ self.resetimages()
+ self.gen.append(self.default_mode())
+
+ def regular(self):
+ return self.still_playing() and self.touchable and not self.is_ghost
+
+ def still_playing(self):
+ return (self.in_list is BubPlayer.MonsterList and
+ self in self.in_list)
+
+ def touched(self, dragon):
+ if self.gen:
+ self.killdragon(dragon)
+ if self.is_ghost and not hasattr(self, 'bubber'):
+ self.gen = [self.default_mode()]
+ self.resetimages()
+ else:
+ self.argh(getattr(self, 'poplist', None)) # frozen monster
+
+ def killdragon(self, dragon):
+ dragon.die()
+
+ def in_bubble(self, bubble):
+ if not hasattr(self.mdef, 'jailed'):
+ return
+ self.untouchable()
+ self.angry = []
+ bubble.move(self.x, self.y)
+ if not hasattr(bubble, 'withmonster'):
+ bubble.to_front()
+ self.to_front()
+ img = self.mdef.jailed
+ self.gen = [self.bubbling(bubble)]
+ self.setimages(self.cyclic([img[1], img[2], img[1], img[0]], 4))
+
+ def bubbling(self, bubble):
+ counter = 0
+ while not hasattr(bubble, 'poplist'):
+ self.move(bubble.x, bubble.y)
+ yield None
+ counter += 1
+ if counter == 50 and hasattr(self, 'bubber'):
+ bubble.setimages(bubble.bubble_red())
+ if bubble.poplist is None:
+ self.touchable = 1
+ self.angry = [self.genangry()]
+ self.resetimages()
+ self.gen.append(self.default_mode())
+ else:
+ previous_len = len(BubPlayer.MonsterList)
+ self.argh(bubble.poplist)
+ dragon = bubble.poplist[0]
+ if dragon is not None:
+ if previous_len and not BubPlayer.MonsterList:
+ points = 990
+ else:
+ points = 90
+ dragon.bubber.givepoints(points)
+
+ def argh(self, poplist=None, onplace=0):
+ if self not in self.in_list:
+ return
+ if not poplist:
+ poplist = [None]
+ poplist.append(self)
+ level = len(poplist) - 2
+ bonuses.BonusMaker(self.x, self.y, self.mdef.dead, onplace=onplace,
+ outcome=(self.MonsterBonus, level))
+ self.kill()
+
+ def freeze(self, poplist):
+ # don't freeze monsters largely out of screen, or they'll never come in
+ if self.regular() and -self.ico.h < self.y < boards.bheight:
+ self.gen = []
+ self.poplist = poplist
+
+ def flying(self):
+ blocked = 0
+ while 1:
+ if random.random() < 0.2 and self.overlapping():
+ yield None
+ hstep = self.tryhstep()
+ vstep = self.tryvstep()
+ if hstep or vstep:
+ blocked = 0
+ elif blocked:
+ # blocked! go up or back to the play area
+ if self.x < 32:
+ self.step(self.vy, 0)
+ elif self.x > boards.bwidth - 64:
+ self.step(-self.vy, 0)
+ else:
+ self.step(0, -self.vy)
+ self.vertical_warp()
+ else:
+ blocked = 1
+ yield None
+
+ def becoming_monster(self, big=0, immed=0):
+ if big:
+ self.is_ghost = 1
+ self.seticon(images.sprget(self.imgrange()[0]))
+ images.Snd.Hell.play()
+ for i in range(5):
+ ico = self.ico
+ self.seticon(self.bubber.icons[11 + immed, self.dir])
+ yield None
+ yield None
+ self.seticon(ico)
+ yield None
+ yield None
+ self.resetimages(is_ghost=big)
+ self.gen.append(self.playing_monster())
+
+ def become_monster(self, bubber, saved_caps, big=0, immed=0):
+ self.timeoutgen = self.back_to_dragon()
+ self.default_mode = self.playing_monster
+ self.bubber = bubber
+ self.dcap = saved_caps
+ self.gen = [self.becoming_monster(big, immed)]
+
+ def back_to_dragon(self):
+ for t in range(259):
+ yield None
+ if bonuses.getdragonlist():
+ yield None
+ yield None
+ yield None
+ from player import Dragon
+ d = Dragon(self.bubber, self.x, self.y, self.dir, self.dcap)
+ d.dcap['shield'] = 50
+ self.bubber.dragons.append(d)
+ self.kill()
+
+ def playing_monster(self):
+ if self.timeoutgen not in self.gen:
+ self.gen.append(self.timeoutgen)
+ bubber = self.bubber
+ while self.is_ghost:
+ # ghost
+ self.angry = []
+ key, dx, dy = max([(bubber.key_left, -1, 0),
+ (bubber.key_right, 1, 0),
+ (bubber.key_jump, 0, -1),
+ (bubber.key_fire, 0, 1)])
+ if key:
+ if dx and self.dir != dx:
+ self.dir = dx
+ self.resetimages(is_ghost=1)
+ nx = self.x + 10*dx
+ ny = self.y + 9*dy
+ if nx < 0: nx = 0
+ if nx > boards.bwidth-2*CELL: nx = boards.bwidth-2*CELL
+ if ny < -CELL: ny = -CELL
+ if ny > boards.bheight-CELL: ny = boards.bheight-CELL
+ self.move(nx, ny)
+ yield None
+ if self.vy:
+ # flying monster
+ while 1:
+ dx = bubber.wannago(self.dcap)
+ if dx and dx != self.dir:
+ self.dir = dx
+ self.resetimages()
+ if bubber.key_jump and bubber.key_jump > bubber.key_fire:
+ dy = self.vdir = -1
+ elif bubber.key_fire:
+ dy = self.vdir = 1
+ else:
+ dy = 0
+ hstep = dx and self.tryhstep()
+ vstep = dy and self.tryvstep()
+ if dx and dy and not (hstep or vstep):
+ # blocked?
+ self.dir = -self.dir
+ self.vdir = -self.vdir
+ blocked = self.blocked() and self.vblocked()
+ self.dir = -self.dir
+ self.vdir = -self.vdir
+ if blocked:
+ # completely blocked! accept move or force back to
+ # play area
+ if self.x < 32:
+ self.step(self.vy, 0)
+ elif self.x > boards.bwidth - 64:
+ self.step(-self.vy, 0)
+ else:
+ self.step(self.vx*dx, self.vy*dy)
+ self.vertical_warp()
+ yield None
+ elif not isinstance(self, Springy):
+ # walking monster
+ jumping_y = 0
+ imgsetter = self.imgsetter
+ while onground(self.x, self.y) or jumping_y:
+ dx = bubber.wannago(self.dcap)
+ if dx and dx != self.dir:
+ self.dir = dx
+ self.resetimages()
+ imgsetter = self.imgsetter
+ if dx and not self.blocked():
+ self.step(self.vx*dx, 0)
+ if not jumping_y:
+ self.setimages(imgsetter)
+ else:
+ self.seticon(images.sprget(self.imgrange1()))
+ self.setimages(None)
+ else:
+ self.setimages(None)
+ dx = 0
+ yield None
+ if not jumping_y:
+ wannafire = bubber.key_fire
+ wannajump = bubber.key_jump
+ if wannafire and self.shoot(1):
+ return
+ if wannajump:
+ jumping_y = CELL
+ if jumping_y:
+ self.step(0, -4)
+ if self.y < -32:
+ self.vertical_warp()
+ if onground(self.x, self.y):
+ jumping_y = 0
+ else:
+ jumping_y -= 1
+ self.gen.append(self.falling())
+ else:
+ # springy
+ if not onground(self.x, self.y):
+ self.gen.append(self.falling())
+ return
+ prevx = self.x
+ for t in self.walking():
+ dx = bubber.wannago(self.dcap)
+ if dx:
+ if dx != self.dir:
+ self.dir = dx
+ self.resetimages()
+ if self.blocked() and (self.x-prevx)*dx <= 0:
+ dx = 0
+ self.move(prevx + self.vx*dx, self.y)
+ yield None
+ prevx = self.x
+
+ def become_ghost(self):
+ self.gen = [self.ghosting()]
+ self.resetimages(is_ghost=1)
+
+ def ghosting(self):
+ counter = 0
+ while counter < 5:
+ for i in range(50):
+ yield None
+ dragon = self.tagdragon()
+ if dragon is None:
+ counter += 1
+ else:
+ counter = 0
+ px, py = dragon.x, dragon.y
+ if abs(px-self.x) < abs(py-self.y):
+ dx = 0
+ if py > self.y:
+ dy = 1
+ else:
+ dy = -1
+ else:
+ dy = 0
+ if px > self.x:
+ dx = 1
+ else:
+ dx = -1
+ self.dir = dx
+ self.resetimages(is_ghost=1)
+ dx *= 10
+ dy *= 9
+ distance = 1E10
+ while 1:
+ self.angry = []
+ self.step(dx, dy)
+ yield None
+ dist1 = (px-self.x)*(px-self.x)+(py-self.y)*(py-self.y)
+ if dist1 > distance:
+ break
+ distance = dist1
+ self.angry = []
+ self.gen = [self.default_mode()]
+ self.resetimages()
+
+ default_mode = falling
+
+
+def argh_em_all():
+ poplist = [None]
+ for s in images.ActiveSprites[:]:
+ if isinstance(s, Monster):
+ s.argh(poplist)
+
+def freeze_em_all():
+ poplist = [None]
+ for s in images.ActiveSprites:
+ if isinstance(s, Monster):
+ s.freeze(poplist)
+
+
+class MonsterShot(ActiveSprite):
+ speed = 6
+ touchable = 1
+
+ def __init__(self, owner, dx=CELL, dy=0):
+ self.owner = owner
+ self.speed = owner.dir * self.speed
+ if owner.dir < 0:
+ nimages = owner.mdef.left_weapon
+ else:
+ nimages = owner.mdef.right_weapon
+ ActiveSprite.__init__(self, images.sprget(nimages[0]),
+ owner.x, owner.y + dy)
+ self.step((owner.ico.w - self.ico.w) // 2,
+ (owner.ico.h - self.ico.h) // 2)
+ if not self.blocked():
+ self.step(dx*owner.dir, 0)
+ if len(nimages) > 1:
+ self.setimages(self.cyclic(nimages, 3))
+ self.gen.append(self.moving())
+
+ def blocked(self):
+ if self.speed < 0:
+ x0 = (self.x-self.speed-8)//16
+ else:
+ x0 = (self.x+self.ico.w+self.speed-8)//16
+ y0 = (self.y+8) // 16 + 1
+ return not (' ' == bget(x0,y0) == bget(x0+1,y0))
+
+ def moving(self):
+ while not self.blocked():
+ yield None
+ self.step(self.speed, 0)
+ self.hitwall()
+
+ def hitwall(self):
+ self.untouchable()
+ self.gen.append(self.die(self.owner.mdef.decay_weapon, 2))
+
+ def touched(self, dragon):
+ dragon.die()
+
+
+class BoomerangShot(MonsterShot):
+ speed = 8
+
+ def hitwall(self):
+ self.gen.append(self.moveback())
+
+ def moveback(self):
+ owner = self.owner
+ if self.speed > 0:
+ nimages = owner.mdef.left_weapon
+ else:
+ nimages = owner.mdef.right_weapon
+ self.setimages(self.cyclic(nimages, 3))
+ while (owner.x-self.x) * self.speed < 0:
+ yield None
+ self.step(-self.speed, 0)
+ if self.blocked():
+ break
+ self.kill()
+
+class FastShot(MonsterShot):
+ speed = 15
+
+
+class DownShot(MonsterShot):
+
+ def __init__(self, owner):
+ MonsterShot.__init__(self, owner, 0, CELL)
+
+ def moving(self):
+ while self.y < boards.bheight:
+ yield None
+ self.step(0, 7)
+ self.kill()
+
+
+##class DragonShot(MonsterShot):
+## speed = 8
+
+## def __init__(self, owner):
+## MonsterShot.__init__(self, owner)
+## self.untouchable()
+## self.gen.append(self.touchdelay(4))
+
+## def touched(self, dragon):
+## if dragon is not self.owner:
+## if dragon.bubber.bonbons == 0:
+## dragon.die()
+## else:
+## from player import scoreboard
+## from bonuses import Sugar1, Sugar2
+## from bonuses import BonusMaker
+## if random.random() < 0.2345:
+## start = 1
+## else:
+## start = 0
+## loose = min(2, dragon.bubber.bonbons)
+## for i in range(start, loose):
+## cls = random.choice([Sugar1, Sugar2])
+## BonusMaker(self.x, self.y, [cls.nimage],
+## outcome=(cls,))
+## dragon.bubber.bonbons -= loose
+## scoreboard()
+## dragon.dcap['shield'] = 25
+## self.owner.play(images.Snd.Yippee)
+## self.kill()
+
+## def blocked(self):
+## return self.x < -self.ico.w or self.x >= gamesrv.game.width
+## #return self.x < CELL or self.x >= boards.bwidth - 3*CELL
+
+
+class Nasty(Monster):
+ pass
+
+class Monky(Monster):
+ shootcls = MonsterShot
+
+class Ghosty(Monster):
+ default_mode = Monster.flying
+ vy = 2
+
+class Flappy(Monster):
+ default_mode = Monster.flying
+ vy = 1
+
+class Springy(Monster):
+ spring_down = 0
+ def imgrange(self):
+ if self.spring_down and not self.is_ghost:
+ if self.angry:
+ if self.dir > 0:
+ r = self.mdef.right_jump_angry
+ else:
+ r = self.mdef.left_jump_angry
+ else:
+ if self.dir > 0:
+ r = self.mdef.right_jump
+ else:
+ r = self.mdef.left_jump
+ return [r[self.spring_down-1]]
+ else:
+ return Monster.imgrange(self)
+ def walking(self):
+ self.spring_down = 1
+ self.resetimages()
+ for t in range(2+self.overlapping()):
+ yield None
+ self.spring_down = 2
+ self.resetimages()
+ for t in range(4+2*self.overlapping()):
+ yield None
+ self.spring_down = 1
+ self.resetimages()
+ for t in range(2+self.overlapping()):
+ yield None
+ self.spring_down = 0
+ self.resetimages()
+ g = 10.0/43
+ vy = -20*g
+ yf = self.y
+ for t in range(40):
+ yf += vy
+ vy += g
+ if self.blocked():
+ self.dir = -self.dir
+ self.resetimages()
+ nx = self.x + self.dir*self.vx
+ if self.y//16 < int(yf)//16:
+ if onground(self.x, (self.y//16+1)*16):
+ break
+ if onground(nx, (self.y//16+1)*16):
+ self.move(nx, self.y)
+ break
+ nx, yf = vertical_warp(nx, yf)
+ self.move(nx, int(yf))
+## if moebius:
+## self.moebius()
+ yield None
+ self.gen.append(self.falling())
+
+class Orcy(Monster):
+ shootcls = FastShot
+
+class Gramy(Monster):
+ shootcls = BoomerangShot
+ vx = 3
+
+class Blitzy(Monster):
+ shootcls = DownShot
+ vx = 3
+
+ def seedragon(self, dragon=None):
+ return 0
+
+ def special(self):
+ if random.random() < 0.3:
+ self.shootcls(self)
+ return 0
+
+ def shoot(self, pause=0):
+ # no pause (only used when controlled by the player)
+ if self.no_shoot_before > BubPlayer.FrameCounter:
+ pass
+ else:
+ self.shootcls(self)
+ self.no_shoot_before = BubPlayer.FrameCounter + 29
+ return 0
+
+MonsterClasses = [c for c in globals().values()
+ if type(c)==type(Monster) and issubclass(c, Monster)]
+MonsterClasses.remove(Monster)
+
+
+class Butterfly(Monster):
+ MonsterBonus = bonuses.IceMonsterBonus
+ fly_away = False
+
+ def waiting(self, delay=0):
+ return Monster.waiting(self, delay)
+
+ def imgrange(self):
+ self.angry = []
+ return Monster.imgrange(self)
+
+ def killdragon(self, dragon):
+ if self.is_ghost:
+ Monster.killdragon(self, dragon)
+ else:
+ self.fly_away = True, dragon.x
+
+ def flying(self):
+ repeat = 0
+ while 1:
+ r = random.random()
+ if self.x < 64:
+ bump = self.dir < 0
+ elif self.x > boards.bwidth - 64:
+ bump = self.dir > 0
+ elif self.fly_away:
+ wannago = self.x - self.fly_away[1]
+ if self.x < 100:
+ wannago = 1
+ elif self.x > boards.bwidth - 100:
+ wannago = -1
+ bump = self.dir * wannago < 0
+ if repeat:
+ self.fly_away = False
+ repeat = 0
+ else:
+ repeat = 1
+ else:
+ bump = r < 0.07
+ if bump:
+ self.dir = -self.dir
+ self.resetimages()
+ elif r > 0.92:
+ self.vdir = -self.vdir
+ self.step(self.dir * (2 + (r < 0.5)), self.vdir * 2)
+ self.vertical_warp()
+ if not repeat:
+ yield None
+
+ default_mode = flying
+
+
+class Sheep(Monster):
+
+ def playing_monster(self):
+ from bonuses import Bonus
+ bubber = self.bubber
+ vy = None
+ imgsetter = self.imgsetter
+ poplist = [None]
+ while 1:
+ dx = bubber.wannago(self.dcap)
+ if dx and dx != self.dir:
+ self.dir = dx
+ self.resetimages()
+ imgsetter = self.imgsetter
+ if dx and vy is None:
+ self.setimages(imgsetter)
+ else:
+ self.setimages(None)
+ if vy is not None:
+ if vy < 0:
+ n = 1
+ else:
+ n = 3
+ self.seticon(images.sprget(self.imgrange()[n]))
+ if dx and not self.blocked():
+ self.step(self.vx*dx, 0)
+ yield None
+ impulse = 0.0
+ wannajump = bubber.key_jump
+ if vy is not None:
+ vy += 0.33
+ if vy > 12.0:
+ vy = 12.0
+ yf = self.y + yfp + vy
+ yfp = yf - int(yf)
+ delta = int(yf) - self.y
+ if delta > 0:
+ by_y = {}
+ for s in images.ActiveSprites:
+ if isinstance(s, Bonus) and s.touchable:
+ if abs(s.x - self.x) <= 22:
+ by_y[s.y] = s
+ for monster in BubPlayer.MonsterList:
+ if abs(monster.x - self.x) <= 22:
+ if monster.regular():
+ by_y[monster.y] = monster
+ for ny in range(self.y - 1, self.y + delta + 1):
+ self.move(self.x, ny)
+ self.vertical_warp()
+ if onground(self.x, self.y):
+ poplist = [None]
+ impulse = vy
+ vy = None
+ break
+ key = self.y + 29
+ if key in by_y:
+ s = by_y[key]
+ if isinstance(s, Monster):
+ self.play(images.Snd.Extra)
+ s.argh(poplist)
+ elif isinstance(s, Bonus):
+ s.reallytouched(self)
+ yfp = 0.0
+ vy = -3.3
+ break
+ else:
+ self.step(0, delta)
+ self.vertical_warp()
+ if vy is None:
+ if onground(self.x, self.y):
+ if wannajump:
+ yfp = 0.0
+ vy = - max(1.0, impulse) - 2.02
+ impulse = 0.0
+ self.play(images.Snd.Jump)
+ else:
+ yfp = vy = 0.0
+ if impulse > 8.1:
+ break
+ self.play(images.Snd.Pop)
+ for n in range(2):
+ for letter in 'abcdefg':
+ ico = images.sprget(('sheep', letter))
+ nx = self.x + random.randrange(-1, self.ico.w - ico.w + 2)
+ ny = self.y + random.randrange(0, self.ico.h - ico.h + 2)
+ dxy = [random.random() * 5.3 - 2.65, random.random() * 4 - 4.4]
+ s = images.ActiveSprite(ico, nx, ny)
+ s.gen.append(s.parabolic(dxy))
+ s.gen.append(s.die([None], random.randrange(35, 54)))
+ self.move(-99, 0)
+ for t in range(68):
+ yield None
+ self.kill()
+
+ default_mode = falling = playing_monster
+
+ def argh(self, *args, **kwds):
+ pass
diff --git a/bubbob/music/Snd1-8.wav b/bubbob/music/Snd1-8.wav
new file mode 100644
index 0000000..f4962cf
--- /dev/null
+++ b/bubbob/music/Snd1-8.wav
Binary files differ
diff --git a/bubbob/music/Snd2-8.wav b/bubbob/music/Snd2-8.wav
new file mode 100644
index 0000000..c59f1e8
--- /dev/null
+++ b/bubbob/music/Snd2-8.wav
Binary files differ
diff --git a/bubbob/music/Snd3-8.wav b/bubbob/music/Snd3-8.wav
new file mode 100644
index 0000000..dda70f0
--- /dev/null
+++ b/bubbob/music/Snd3-8.wav
Binary files differ
diff --git a/bubbob/music/Snd4-8.wav b/bubbob/music/Snd4-8.wav
new file mode 100644
index 0000000..509afad
--- /dev/null
+++ b/bubbob/music/Snd4-8.wav
Binary files differ
diff --git a/bubbob/music/Snd5-8.wav b/bubbob/music/Snd5-8.wav
new file mode 100644
index 0000000..b4bfabc
--- /dev/null
+++ b/bubbob/music/Snd5-8.wav
Binary files differ
diff --git a/bubbob/music/Snd6-8.wav b/bubbob/music/Snd6-8.wav
new file mode 100644
index 0000000..0f2bad5
--- /dev/null
+++ b/bubbob/music/Snd6-8.wav
Binary files differ
diff --git a/bubbob/patmap.py b/bubbob/patmap.py
new file mode 100644
index 0000000..3b00252
--- /dev/null
+++ b/bubbob/patmap.py
@@ -0,0 +1,177 @@
+patmap = {(0, 0, 0): ('pat03.ppm', (0, 120, 24, 24)),
+ (1, 0, 0): ('pat01.ppm', (0, 24, 24, 24)),
+ (2, 0, 0): ('pat09.ppm', (0, 0, 24, 24)),
+ (2, 'l'): ('pat14.ppm', (0, 160, 40, 32)),
+ (3, 0, 0): ('pat06.ppm', (0, 120, 24, 24)),
+ (3, 'l'): ('pat17.ppm', (0, 32, 40, 32)),
+ (4, 0, 0): ('pat04.ppm', (0, 48, 24, 24)),
+ (4, 'l'): ('pat15.ppm', (0, 192, 40, 32)),
+ (5, 0, 0): ('pat01.ppm', (0, 216, 24, 24)),
+ (5, 'l'): ('pat19.ppm', (0, 96, 40, 32)),
+ (6, 0, 0): ('pat09.ppm', (0, 144, 24, 24)),
+ (6, 'l'): ('pat18.ppm', (0, 96, 40, 32)),
+ (7, 0, 0): ('pat07.ppm', (0, 24, 24, 24)),
+ (8, 0, 0): ('pat02.ppm', (0, 48, 24, 24)),
+ (9, 0, 0): ('pat09.ppm', (0, 216, 24, 24)),
+ (9, 'l'): ('pat11.ppm', (0, 64, 40, 32)),
+ (10, 0, 0): ('pat07.ppm', (0, 96, 24, 24)),
+ (10, 'l'): ('pat14.ppm', (0, 96, 40, 32)),
+ (11, 0, 0): ('pat05.ppm', (0, 24, 24, 24)),
+ (11, 'l'): ('pat13.ppm', (0, 32, 40, 32)),
+ (12, 0, 0): ('pat02.ppm', (0, 216, 24, 24)),
+ (12, 'l'): ('pat18.ppm', (0, 0, 40, 32)),
+ (13, 0, 0): ('pat00.ppm', (0, 72, 24, 24)),
+ (13, 'l'): ('pat18.ppm', (0, 128, 40, 32)),
+ (14, 0, 0): ('pat07.ppm', (0, 192, 24, 24)),
+ (15, 0, 0): ('pat05.ppm', (0, 192, 24, 24)),
+ (15, 'l'): ('pat18.ppm', (0, 32, 40, 32)),
+ (16, 0, 0): ('pat06.ppm', (0, 96, 24, 24)),
+ (16, 'l'): ('pat11.ppm', (0, 32, 40, 32)),
+ (17, 0, 0): ('pat04.ppm', (0, 0, 24, 24)),
+ (17, 'l'): ('pat14.ppm', (0, 64, 40, 32)),
+ (18, 0, 0): ('pat01.ppm', (0, 168, 24, 24)),
+ (18, 'l'): ('pat13.ppm', (0, 0, 40, 32)),
+ (19, 0, 0): ('pat09.ppm', (0, 96, 24, 24)),
+ (19, 'l'): ('pat16.ppm', (0, 192, 40, 32)),
+ (20, 0, 0): ('pat07.ppm', (0, 0, 24, 24)),
+ (20, 'l'): ('pat15.ppm', (0, 160, 40, 32)),
+ (21, 0, 0): ('pat04.ppm', (0, 192, 24, 24)),
+ (21, 'l'): ('pat19.ppm', (0, 64, 40, 32)),
+ (22, 0, 0): ('pat02.ppm', (0, 120, 24, 24)),
+ (22, 'l'): ('pat17.ppm', (0, 224, 40, 32)),
+ (23, 0, 0): ('pat10.ppm', (0, 24, 24, 24)),
+ (23, 'l'): ('pat12.ppm', (0, 64, 40, 32)),
+ (24, 0, 0): ('pat05.ppm', (0, 0, 24, 24)),
+ (24, 'l'): ('pat17.ppm', (0, 96, 40, 32)),
+ (25, 0, 0): ('pat02.ppm', (0, 168, 24, 24)),
+ (25, 'l'): ('pat15.ppm', (0, 64, 40, 32)),
+ (26, 0, 0): ('pat00.ppm', (0, 24, 24, 24)),
+ (26, 0, 1): ('pat00.ppm', (0, 0, 24, 24)),
+ (26, 1, 0): ('pat01.ppm', (0, 120, 24, 24)),
+ (26, 1, 1): ('pat08.ppm', (0, 144, 24, 24)),
+ (26, 'l'): ('pat13.ppm', (0, 224, 40, 32)),
+ (27, 0, 0): ('pat07.ppm', (0, 216, 24, 24)),
+ (27, 'l'): ('pat17.ppm', (0, 64, 40, 32)),
+ (28, 0, 0): ('pat05.ppm', (0, 168, 24, 24)),
+ (28, 'l'): ('pat15.ppm', (0, 224, 40, 32)),
+ (29, 0, 0): ('pat03.ppm', (0, 72, 24, 24)),
+ (30, 0, 0): ('pat00.ppm', (0, 216, 24, 24)),
+ (30, 'l'): ('pat17.ppm', (0, 160, 40, 32)),
+ (31, 0, 0): ('pat08.ppm', (0, 168, 24, 24)),
+ (31, 'l'): ('pat12.ppm', (0, 32, 40, 32)),
+ (32, 0, 0): ('pat08.ppm', (0, 0, 24, 24)),
+ (33, 0, 0): ('pat05.ppm', (0, 144, 24, 24)),
+ (33, 'l'): ('pat12.ppm', (0, 192, 40, 32)),
+ (34, 0, 0): ('pat03.ppm', (0, 48, 24, 24)),
+ (35, 0, 0): ('pat00.ppm', (0, 168, 24, 24)),
+ (35, 'l'): ('pat14.ppm', (0, 128, 40, 32)),
+ (36, 0, 0): ('pat07.ppm', (0, 120, 24, 24)),
+ (36, 'l'): ('pat16.ppm', (0, 96, 40, 32)),
+ (37, 0, 0): ('pat06.ppm', (0, 72, 24, 24)),
+ (38, 0, 0): ('pat03.ppm', (0, 216, 24, 24)),
+ (38, 'l'): ('pat11.ppm', (0, 128, 40, 32)),
+ (39, 0, 0): ('pat01.ppm', (0, 96, 24, 24)),
+ (40, 0, 0): ('pat08.ppm', (0, 48, 24, 24)),
+ (41, 0, 0): ('pat04.ppm', (0, 24, 24, 24)),
+ (41, 'l'): ('pat12.ppm', (0, 160, 40, 32)),
+ (42, 0, 0): ('pat01.ppm', (0, 192, 24, 24)),
+ (43, 0, 0): ('pat05.ppm', (0, 120, 24, 24)),
+ (43, 1, 0): ('pat08.ppm', (0, 72, 24, 24)),
+ (44, 0, 0): ('pat07.ppm', (0, 48, 24, 24)),
+ (45, 0, 0): ('pat04.ppm', (0, 216, 24, 24)),
+ (45, 'l'): ('pat16.ppm', (0, 224, 40, 32)),
+ (46, 0, 0): ('pat02.ppm', (0, 144, 24, 24)),
+ (46, 'l'): ('pat16.ppm', (0, 64, 40, 32)),
+ (47, 0, 0): ('pat10.ppm', (0, 72, 24, 24)),
+ (47, 'l'): ('pat19.ppm', (0, 224, 40, 32)),
+ (48, 0, 0): ('pat00.ppm', (0, 120, 24, 24)),
+ (48, 'l'): ('pat12.ppm', (0, 128, 40, 32)),
+ (49, 0, 0): ('pat08.ppm', (0, 96, 24, 24)),
+ (49, 'l'): ('pat11.ppm', (0, 192, 40, 32)),
+ (50, 0, 0): ('pat06.ppm', (0, 24, 24, 24)),
+ (50, 'l'): ('pat16.ppm', (0, 128, 40, 32)),
+ (51, 0, 0): ('pat03.ppm', (0, 144, 24, 24)),
+ (51, 'l'): ('pat14.ppm', (0, 0, 40, 32)),
+ (52, 0, 0): ('pat01.ppm', (0, 72, 24, 24)),
+ (52, 'l'): ('pat15.ppm', (0, 0, 40, 32)),
+ (53, 0, 0): ('pat09.ppm', (0, 48, 24, 24)),
+ (53, 'l'): ('pat16.ppm', (0, 32, 40, 32)),
+ (54, 0, 0): ('pat06.ppm', (0, 192, 24, 24)),
+ (54, 'l'): ('pat19.ppm', (0, 192, 40, 32)),
+ (55, 0, 0): ('pat04.ppm', (0, 96, 24, 24)),
+ (55, 'l'): ('pat18.ppm', (0, 160, 40, 32)),
+ (56, 0, 0): ('pat09.ppm', (0, 120, 24, 24)),
+ (56, 'l'): ('pat18.ppm', (0, 64, 40, 32)),
+ (57, 0, 0): ('pat00.ppm', (0, 144, 24, 24)),
+ (57, 'l'): ('pat12.ppm', (0, 96, 40, 32)),
+ (58, 0, 0): ('pat04.ppm', (0, 168, 24, 24)),
+ (59, 0, 0): ('pat02.ppm', (0, 72, 24, 24)),
+ (59, 'l'): ('pat14.ppm', (0, 192, 40, 32)),
+ (60, 0, 0): ('pat10.ppm', (0, 48, 24, 24)),
+ (60, 'l'): ('pat13.ppm', (0, 96, 40, 64)),
+ (61, 0, 0): ('pat07.ppm', (0, 168, 24, 24)),
+ (62, 0, 0): ('pat05.ppm', (0, 96, 24, 24)),
+ (62, 'l'): ('pat19.ppm', (0, 128, 40, 32)),
+ (63, 0, 0): ('pat03.ppm', (0, 0, 24, 24)),
+ (63, 'l'): ('pat20.ppm', (0, 0, 40, 32)),
+ (64, 0, 0): ('pat02.ppm', (0, 96, 24, 24)),
+ (64, 'l'): ('pat13.ppm', (0, 64, 40, 32)),
+ (65, 0, 0): ('pat10.ppm', (0, 0, 24, 24)),
+ (65, 'l'): ('pat19.ppm', (0, 32, 40, 32)),
+ (66, 0, 0): ('pat07.ppm', (0, 144, 24, 24)),
+ (66, 'l'): ('pat17.ppm', (0, 192, 40, 32)),
+ (67, 0, 0): ('pat05.ppm', (0, 72, 24, 24)),
+ (67, 'l'): ('pat11.ppm', (0, 224, 40, 32)),
+ (68, 0, 0): ('pat03.ppm', (0, 24, 24, 24)),
+ (68, 'l'): ('pat15.ppm', (0, 32, 40, 32)),
+ (69, 0, 0): ('pat00.ppm', (0, 96, 24, 24)),
+ (69, 'l'): ('pat13.ppm', (0, 192, 40, 32)),
+ (70, 0, 0): ('pat08.ppm', (0, 192, 24, 24)),
+ (70, 'l'): ('pat17.ppm', (0, 0, 40, 32)),
+ (71, 0, 0): ('pat06.ppm', (0, 0, 24, 24)),
+ (72, 0, 0): ('pat00.ppm', (0, 192, 24, 24)),
+ (72, 'l'): ('pat15.ppm', (0, 128, 40, 32)),
+ (73, 0, 0): ('pat08.ppm', (0, 120, 24, 24)),
+ (74, 0, 0): ('pat06.ppm', (0, 48, 24, 24)),
+ (75, 0, 0): ('pat03.ppm', (0, 192, 24, 24)),
+ (76, 0, 0): ('pat01.ppm', (0, 144, 24, 24)),
+ (76, 'l'): ('pat11.ppm', (0, 0, 40, 32)),
+ (77, 0, 0): ('pat09.ppm', (0, 72, 24, 24)),
+ (77, 'l'): ('pat14.ppm', (0, 32, 40, 32)),
+ (78, 0, 0): ('pat06.ppm', (0, 216, 24, 24)),
+ (78, 'l'): ('pat13.ppm', (0, 160, 40, 32)),
+ (79, 0, 0): ('pat04.ppm', (0, 144, 24, 24)),
+ (79, 'l'): ('pat16.ppm', (0, 160, 40, 32)),
+ (80, 0, 0): ('pat05.ppm', (0, 48, 24, 24)),
+ (81, 0, 0): ('pat02.ppm', (0, 192, 24, 24)),
+ (81, 'l'): ('pat17.ppm', (0, 128, 40, 32)),
+ (82, 0, 0): ('pat00.ppm', (0, 48, 24, 24)),
+ (82, 'l'): ('pat12.ppm', (0, 0, 40, 32)),
+ (83, 0, 0): ('pat08.ppm', (0, 24, 24, 24)),
+ (83, 'l'): ('pat11.ppm', (0, 96, 40, 32)),
+ (84, 0, 0): ('pat05.ppm', (0, 216, 24, 24)),
+ (84, 'l'): ('pat14.ppm', (0, 224, 40, 32)),
+ (84, 'r'): ('pat18.ppm', (0, 224, 40, 32)),
+ (85, 0, 0): ('pat03.ppm', (0, 96, 24, 24)),
+ (86, 0, 0): ('pat01.ppm', (0, 0, 24, 24)),
+ (87, 0, 0): ('pat08.ppm', (0, 216, 24, 24)),
+ (88, 0, 0): ('pat03.ppm', (0, 168, 24, 24)),
+ (88, 'l'): ('pat20.ppm', (0, 32, 40, 32)),
+ (89, 0, 0): ('pat01.ppm', (0, 48, 24, 24)),
+ (90, 0, 0): ('pat09.ppm', (0, 24, 24, 24)),
+ (90, 'l'): ('pat12.ppm', (0, 224, 40, 32)),
+ (91, 0, 0): ('pat06.ppm', (0, 144, 24, 24)),
+ (92, 0, 0): ('pat04.ppm', (0, 120, 24, 24)),
+ (92, 'l'): ('pat18.ppm', (0, 192, 40, 32)),
+ (93, 0, 0): ('pat02.ppm', (0, 24, 24, 24)),
+ (93, 'l'): ('pat11.ppm', (0, 160, 40, 32)),
+ (94, 0, 0): ('pat09.ppm', (0, 192, 24, 24)),
+ (94, 'l'): ('pat19.ppm', (0, 0, 40, 32)),
+ (95, 0, 0): ('pat07.ppm', (0, 72, 24, 24)),
+ (95, 'l'): ('pat15.ppm', (0, 96, 40, 32)),
+ (96, 0, 0): ('pat06.ppm', (0, 168, 24, 24)),
+ (97, 0, 0): ('pat04.ppm', (0, 72, 24, 24)),
+ (97, 'l'): ('pat16.ppm', (0, 0, 40, 32)),
+ (98, 0, 0): ('pat02.ppm', (0, 0, 24, 24)),
+ (98, 'l'): ('pat19.ppm', (0, 160, 40, 32)),
+ (99, 0, 0): ('pat09.ppm', (0, 168, 24, 24))}
diff --git a/bubbob/player.py b/bubbob/player.py
new file mode 100644
index 0000000..3a336a9
--- /dev/null
+++ b/bubbob/player.py
@@ -0,0 +1,1213 @@
+from __future__ import generators
+import random, math, time
+import gamesrv
+import images
+import boards
+import bubbles
+from boards import *
+from images import ActiveSprite
+from mnstrmap import GreenAndBlue, LetterBubbles, PlayerBubbles
+from mnstrmap import DigitsMisc
+
+KEEPALIVE = 5*60 # seconds
+CheatDontDie = 0
+
+
+class Dragon(ActiveSprite):
+ priority = 1
+ mdef = PlayerBubbles
+ fly_counter = 0
+## glueddown = None
+
+ DCAP = {
+ 'hspeed': 1,
+ 'firerate': 2,
+ 'shootthrust': 8.0,
+ 'infinite_shield': 1,
+ 'shield': 50,
+ 'gravity': 0.21,
+ 'bubbledelay': 0,
+ 'shootbubbles': (),
+ 'pinball': 0,
+## 'nojump': 0,
+ 'autofire': 0,
+ 'ring': 0,
+ 'hotstuff': 0,
+ 'left2right': 1,
+ 'slippy': 0,
+ 'vslippy': 0.0,
+ 'lookforward': 1,
+ 'fly': 0,
+ 'jumpdown': 0,
+ 'flower': 1,
+ 'bigflower': None,
+ 'overlayglasses': 0,
+ 'teleport': 0,
+ 'breakwalls': 0,
+ 'carrying': (),
+ 'key_left': 'key_left',
+ 'key_right': 'key_right',
+ 'key_jump': 'key_jump',
+ 'key_fire': 'key_fire',
+ }
+ SAVE_CAP = {'hspeed': 1,
+ 'firerate': 2,
+ 'shootthrust': 8.0 / 1.5,
+ 'flower': -1}
+
+ def __init__(self, bubber, x, y, dir, dcap=DCAP):
+ self.bubber = bubber
+ self.dir = dir
+ icobubber = dcap.get('bubbericons', bubber)
+ ActiveSprite.__init__(self, icobubber.icons[0, dir], x, y)
+ self.fire = 0
+ self.up = 0.0
+ self.watermoveable = 0
+ self.dcap = dcap.copy()
+ self.dcap.update(self.bubber.pcap)
+ BubPlayer.DragonList.append(self)
+ self.gen.append(self.normal_movements())
+ self.overlaysprite = None
+ self.overlayyoffset = 4
+ self.hatsprite = None
+ self.hatangle = 1
+ self.isdying = 0
+ self.lifegained = 0
+ self.playing_fish = False
+ if BubPlayer.SuperSheep:
+ self.become_monster('Sheep', immed=1)
+ elif BubPlayer.SuperFish:
+ self.become_fish()
+
+ def kill(self):
+ try:
+ BubPlayer.DragonList.remove(self)
+ except ValueError:
+ pass
+ try:
+ self.bubber.dragons.remove(self)
+ except ValueError:
+ pass
+ ActiveSprite.kill(self)
+ if self.hatsprite is not None:
+ if self.hatsprite.alive:
+ self.hatsprite.kill()
+ self.hatsprite = None
+
+ def die(self):
+ if (self in BubPlayer.DragonList and not self.dcap['shield']
+ and not CheatDontDie):
+ BubPlayer.DragonList.remove(self)
+ self.gen = [self.dying()]
+ self.play(images.Snd.Die)
+ #wasting = boards.curboard.wastingplay
+ #if wasting is not None:
+ # wasting[self.bubber] = len(wasting)
+
+ def dying(self, can_loose_letter=1):
+ self.isdying = 1
+ if self.hatsprite is not None:
+ if self.hatsprite.alive:
+ dxy = [3*self.dir, -7]
+ self.hatsprite.gen = [self.hatsprite.parabolic(dxy)]
+ self.hatsprite = None
+ lst = [bonus for timeout, bonus in self.dcap['carrying']
+ if hasattr(bonus, 'buildoutcome')]
+ #if random.random() > 0.2]
+ if lst:
+ # loose some bonuses
+ from bonuses import BonusMaker
+ for bonus in lst:
+ self.bubber.givepoints(-bonus.points)
+ BonusMaker(self.x, self.y, [bonus.nimage],
+ outcome=bonus.buildoutcome())
+ elif self.bubber.letters and random.random() > 0.59 and can_loose_letter:
+ # loose a letter
+ lst = range(6)
+ random.shuffle(lst)
+ for l in lst:
+ lettername = bubbles.extend_name(l)
+ if lettername in self.bubber.letters:
+ s = self.bubber.letters[lettername]
+ del self.bubber.letters[lettername]
+ if isinstance(s, ActiveSprite):
+ s.kill()
+ scoreboard()
+ s = bubbles.LetterBubble(self.bubber.pn, l)
+ s.move(self.x, self.y)
+ break
+ icons = self.getcurrenticons()
+ for i in range(2, 32):
+ mode = 5 + ((i>>1) & 3)
+ self.seticon(icons[mode, self.dir])
+ yield None
+ self.kill()
+ if not self.bubber.dragons:
+ self.bubber.bubberdie()
+ # self.bubber.badpoints = self.bubber.points // 3
+
+ def killing(self):
+ self.kill()
+ if 0:
+ yield None
+
+ def carrybonus(self, bonus, timeout=500):
+ timeout += BubPlayer.FrameCounter
+ lst = list(self.dcap['carrying'])
+ lst.append((timeout, bonus))
+ lst.sort()
+ self.dcap['carrying'] = lst
+
+ def listcarrybonuses(self):
+ return [bonus for timeout, bonus in self.dcap['carrying']]
+
+## def moebius(self):
+## self.dir = -self.dir
+## self.dcap['left2right'] *= -1
+
+ def monstervisible(self):
+ return not self.dcap['ring'] and not self.dcap['shield']
+
+ def getcurrenticons(self, imgtransform=''):
+ if self.playing_fish:
+ imgtransform = 'fish'
+ icobubber = self.dcap.get('bubbericons', self.bubber)
+ try:
+ return icobubber.transformedicons[imgtransform]
+ except KeyError:
+ icons = icobubber.transformedicons[imgtransform] = {}
+ icobubber.loadicons(imgtransform)
+ return icons
+
+ def normal_movements(self):
+ yfp = 0.0
+ hfp = 0
+ angryticks = 0
+ mytime = 0
+ privatetime = 0
+ while 1:
+ dcap = self.dcap
+ self.poplist = [self]
+ carrying = dcap['carrying']
+ while carrying and carrying[0][0] < BubPlayer.FrameCounter:
+ timeout, bonus = carrying.pop(0)
+ if bonus.endaction:
+ bonus.endaction(self)
+ del bonus
+
+ bubber = self.bubber
+ wannafire = bubber.getkey(dcap, 'key_fire')
+ wannajump = bubber.getkey(dcap, 'key_jump')
+ wannago = bubber.wannago(dcap)
+ bottom_up = self.bottom_up()
+ onground1 = (onground,underground)[bottom_up]
+
+ if dcap['autofire']:
+ wannafire = 1
+ if dcap['pinball']:
+ wannajump = 1
+ if dcap['pinball'] > 1:
+ if self.up:
+ self.up *= 0.982 ** dcap['pinball']
+ if dcap['hotstuff']:
+ if not wannago:
+ if self.dir * (random.random()-0.07) < 0:
+ wannago = -1
+ else:
+ wannago = 1
+ wannafire = 1
+ if self.fire > (11 // dcap['hotstuff']):
+ self.fire = 0
+## if dcap['hotstuff'] > 1 and random.random() < 0.4:
+## from bubbles import FireDrop
+## FireDrop(self.x + HALFCELL, self.y + HALFCELL)
+ if wannago:
+ self.dir = wannago * dcap['lookforward']
+ if self.x & 1:
+ self.step(self.dir, 0)
+ if dcap['slippy']:
+ vx = dcap['vslippy']
+ if wannago:
+ vx += wannago * 0.05
+ else:
+ vx *= 0.95
+ if vx < 0.0:
+ wannago = -1
+ else:
+ wannago = 1
+ dcap['vslippy'] = vx
+ mytime = (mytime+dcap['lookforward']) % 12
+ hfp += abs(vx)
+ else:
+ hfp += dcap['hspeed']
+
+## if self.glueddown:
+## if wannajump or not dcap['nojump']:
+## del self.glueddown
+## else:
+## # glued gliding movements
+## self.step(self.x & 1, self.y & 1)
+## if wannago:
+## mytime = (mytime+dcap['lookforward']) % 12
+## else:
+## hfp = 0
+## while hfp > 0 and self.glueddown:
+## gx, gy = self.glueddown
+## dx = wannago*gy
+## dy = -wannago*gx
+## x0 = (self.x + gx + dx) // CELL + 1
+## y0 = (self.y + gy + dy) // CELL + 1
+## if ' ' == bget(x0+dx, y0+dy) == bget(x0+dx-gx, y0+dy-gy):
+## self.step(2*dx, 2*dy)
+## # detached from this wall?
+## x1 = (self.x + gx + dx) // CELL + 1
+## y1 = (self.y + gy + dy) // CELL + 1
+## if (' ' == bget(x1-dx+gx, y1-dy+gy)
+## == bget(x0 +gx, y0 +gy)
+## == bget(x0+dx+gx, y0+dy+gy)):
+## if bget(x0-dx+gx, y0-dy+gy) != ' ':
+## # rotate around the corner
+## self.glueddown = -dx, -dy
+## self.step(2*gx, 2*gy)
+## else:
+## del self.glueddown
+## elif bget(x0-gx, y0-gy) == ' ':
+## # attach to the wall into which we are running
+## if (self.x*dx | self.y*dy) % CELL != 0:
+## if ((((self.x-2*dx)*dx | (self.y-2*dy)*dy) &
+## ((self.x-4*dx)*dx | (self.y-4*dy)*dy))
+## % CELL == 0):
+## self.step(-2*dx, -2*dy)
+## else:
+## del self.glueddown
+## else:
+## self.glueddown = dx, dy
+## else:
+## del self.glueddown
+## self.vertical_warp()
+## hfp -= 0.82 # slightly faster than usual
+ # normal left or right movements
+ breakwalls = dcap['breakwalls']
+ while hfp > 0:
+ hfp -= 1
+ dir = 0
+ if wannago == -1:
+ x0 = (self.x+1)//CELL
+ y0 = (self.y+4 - bottom_up*(CELL+4)) // CELL + 1
+ y0bis = (self.y+CELL-1) // CELL + 1 - bottom_up
+ if bget(x0,y0) == ' ' == bget(x0,y0bis):
+ dir = -1
+ elif breakwalls:
+ self.breakwalls(x0, y0bis, -1)
+ elif wannago == 1:
+ x0 = (self.x-3)//CELL + 2
+ y0 = self.y // CELL + 1 - bottom_up
+ y0bis = (self.y+CELL-1) // CELL + 1 - bottom_up
+ if bget(x0,y0) == ' ' == bget(x0,y0bis):
+ dir = +1
+ elif breakwalls:
+ self.breakwalls(x0, y0bis, 1)
+ self.step(2*dir, 0)
+ if dir:
+ mytime = (mytime+dcap['lookforward']) % 12
+ else:
+ f = - dcap['vslippy'] * (dcap['slippy']+1)/3.0
+ dcap['vslippy'] = max(min(f, 10.0), -10.0)
+ hfp = 0
+ onbubble = None
+ if not dcap['infinite_shield']:
+ touching = images.touching(self.x+1, self.y+1, 30, 30)
+ touching.reverse()
+ for s in touching:
+ if s.touched(self):
+ onbubble = s
+ elif bubber.key_left or bubber.key_right or bubber.key_jump or bubber.key_fire:
+ dcap['infinite_shield'] = 0
+
+ dir = self.dir
+ icons = self.getcurrenticons('vflip' * bottom_up)
+
+ if self.playing_fish:
+ mode = self.one_fish_frame(onground1, bottom_up)
+ elif self.up:
+ # going up
+ mode = 9
+ self.up -= dcap['gravity']
+ if (self.up,-self.up)[bottom_up] < 4.0:
+ self.up = 0.0
+ mode = 10
+ else:
+ ny = self.y + yfp - self.up
+ self.move(self.x, int(ny))
+ yfp = ny - self.y
+ self.vertical_warp()
+ if wannago and dcap['teleport']:
+ for t in self.teleport(wannago, icons, 0):
+ yield t
+ else:
+ # going down or staying on ground
+ if wannajump and onbubble:
+ ground = True
+ onbubble.dragon_jumped = True, bottom_up
+ else:
+ ground = onground1(self.x, self.y)
+ if ground:
+ if wannajump:
+ self.play(images.Snd.Jump)
+ if dcap['jumpdown'] and not onbubble:
+ self.step(0, (1, -1)[bottom_up])
+ mode = 10
+ bubber.emotic(self, 4)
+ else:
+ yfp = 0.0
+ self.up = (7.5,-7.5)[bottom_up]
+ mode = 9
+ else:
+ mode = mytime // 4
+ if wannago and dcap['teleport']:
+ for t in self.teleport(wannago, icons):
+ yield t
+ else:
+ mode = 10
+ if dcap['fly']:
+ self.fly_counter += 1
+ if self.fly_counter < dcap['fly']:
+ ny = self.y
+ else:
+ del self.fly_counter
+ ny = self.y+(1,-1)[bottom_up]
+ else:
+ ny = (self.y+(4,-1)[bottom_up]) & ~3
+ nx = self.x
+ if nx < 32:
+ nx += 2
+ elif nx > boards.bwidth - 64:
+ nx -= 2
+ self.move(nx, ny)
+ self.vertical_warp()
+ if wannago and dcap['teleport']:
+ for t in self.teleport(wannago, icons, 0):
+ yield t
+
+ if wannafire and not self.fire:
+ self.firenow()
+ self.hatangle = 1
+ if self.fire:
+ if self.fire <= 5:
+ mode = 3
+ self.hatangle = 2
+ elif self.fire <= 10:
+ mode = 4
+ self.hatangle = 3
+ self.fire += 1
+ if self.fire >= 64 // dcap['firerate']:
+ self.fire = 0
+
+ s = dcap['shield']
+ if s:
+ if dcap['infinite_shield'] and s < 20:
+ s += 4
+ s -= 1
+ if dcap['overlayglasses']:
+ self.overlayyoffset = ({3: 2, 4: 0,
+ 9: 3, 10: 5}.get(mode, 4)
+ + self.playing_fish * 2)
+ elif s & 2:
+ mode = 11
+ dcap['shield'] = s
+ if dcap['ring']:# and random.random() > 0.1:
+ if dcap['ring'] > 1:
+ mode = 12
+ else:
+ mode = 11
+ self.seticon(icons[mode, dir])
+ self.watermoveable = not wannajump
+
+ privatetime += BubPlayer.PlayersPrivateTime
+ while privatetime >= 100:
+ yield None
+ privatetime -= 100
+
+ if self.angry:
+ if angryticks == 0:
+ s = ActiveSprite(icons[11, self.dir], self.x, self.y)
+ s.gen.append(s.die([None], speed=10))
+ angryticks = 6
+ angryticks -= 1
+ #if BubPlayer.Moebius and BubPlayer.FrameCounter % 5 == 0:
+ # s = ActiveSprite(icons[11, -self.dir],
+ # boards.bwidth - 2*CELL - self.x, self.y)
+ # s.gen.append(s.die([None], speed=2))
+
+ def teleport(self, wannago, icons, max_delta_y=CELL):
+ #if self.dcap['shield']:
+ # return
+ from bonuses import Bonus, Megabonus
+ best_dx = boards.bwidth
+ centerx = self.x + self.ico.w // 2
+ basey = (self.y + self.ico.h + 8) & ~15
+ for s in images.ActiveSprites:
+ if (isinstance(s, Bonus) and s.touchable
+ and abs(s.y+s.ico.h - basey) <= max_delta_y
+ and s.is_on_ground()):
+ dx = (s.x + (wannago < 0 and s.ico.w)) - centerx
+ if dx*wannago > 0:
+ dx = abs(dx) + CELL
+ if dx < best_dx:
+ best_dx = dx
+ best = s
+ if not (42 <= best_dx < boards.bwidth):
+ return
+ self.play(images.Snd.Shh)
+ self.up = 0.0
+ s = best
+ dx = best_dx
+ basey = s.y+s.ico.h
+ desty = basey - self.ico.h
+ dy_dx = float(desty - self.y) / dx
+ self.dir = wannago
+ ico = images.make_darker(icons[0, wannago], True)
+ # speed up
+ fx = self.x
+ fy = self.y
+ curdx = 0.0
+ stepx = 2.0
+ t = 0
+ while 1:
+ if curdx < 0.5*dx:
+ stepx *= 1.13
+ else:
+ stepx /= 1.13
+ fx += wannago * stepx
+ fy += dy_dx * stepx
+ curdx += stepx
+ if curdx >= dx or stepx < 2.0:
+ fx += wannago * (dx - curdx)
+ break
+ self.move(int(fx), int(fy), ico)
+ # make the target bonus bounce a bit
+ if s.alive:
+ dy = (t & 7) * 4
+ if dy > 16:
+ dy = 32-dy
+ s.move(s.x, basey - s.ico.h - dy)
+ t += 1
+ yield None
+ self.move(int(fx), desty)
+ self.dcap['shield'] = 50
+
+ def breakwalls(self, x, y0, dir):
+ if self.dcap['breakwalls'] > BubPlayer.FrameCounter:
+ return # wait before breaking more walls
+ if not (2 <= x < boards.curboard.width-2):
+ return
+ ys = []
+ for y in (y0, y0-1):
+ if 0 <= y < boards.curboard.height and bget(x, y) == '#':
+ ys.append(y)
+ if len(ys) == 2:
+ from bonuses import DustStar
+ dir *= self.dcap['hspeed']
+ for y in ys:
+ w = boards.curboard.killwall(x, y)
+ s = ActiveSprite(w.ico, w.x, w.y)
+ dxy = [dir+random.random()-0.5,
+ -random.random()*3.0]
+ DustStar(w.x, w.y, dxy[0], dxy[1], big=0)
+ s.gen.append(s.parabolic(dxy))
+ self.dcap['breakwalls'] = BubPlayer.FrameCounter + 40
+
+ def enter_new_board(self):
+ self.playing_fish = False
+ self.lifegained = 0
+
+ def become_fish(self):
+ self.playing_fish = True
+ icons = self.getcurrenticons()
+ self.seticon(icons[11, self.dir])
+
+ def one_fish_frame(self, onground1, bottom_up):
+ if self.bubber.getkey(self.dcap, 'key_jump'):
+ # swimming up
+ self.step(0, (-2, 2)[bottom_up])
+ else:
+ if random.random() < 0.05:
+ bubbles.FishBubble(self)
+ if not onground1(self.x, self.y):
+ # swimming down
+ ny = (self.y+(2,-1)[bottom_up]) & ~1
+ self.move(self.x, ny)
+ self.vertical_warp()
+ return ((BubPlayer.FrameCounter // 3) % 6) * 0.5
+
+ def to_front(self):
+ ActiveSprite.to_front(self)
+ if self.dcap['overlayglasses']:
+ ico = images.sprget(('glasses', self.dir))
+ y = self.y + self.overlayyoffset
+ if self.overlaysprite is None or not self.overlaysprite.alive:
+ self.overlaysprite = images.ActiveSprite(ico, self.x, y)
+ else:
+ self.overlaysprite.to_front()
+ self.overlaysprite.move(self.x, y, ico)
+ self.overlaysprite.gen = [self.overlaysprite.die([None])]
+
+ def bottom_up(self):
+ return self.dcap['gravity'] < 0.0
+
+ def watermove(self, x, y):
+ # for WaterCell.flooding()
+ if self in BubPlayer.DragonList and self.watermoveable:
+ self.watermoveable = 0
+ self.move(x, y)
+ self.up = 0.0
+ if self.dcap['shield'] < 6:
+ self.dcap['shield'] = 6
+ if self.fire <= 10:
+ self.fire = 11
+
+ def become_monster(self, clsname, big=0, immed=0):
+ if self in BubPlayer.DragonList:
+ BubPlayer.DragonList.remove(self)
+
+ import monsters, mnstrmap
+ mcls = getattr(monsters, clsname)
+ mdef = getattr(mnstrmap, clsname)
+ m = mcls(mdef, self.x, self.y, self.dir, in_list=self.bubber.dragons)
+ m.become_monster(self.bubber, self.dcap, big, immed)
+ self.seticon(m.ico)
+ self.gen = [self.killing()]
+
+ def become_bubblingeyes(self, bubble):
+ if self in BubPlayer.DragonList:
+ self.bubber.emotic(self, 4)
+ BubPlayer.DragonList.remove(self)
+
+ import bubbles
+ bubble.to_front()
+ m = bubbles.BubblingEyes(self.bubber, self.dcap, bubble)
+ self.bubber.dragons.append(m)
+ self.gen = [self.killing()]
+ return 1
+ else:
+ return 0
+
+ def firenow(self):
+ self.fire = 1
+ #if boards.curboard.wastingplay is None:
+ shootbubbles = self.dcap['shootbubbles']
+ special_bubbles = shootbubbles and shootbubbles.pop()
+ thrustfactors = None
+ N = self.dcap['flower']
+ if N == 1:
+ angles = [0]
+ elif N > 1:
+ angles = [i*(2.0*math.pi/N) for i in range(N)]
+ self.dcap['flower'] = N*2//3
+ elif N > -16: # triple fire, possibly cumulative
+ angles = [0]
+ for i in range(1, -N+1):
+ angles.append(i * 0.19)
+ angles.append(i * -0.19)
+ else: # heptuple fire
+ c = 0.17
+ a = math.sqrt(1-c+c*c)
+ alpha = math.atan2(math.sqrt(3)/2*c, 1-c/2)
+ b = math.sqrt(1+c+c*c)
+ beta = math.atan2(math.sqrt(3)/2*c, 1+c/2)
+ angles = [0, 0, 0, alpha, -alpha, beta, -beta]
+ thrustfactors = [1, 1-c, 1+c, a, a, b, b]
+ dir = self.dir
+ x = self.x
+## if self.glueddown:
+## gx, gy = self.glueddown
+## dir = dir*gy
+## if not dir:
+## dir = 1
+## delta = self.dir*gx * math.pi/2
+## angles = [angle-delta for angle in angles]
+## x -= 16
+ if self.dcap['hotstuff'] > 1:
+ base = (random.random()-0.5)*math.pi
+ angles = [a + base for a in angles]
+ if self.dcap['bigflower'] is not None:
+ N = 45
+ angle = BubPlayer.FrameCounter - self.dcap['bigflower']
+ if not (0 <= angle < N):
+ self.dcap['bigflower'] = BubPlayer.FrameCounter
+ angle = 0
+ angles = [-angle * (2.0*math.pi/N) * self.dir]
+ thrustfactors = None
+ self.fire = max(1, 64 // self.dcap['firerate'] - 2)
+ if self.dcap['autofire'] >= 1:
+ self.dcap['autofire'] -= 1
+ if not thrustfactors:
+ thrustfactors = [None] * len(angles)
+ import bonuses
+ for angle, thrustfactor in zip(angles, thrustfactors):
+ args = (self, x + 4*dir, self.y, dir,
+ special_bubbles, angle, thrustfactor,
+ self.dcap['shootthrust'])
+ bonuses.record_shot(args)
+ bubbles.DragonBubble(*args)
+ #else:
+ # from monsters import DragonShot
+ # DragonShot(self)
+
+class BubPlayer(gamesrv.Player):
+ # global state
+ FrameCounter = 0
+ PlayerList = []
+ DragonList = []
+ MonsterList = []
+ LimitScore = 0
+ LimitScoreColor = None
+ LimitTime = None
+ PlayersPrivateTime = 100
+ SuperSheep = False
+ SuperFish = False
+ #HighScore = 0
+ #HighScoreColor = None
+
+ INIT_BOARD_CAP = {
+ #'LatestLetsGo': -999,
+ 'BubblesBecome': None,
+ 'MegaBonus': None,
+ 'BaseFrametime': 1.0,
+ 'LeaveBonus': None,
+## 'Moebius': 0,
+ 'OverridePlayerIcon': None,
+ 'DisplayPoints': None,
+ 'SuperSheep': False,
+ 'SuperFish' : False,
+ }
+ TRANSIENT_DATA = ('_client', 'key_left', 'key_right',
+ 'key_jump', 'key_fire', 'pn', 'nameicons',
+ 'icons', 'transformedicons',
+ 'standardplayericon', 'iconnames')
+
+ FISH_MODE_MAP = {0: ('', 0), # swim
+ 0.5: ('', 1),
+ 1: ('', 2),
+ 1.5: ('', 3),
+ 2: ('', 2),
+ 2.5: ('', 1),
+ 3: ('', 4), # lancer de bulle
+ 4: ('', 5),
+ 5: ('', 3), # mort
+ 6: ('cw', 3),
+ 7: ('rot180', 3),
+ 8: ('ccw', 3),
+ 9: ('', 3), # saut, montant
+ 10: ('', 3), # saut, descend
+ 11: ('', 6), # shielded
+ 12: 'black',
+ }
+
+ def __init__(self, n):
+ self.pn = n
+ self.icons = {}
+ self.transformedicons = {'': self.icons}
+ self.standardplayericon = images.sprget(GreenAndBlue.players[n][3])
+ self.iconnames = {
+ (0, -1): GreenAndBlue.players[n][0], # walk
+ (0, +1): GreenAndBlue.players[n][3],
+ (1, -1): GreenAndBlue.players[n][1],
+ (1, +1): GreenAndBlue.players[n][4],
+ (2, -1): GreenAndBlue.players[n][2],
+ (2, +1): GreenAndBlue.players[n][5],
+ (3, -1): GreenAndBlue.players[n][6], # lancer de bulle
+ (3, +1): GreenAndBlue.players[n][8],
+ (4, -1): GreenAndBlue.players[n][7],
+ (4, +1): GreenAndBlue.players[n][9],
+ (5, -1): GreenAndBlue.players[n][0], # mort
+ (5, +1): GreenAndBlue.players[n][0],
+ (6, -1): GreenAndBlue.players[n][11],
+ (6, +1): GreenAndBlue.players[n][10],
+ (7, -1): GreenAndBlue.players[n][12],
+ (7, +1): GreenAndBlue.players[n][12],
+ (8, -1): GreenAndBlue.players[n][10],
+ (8, +1): GreenAndBlue.players[n][11],
+ (9, -1): GreenAndBlue.jumping_players[n][2], # saut, montant
+ (9, +1): GreenAndBlue.jumping_players[n][3],
+ (10,-1): GreenAndBlue.jumping_players[n][0], # saut, descend
+ (10,+1): GreenAndBlue.jumping_players[n][1],
+ (11,-1): 'shield-left', # shielded
+ (11,+1): 'shield-right',
+ (12,-1): 'black', # totally invisible
+ (12,+1): 'black',
+ }
+ self.nameicons = []
+ self.team = -1
+ self.reset()
+
+ def reset(self):
+ self.letters = {}
+ #self.bonbons = 0
+ self.points = 0
+ self.nextextralife = gamesrv.game.extralife
+ self.lives = boards.get_lives()
+ self.lifegained = 0
+ #self.badpoints = 0
+ self.pcap = {}
+ self.dragons = []
+ self.keepalive = None
+ self.stats = {'bubble': 0, 'die': 0}
+
+ def loadicons(self, flip):
+ icons = self.transformedicons[flip]
+ if flip == 'fish':
+ for dir in (-1, 1):
+ for key, value in self.FISH_MODE_MAP.items():
+ if value == 'black':
+ flip = ''
+ else:
+ flip, index = value
+ value = GreenAndBlue.fish[self.pn][index]
+ if dir > 0:
+ flip = flip or 'hflip'
+ icons[key, dir] = images.sprget((flip, value))
+ else:
+ for key, value in self.iconnames.items():
+ icons[key] = images.sprget((flip, value))
+
+ def setplayername(self, name):
+ name = name.strip()
+ for t in [0, 1]:
+ if name.endswith('(%d)' % (t+1)):
+ self.team = t
+ name = name[:-3].strip()
+ #print "New player in team", t, "with name", name
+ break
+ else:
+ self.team = -1
+ #print "New player with no team:", name
+ icons = [images.sprcharacterget(c) for c in name]
+ self.nameicons = [ico for ico in icons if ico is not None][:16]
+ self.nameicons.reverse()
+ scoreboard()
+
+ def playerjoin(self):
+ n = self.pn
+ if not self.icons:
+ self.loadicons(flip='')
+ self.keepalive = None
+ if self.points or self.letters:
+ print 'New player continues at position #%d.' % n
+ else:
+ print 'New player is at position #%d.' % n
+ self.reset()
+ self.key_left = 0
+ self.key_right = 0
+ self.key_jump = 0
+ self.key_fire = 0
+ players = [p for p in BubPlayer.PlayerList
+ if p.isplaying() and p is not self]
+ self.enterboard(players)
+ scoreboard()
+ #if BubPlayer.LatestLetsGo < BubPlayer.FrameCounter - 30:
+ images.Snd.LetsGo.play()
+ #BubPlayer.LatestLetsGo = BubPlayer.FrameCounter
+
+ def playerleaves(self):
+ print 'Closing position #%d.' % self.pn
+ self.savecaps()
+ self.zarkoff()
+ self.keepalive = time.time() + KEEPALIVE
+ scoreboard()
+
+ def sameteam(self, other):
+ return self.team != -1 and self.team == other.team
+
+ def enterboard(self, players):
+ players = [p for p in players if not p.sameteam(self)]
+ leftplayers = [p for p in players if p.start_left]
+ rightplayers = [p for p in players if not p.start_left]
+ self.start_left = (len(leftplayers) + random.random() <
+ len(rightplayers) + random.random())
+ self.lifegained = 0
+
+ def savecaps(self):
+ self.pcap = {}
+ dragons = self.dragons
+ if dragons:
+ for key, minimum in Dragon.SAVE_CAP.items():
+ self.pcap[key] = max(minimum,
+ max([d.dcap[key] for d in dragons]))
+
+ def zarkoff(self):
+ for d in self.dragons[:]:
+ d.kill()
+ del self.dragons[:]
+
+ def zarkon(self):
+ if self.key_left + self.key_right >= 1999997:
+ for dragon in self.dragons:
+ self.emotic(dragon, 6)
+ self.key_left = self.key_right = 900000
+ if self.key_left: self.key_left -= 1
+ if self.key_right: self.key_right -= 1
+ if self.key_jump: self.key_jump -= 1
+ if self.key_fire: self.key_fire -= 1
+ #if self.badpoints and not (self.FrameCounter & 7):
+ # percent = (int(self.points*0.0000333)+1) * 100
+ # decr = min(self.badpoints, percent)
+ # self.badpoints -= decr
+ # self.givepoints(-decr)
+ if boards.curboard and not self.dragons and self.lives != 0:
+ #wasting = boards.curboard.wastingplay
+ #if wasting is not None and self in wasting:
+ # return
+ if self.start_left:
+ x0 = 3*CELL
+ dir = 1
+ else:
+ x0 = boards.bwidth - 5*CELL
+ dir = -1
+ y = boards.bheight - 3*CELL
+ for x in [x0, x0+4*dir, x0+8*dir, x0+12*dir, x0+16*dir,
+ x0-4*dir, x0-8*dir, x0]:
+ if onground(x,y):
+ for d in BubPlayer.DragonList:
+ if d.y == y and abs(d.x-x) <= 5:
+ break
+ else:
+ break
+ self.dragons.append(Dragon(self, x, y, dir))
+ for key in self.pcap.keys():
+ if key not in ('teleport', 'jumpdown'):
+ del self.pcap[key]
+
+ def kLeft(self):
+ if self.key_left <= 1:
+ self.key_left = 1000000
+ def kmLeft(self):
+ self.key_left = (self.key_left == 1000000)
+ def kRight(self):
+ if self.key_right <= 1:
+ self.key_right = 1000000
+ def kmRight(self):
+ self.key_right = (self.key_right == 1000000)
+ def kJump(self):
+ if self.key_jump <= 1:
+ self.key_jump = 1000000
+ def kmJump(self):
+ self.key_jump = (self.key_jump == 1000000)
+ def kFire(self):
+ if self.key_fire <= 1:
+ self.key_fire = 1000000
+ def kmFire(self):
+ self.key_fire = (self.key_fire == 1000000)
+
+ def bubberdie(self):
+ self.stats['die'] += 1
+ if self.lives is not None and self.lives > 0:
+ self.lives -= 1
+ scoreboard()
+
+ def getkey(self, dcap, key_name):
+ return getattr(self, dcap[key_name])
+
+ def setkey(self, dcap, key_name, value):
+ setattr(self, dcap[key_name], value)
+
+ def wannago(self, dcap):
+ return dcap['left2right'] * cmp(self.getkey(dcap, 'key_right'),
+ self.getkey(dcap, 'key_left'))
+
+ def turn_single_shot(self, dcap):
+ for name in ('key_left', 'key_right'):
+ n = self.getkey(dcap, name)
+ if n < 999997 and n != 1:
+ self.setkey(dcap, name, 0)
+ wannago = self.wannago(dcap)
+ for name in ('key_left', 'key_right'):
+ self.setkey(dcap, name, 0)
+ return wannago
+
+ def givepoints(self, points):
+ self.points += points
+ if self.points < 0:
+ self.points = 0
+ while self.points >= self.nextextralife:
+ if self.lives is not None and self.lives > 0:
+ if gamesrv.game.lifegainlimit is None or self.lifegained < gamesrv.game.lifegainlimit:
+ if self.dragons:
+ dragon = random.choice(self.dragons)
+ dragon.play(images.Snd.Extralife)
+ else:
+ images.Snd.Extralife.play()
+ self.lives += 1
+ self.lifegained += 1
+ self.nextextralife += gamesrv.game.extralife
+ if self.LimitScoreColor is not None and self.points >= self.LimitScore:
+ boards.replace_boardgen(boards.game_over(), 1)
+ #if self.points > BubPlayer.HighScore:
+ # BubPlayer.HighScore = self.points
+ # BubPlayer.HighScoreColor = self.pn
+ scoreboard()
+
+ def giveletter(self, l, promize=100000):
+
+ #logf = open('log', 'a')
+ #print >> logf 'giveletter %d:' % self.pn, l
+ #logf.close()
+
+ lettername = bubbles.extend_name(l)
+ if lettername not in self.letters:
+ self.letters[lettername] = 1
+## nimage = getattr(LetterBubbles, lettername)
+## x0, y0 = self.infocoords()
+## s = images.ActiveSprite(images.sprget(nimage[1]), x0+l*(CELL-1), y0 - 3*CELL)
+## s.gen.append(s.cyclic([nimage[1], nimage[2], nimage[1], nimage[0]], 7))
+ scoreboard()
+ if len(self.letters) == 6:
+ import monsters
+ monsters.argh_em_all()
+ import bonuses
+ if self.dragons:
+ for i in range(3):
+ dragon = random.choice(self.dragons)
+ bonuses.starexplosion(dragon.x, dragon.y, 1)
+ for lettername in self.letters:
+ dragon = random.choice(self.dragons)
+ nimages = getattr(LetterBubbles, lettername)
+ bonuses.Parabolic2(dragon.x, dragon.y, nimages)
+ dragon = random.choice(self.dragons)
+ dragon.play(images.Snd.Extralife)
+ music = [images.music_old]
+ boards.replace_boardgen(boards.last_monster_killed(460, music))
+ self.givepoints(promize)
+
+ def emotic(self, dragon, strenght):
+ bottom_up = hasattr(dragon, 'bottom_up') and dragon.bottom_up()
+ vshift = getattr(dragon, 'up', 0.0)
+ for i in range(7):
+ angle = math.pi/6 * i
+ dx, dy = -math.cos(angle), -math.sin(angle)
+ nx = random.randrange(3,12)*dx
+ ny = random.randrange(3,9)*dy - 12
+ if bottom_up:
+ dy = -dy
+ ny = -ny
+ e = ActiveSprite(images.sprget(('vflip'*bottom_up, ('emotic', i))),
+ int(dragon.x + 8 + nx),
+ int(dragon.y + 8 + ny - vshift))
+ e.gen.append(e.straightline((3.3+random.random())*dx, (2.3+random.random())*dy))
+ e.gen.append(e.die([None], strenght))
+
+
+def upgrade(p):
+ p.__class__ = BubPlayer
+ p.key_left = 0
+ p.key_right = 0
+ p.key_jump = 0
+ p.key_fire = 0
+ p.dragons = []
+
+
+def xyiconumber(digits, x, y, pts, lst, width=7):
+ if pts >= 10**width:
+ pts = 10**width-1
+ for l in range(width):
+ ico = images.sprget(digits[pts % 10])
+ lst.append((x + (ico.w+1)*(width-1-l), y, ico))
+ pts = pts//10
+ if not pts:
+ break
+ return lst[-1][0]
+
+def scoreboard(reset=0, inplace=0, compresslimittime=0):
+ endgame = 1
+ if reset:
+ for p in BubPlayer.PlayerList:
+ if inplace:
+ for s in p.letters.values():
+ if isinstance(s, ActiveSprite):
+ s.kill()
+ if len(p.letters) == 6:
+ p.letters.clear()
+ for key in p.letters:
+ p.letters[key] = 2
+ brd = boards.curboard
+ if not brd or not gamesrv.sprites_by_n:
+ return
+ lst = []
+ bubblesshown = {}
+ plist = []
+ teamslist = [[], []]
+ teamspoints = [0, 0]
+ for p in BubPlayer.PlayerList:
+ if p.isplaying():
+ if p.lives != 0:
+ endgame = 0
+ else:
+ if not p.keepalive:
+ continue
+ if p.keepalive < time.time():
+ p.reset()
+ continue
+ if BubPlayer.DisplayPoints is not None:
+ points = BubPlayer.DisplayPoints(p)
+ else:
+ points = p.points
+ if p.team == -1:
+ plist.append((points, p, None))
+ else:
+ teamslist[p.team].append((points,p))
+ teamspoints[p.team] += points
+ teamslist[0].sort()
+ teamslist[1].sort()
+ plist.append((teamspoints[0], None, teamslist[0]))
+ plist.append((teamspoints[1], None, teamslist[1]))
+ plist.sort()
+ x0 = boards.bwidth
+ y0 = boards.bheight
+ for score, p, t in plist:
+ if p:
+ if p.lives == 0:
+ ico = images.sprget(GreenAndBlue.gameover[p.pn][0])
+ elif p.icons:
+ if p.isplaying():
+ mode = 0
+ else:
+ mode = 11
+ ico = BubPlayer.OverridePlayerIcon or p.icons[mode, -1]
+ lst.append((x0+9*CELL-ico.w, y0-ico.h, ico))
+ #if boards.curboard.wastingplay is None:
+ for l in range(6):
+ name = bubbles.extend_name(l)
+ if name in p.letters:
+ x, y = x0+l*(CELL-1), y0-3*CELL
+ imglist = getattr(LetterBubbles, name)
+ ico = images.sprget(imglist[1])
+ if gamesrv.game.End in (0, 1):
+ s = p.letters[name]
+ if (isinstance(s, ActiveSprite) and
+ BubPlayer.FrameCounter <= s.timeout):
+ s.move(x, y)
+ bubblesshown[s] = 1
+ continue
+ if s == 1:
+ s = ActiveSprite(ico, x, y)
+ s.setimages(s.cyclic([imglist[0], imglist[1],
+ imglist[2], imglist[1]]))
+ s.timeout = BubPlayer.FrameCounter + 500
+ p.letters[name] = s
+ bubblesshown[s] = 1
+ continue
+ lst.append((x, y, ico))
+ ## else:
+ ## ico = images.sprget(Bonuses.blue_sugar)
+ ## lst.append((x0+12, y0-3*CELL-8, ico))
+ ## xyiconumber(DigitsMisc.digits_white, x0-19, y0-3*CELL+5,
+ ## p.bonbons, lst)
+ xyiconumber(GreenAndBlue.digits[p.pn], x0+2, y0-18, score, lst)
+ if p.lives is not None and p.lives > 0:
+ xyiconumber(DigitsMisc.digits_white, x0+7*CELL, y0-18,
+ p.lives, lst, width=2)
+ x = x0+13*HALFCELL
+ for ico in p.nameicons:
+ x -= 7
+ lst.append((x, y0-35, ico))
+ y0 -= 7*HALFCELL
+ else: # Team
+ for pscore, p in t:
+ if p.lives == 0:
+ ico = images.sprget(GreenAndBlue.gameover[p.pn][0])
+ elif p.icons:
+ if p.isplaying():
+ mode = 0
+ else:
+ mode = 11
+ ico = BubPlayer.OverridePlayerIcon or p.icons[mode, -1]
+ lst.append((x0+9*CELL-ico.w, y0-ico.h, ico))
+ for l in range(6):
+ name = bubbles.extend_name(l)
+ if name in p.letters:
+ x, y = x0+l*(CELL-1), y0-2*CELL
+ imglist = getattr(LetterBubbles, name)
+ ico = images.sprget(imglist[1])
+ if gamesrv.game.End in (0, 1):
+ s = p.letters[name]
+ if (isinstance(s, ActiveSprite) and
+ BubPlayer.FrameCounter <= s.timeout):
+ s.move(x, y)
+ bubblesshown[s] = 1
+ continue
+ if s == 1:
+ s = ActiveSprite(ico, x, y)
+ s.setimages(s.cyclic([imglist[0], imglist[1],
+ imglist[2], imglist[1]]))
+ s.timeout = BubPlayer.FrameCounter + 500
+ p.letters[name] = s
+ bubblesshown[s] = 1
+ continue
+ lst.append((x, y, ico))
+ x = x0+13*HALFCELL
+ for ico in p.nameicons:
+ x -= 7
+ lst.append((x, y0-19, ico))
+ y0 -= 4*HALFCELL
+ if t != []:
+ xyiconumber(GreenAndBlue.digits[t[-1][1].pn], x0+2, y0-18, score, lst)
+ ico = images.sprget(('hat', p.team, -1, 1))
+ lst.append((x0+9*CELL-ico.w, y0-ico.h+16, ico))
+ y0 -= 5*HALFCELL
+ for p in BubPlayer.PlayerList:
+ for name, s in p.letters.items():
+ if isinstance(s, ActiveSprite) and s not in bubblesshown:
+ p.letters[name] = 2
+ s.kill()
+ compressable = len(lst)
+ #if BubPlayer.HighScoreColor is not None:
+ # x = xyiconumber(GreenAndBlue.digits[BubPlayer.HighScoreColor],
+ # x0+2*CELL, HALFCELL, BubPlayer.HighScore, lst)
+ # ico = images.sprget(GreenAndBlue.players[BubPlayer.HighScoreColor][3])
+ # lst.append((x-5*HALFCELL, 1, ico))
+ if BubPlayer.LimitScoreColor is not None:
+ xyiconumber(GreenAndBlue.digits[BubPlayer.LimitScoreColor],
+ x0+2*CELL, HALFCELL, BubPlayer.LimitScore, lst)
+ if BubPlayer.LimitTime is not None:
+ seconds = int(BubPlayer.LimitTime)
+ xyiconumber(DigitsMisc.digits_white, x0+2*CELL, HALFCELL,
+ seconds // 60, lst, width=3)
+ ico = images.sprget('colon')
+ lst.append((x0+5*CELL-1, HALFCELL+1, ico))
+ seconds = seconds % 60
+ ico = images.sprget(DigitsMisc.digits_white[seconds // 10])
+ lst.append((x0+6*CELL, HALFCELL, ico))
+ ico = images.sprget(DigitsMisc.digits_white[seconds % 10])
+ lst.append((x0+6*CELL+ico.w, HALFCELL, ico))
+ ymin = HALFCELL + ico.h
+ elif compresslimittime:
+ ico = images.sprget(DigitsMisc.digits_white[0])
+ ymin = HALFCELL + ico.h
+ else:
+ ymin = 0
+ if not brd.bonuslevel:
+ if brd.num < 99:
+ xyiconumber(DigitsMisc.digits_white, 2, 2, brd.num+1, lst, width=2)
+ else:
+ xyiconumber(DigitsMisc.digits_white, 2, 2, brd.num+1, lst, width=3)
+
+ # compress the scoreboard vertically if it doesn't fit
+ ymin += HALFCELL
+ if y0 < ymin:
+ factor = float(boards.bheight-ymin) / (boards.bheight-y0)
+ shift = ymin - y0*factor + 0.5
+ for i in range(compressable):
+ x, y, ico = lst[i]
+ lst[i] = x, int((y+ico.h)*factor+shift)-ico.h, ico
+
+ brd.writesprites('scoreboard', lst)
+
+ if gamesrv.game.End in (0, 1):
+ gamesrv.game.End = endgame
+
+
+# initialize global board data
+def reset_global_board_state():
+ for key, value in BubPlayer.INIT_BOARD_CAP.items():
+ setattr(BubPlayer, key, value)
+reset_global_board_state()
diff --git a/bubbob/ranking.py b/bubbob/ranking.py
new file mode 100644
index 0000000..d4a4d22
--- /dev/null
+++ b/bubbob/ranking.py
@@ -0,0 +1,391 @@
+from __future__ import generators
+import random
+import boards, images, gamesrv
+from boards import CELL, HALFCELL
+from mnstrmap import DigitsMisc, Flood, GreenAndBlue
+from bubbles import Bubble
+from bonuses import Points
+from player import BubPlayer
+
+MARGIN = 22
+VMARGIN = 12
+
+class RPicture:
+ def __init__(self):
+ self.icons = []
+ def put(self, ico, dx=0, dy=0):
+ self.icons.append((dx, dy, ico))
+ def getsize(self):
+ if self.icons:
+ return (max([dx+ico.w for dx, dy, ico in self.icons]) + MARGIN,
+ max([dy+ico.h for dx, dy, ico in self.icons]))
+ else:
+ return 0, 0
+ def render(self, x, y):
+ return [gamesrv.Sprite(ico, x+dx, y+dy) for dx, dy, ico in self.icons]
+
+class RPoints:
+ def __init__(self, bubber, nbpoints):
+ self.bubber = bubber
+ self.nbpoints = nbpoints
+ def getsize(self):
+ return 0, 0
+ def render(self, x, y):
+ Points(x, y, self.bubber.pn, self.nbpoints)
+ return []
+
+class RNumber(RPicture):
+ map = {'%': 'percent'}
+ for digit in range(10):
+ map[str(digit)] = DigitsMisc.digits_white[digit]
+
+ def __init__(self, text):
+ RPicture.__init__(self)
+ x = 0
+ for c in text:
+ ico = images.sprget(self.map[c])
+ self.put(ico, dx=x)
+ x += ico.w+1
+
+class RText(RPicture):
+ def __init__(self, text, margin=VMARGIN):
+ RPicture.__init__(self)
+ x = 0
+ for c in text:
+ ico = images.sprcharacterget(c)
+ if ico is not None:
+ self.put(ico, dx=x)
+ x += 7
+ self.margin = margin
+ def getsize(self):
+ w, h = RPicture.getsize(self)
+ h -= (VMARGIN-self.margin)
+ return w, h
+
+
+def linesize(line):
+ width = MARGIN
+ height = 0
+ for item in line:
+ w, h = item.getsize()
+ width += w
+ if h > height:
+ height = h
+ return width, height
+
+def display(lines, timeleft, bgen=None, black=0):
+ waves = []
+ if lines:
+ totalwidth = 0
+ totalheight = 0
+ for line in lines:
+ w, h = linesize(line)
+ if w > totalwidth:
+ totalwidth = w
+ totalheight += h
+ heightmargin = (boards.bheight-2*CELL - totalheight) // (len(lines)+1)
+ if heightmargin > VMARGIN:
+ heightmargin = VMARGIN
+ totalheight += heightmargin * (len(lines)+1)
+
+ # size in number of CELLs
+ cwidth = (totalwidth+CELL-1) // CELL
+ cheight = (totalheight+CELL-1) // CELL
+
+ x0 = ((boards.width - cwidth) // 2) * CELL + HALFCELL
+ y0 = ((boards.height - cheight) // 2) * CELL + HALFCELL
+ extras = boards.curboard.sprites.setdefault('ranking', [])
+ #while extras:
+ # extras.pop().kill()
+ # yield 0.12
+ vspeed = -4
+ while extras:
+ nextras = []
+ for s in extras:
+ s.step(0, vspeed)
+ if s.y + s.ico.h <= 0:
+ s.kill()
+ else:
+ nextras.append(s)
+ extras[:] = nextras
+ yield 1
+ vspeed -= 1
+
+ # draw the box filled with water
+ original_y0 = y0
+ wallicon = boards.patget((boards.curboard.num, 0, 0), images.KEYCOL)
+ if black:
+ fillicon = images.sprget('gameoverbkgnd')
+ waveicons = [wallicon]
+ y0 = boards.bheight+CELL
+ else:
+ fillicon = images.sprget(Flood.fill)
+ waveicons = [images.sprget(n) for n in Flood.waves]
+ for y in range(y0-CELL, y0+cheight*CELL+CELL, CELL):
+ w = gamesrv.Sprite(wallicon, x0-CELL, y)
+ extras.append(w)
+ for x in range(x0, x0+cwidth*CELL, CELL):
+ w = gamesrv.Sprite(wallicon, x, y0+cheight*CELL)
+ extras.append(w)
+ w = gamesrv.Sprite(waveicons[-1], x, y0-CELL)
+ extras.append(w)
+ waves.append(w)
+ for y in range(y0, y0+cheight*CELL, CELL):
+ w = gamesrv.Sprite(fillicon, x, y)
+ extras.append(w)
+ for y in range(y0-CELL, y0+cheight*CELL+CELL, CELL):
+ w = gamesrv.Sprite(wallicon, x0+cwidth*CELL, y)
+ extras.append(w)
+
+ # draw the individual items inside
+ y = y0 + totalheight
+ lines.reverse()
+ for line in lines:
+ linew, lineh = linesize(line)
+ x = x0 + MARGIN
+ y -= (lineh + heightmargin)
+ for item in line:
+ w, h = item.getsize()
+ extras += item.render(x, y+(lineh-h)//2)
+ x += w
+
+ vspeed = 0
+ while y0 > original_y0:
+ vspeed = max(vspeed-1, original_y0 - y0)
+ y0 += vspeed
+ for s in extras:
+ s.step(0, vspeed)
+ yield 1
+
+ while timeleft > 0.0:
+ if waves:
+ ico = waveicons.pop(0)
+ waveicons.append(ico)
+ for w in waves:
+ w.seticon(ico)
+ for i in range(2):
+ if bgen is None:
+ t = boards.normal_frame()
+ else:
+ try:
+ t = bgen.next()
+ except StopIteration:
+ timeleft = 0.0
+ break
+ timeleft -= t
+ yield t
+
+# ____________________________________________________________
+
+def ranking_picture(results, maximum, givepoints):
+ if maximum is None:
+ maximum = 0
+ for n in results.values():
+ maximum += n
+ maximum = maximum or 1
+ ranking = []
+ teamrank = [0, 0]
+ teamplayers = [[], []]
+ for p, n in results.items():
+ if p.team != -1:
+ teamrank[p.team] += n
+ teamplayers[p.team].append((n,p))
+ else:
+ ranking.append((n, random.random(), p))
+ teamplayers[0].sort()
+ teamplayers[0].reverse()
+ teamplayers[1].sort()
+ teamplayers[1].reverse()
+ if teamplayers[0] != []:
+ ranking.append((teamrank[0], random.random(), teamplayers[0]))
+ if teamplayers[1] != []:
+ ranking.append((teamrank[1], random.random(), teamplayers[1]))
+ ranking.sort()
+ ranking.reverse()
+
+ nbpoints = givepoints and ((len(ranking)+1)//2)*10000
+ lines = []
+ for (n, dummy, bubber), i in zip(ranking, range(len(ranking))):
+ pic = RPicture()
+ if isinstance(bubber, list):
+ fraction = (nbpoints//(10*len(bubber))) * 10
+ total = fraction * len(bubber)
+ for n, bub in bubber:
+ bub.givepoints(fraction)
+ bubber = bubber[0][1]
+ pic.put(images.sprget(('hat', bubber.team)))
+ else:
+ if len(ranking) == 1:
+ icon = 0
+ elif i == 0:
+ icon = 10
+ elif i == len(ranking) - 1:
+ icon = 9
+ else:
+ icon = 0
+ pic.put(bubber.icons[icon, +1])
+ total = 0
+ line = []
+ if nbpoints > 0:
+ line.append(RPoints(bubber, nbpoints))
+ bubber.givepoints(nbpoints - total)
+ nbpoints -= 10000
+ line.append(pic)
+ line.append(RNumber(str(int(n*100.00001/maximum)) + '%'))
+ lines.append(line)
+ return lines
+
+
+def just_wait():
+ while 1:
+ yield 2
+
+def screen_scores():
+ results = {}
+ for p in BubPlayer.PlayerList:
+ if p.points:
+ results[p] = p.points
+ lines = ranking_picture(results, None, 0)
+ lines.insert(0, [RText(" THE END")])
+ return lines
+
+def screen_monster():
+ pairs = []
+ for p in BubPlayer.PlayerList:
+ catch = p.stats.get('monster', {})
+ for p2, count in catch.items():
+ if count:
+ pairs.append((count, p, p2))
+ random.shuffle(pairs)
+ pairs.sort()
+ pairs.reverse()
+ del pairs[5:]
+ lines = []
+ if pairs:
+ lines.append([RText('Best Monster Bubblers')])
+ for count, p, p2 in pairs:
+ pic = RPicture()
+ pic.put(p.icons[4,+1], 0, 6)
+ pic.put(images.sprget(GreenAndBlue.new_bubbles[p.pn][1]), 31, 6)
+ pic.put(images.sprget(GreenAndBlue.new_bubbles[p.pn][3]), 69, 6)
+ pic.put(images.sprget(GreenAndBlue.normal_bubbles[p.pn][0]), 101)
+ pic.put(images.sprget(p2), 101)
+ lines.append([pic, RNumber(str(count))])
+ return lines
+
+def screen_catch():
+ pairs = []
+ for p in BubPlayer.PlayerList:
+ catch = p.stats.get('catch', {})
+ for p2, count in catch.items():
+ if count:
+ pairs.append((count, p, p2))
+ random.shuffle(pairs)
+ pairs.sort()
+ pairs.reverse()
+ del pairs[5:]
+ lines = []
+ if pairs:
+ lines.append([RText('Best Dragon Bubblers')])
+ for count, p, p2 in pairs:
+ pic = RPicture()
+ pic.put(p.icons[4,+1], 0, 6)
+ pic.put(images.sprget(GreenAndBlue.new_bubbles[p.pn][1]), 31, 6)
+ pic.put(images.sprget(GreenAndBlue.new_bubbles[p.pn][3]), 69, 6)
+ pic.put(images.sprget(GreenAndBlue.normal_bubbles[p2.pn][0]), 101)
+ pic.put(images.sprget(('eyes', 0,0)), 101)
+ lines.append([pic, RNumber(str(count))])
+ return lines
+
+def screen_bonus():
+ pairs = []
+ for p in BubPlayer.PlayerList:
+ catch = p.stats.get('bonus', {})
+ for p2, count in catch.items():
+ if count > 1:
+ pairs.append((count, p, p2))
+ random.shuffle(pairs)
+ pairs.sort()
+ pairs.reverse()
+ seen = {}
+ npairs = []
+ for count, p, p2 in pairs:
+ if p2 not in seen:
+ npairs.append((count, p, p2))
+ seen[p2] = 1
+ pairs = npairs
+ del pairs[5:]
+ lines = []
+ if pairs:
+ lines.append([RText('Best Bonus Catchers')])
+ for count, p, p2 in pairs:
+ pic = RPicture()
+ pic.put(p.icons[1,+1], 0)
+ pic.put(images.sprget(p2), 44)
+ lines.append([pic, RNumber(str(count))])
+ return lines
+
+def screen_bubble():
+ pairs = []
+ for p in BubPlayer.PlayerList:
+ count = p.stats['bubble']
+ if count:
+ pairs.append((count, p))
+ random.shuffle(pairs)
+ pairs.sort()
+ pairs.reverse()
+ del pairs[5:]
+ lines = []
+ if pairs:
+ lines.append([RText('Best Bubble Exploders')])
+ for count, p in pairs:
+ pic = RPicture()
+ pic.put(p.icons[1,+1], 0)
+ pic.put(images.sprget(Bubble.exploding_bubbles[1]), 27)
+ lines.append([pic, RNumber(str(count))])
+ return lines
+
+def screen_die():
+ pairs = []
+ for p in BubPlayer.PlayerList:
+ count = p.stats['die']
+ if count:
+ pairs.append((count, p))
+ random.shuffle(pairs)
+ pairs.sort()
+ pairs.reverse()
+ del pairs[5:]
+ lines = []
+ if pairs:
+ lines.append([RText('Top Deaths')])
+ n = 0
+ for count, p in pairs:
+ pic = RPicture()
+ pic.put(p.icons[6+(n%3),+1], 0)
+ lines.append([pic, RNumber(str(count))])
+ n += 1
+ return lines
+
+def screen_authors():
+ return [
+ [RText('programming', 6)],
+ [RText(' Armin & Odie')],
+ [RText('art', 6)],
+ [RText(' David Gowers, based on McSebi')],
+ [RText('levels', 6)],
+ [RText(' Gio & Odie & MS & Armin')],
+ [RText('special thanks', 6)],
+ [RText(' Odie & Brachamutanda')],
+ [RText('beta-testers', 6)],
+ [RText(' IMA Connection')],
+ ]
+
+def game_over():
+ while 1:
+ for screen in [screen_scores, screen_monster, screen_catch,
+ screen_bonus, screen_bubble, screen_die,
+ screen_authors]:
+ lines = screen()
+ if lines:
+ for t in display(lines, 300, just_wait(), 1):
+ yield t
diff --git a/bubbob/save_rnglevel.py b/bubbob/save_rnglevel.py
new file mode 100644
index 0000000..8b09571
--- /dev/null
+++ b/bubbob/save_rnglevel.py
@@ -0,0 +1,125 @@
+#
+# This script outputs the random levels in a format you can
+# save into a file in the levels-directory and bub'n'bros
+# will be able to use it.
+#
+# this accepts the following parameters:
+# -seed N use random seed N for the generation
+#
+
+import sys
+import random
+import string
+sys.path.append('..')
+sys.path.append('../common')
+
+n_lvls = 1
+
+idx = 0
+while idx < len(sys.argv):
+ arg = sys.argv[idx]
+ idx += 1
+ if arg == '-seed':
+ arg = sys.argv[idx]
+ idx += 1
+ print "# Using seed: " + arg + "\n"
+ random.seed(arg)
+
+def printlvl(level):
+ mons = {}
+ monconv = {}
+ tmpmons = {}
+
+ # Populate monster tables
+ for y in range(0,level.HEIGHT):
+ for x in range(0,level.WIDTH):
+ wm = level.wmap[y][x]
+ if wm >= 'a':
+ m = getattr(level, wm)
+ if m.dir == 1:
+ dir = 'L'
+ else:
+ dir = 'R'
+ s = dir + m.cls.__name__
+ if tmpmons.has_key(s):
+ tmpmons[s].append(wm)
+ else:
+ tmpmons[s] = [wm]
+ # Build monster character conversion tables
+ lettr = 'a'
+ for m in tmpmons:
+ for n in tmpmons[m]:
+ monconv[n] = lettr
+ mons[lettr] = m
+ lettr = chr(ord(lettr) + 1)
+
+ # Build walls, replacing monsters from mons[]
+ walls = ""
+
+ for y in range(0,level.HEIGHT-1):
+ walls += "##"
+ for x in range(0,level.WIDTH):
+ wm = level.wmap[y][x]
+ if wm >= 'a':
+ if monconv.has_key(wm):
+ walls += monconv[wm]
+ else:
+ walls += '?'
+ else:
+ walls += wm
+ walls += "##\n"
+ walls += "##"
+ for x in range(0,level.WIDTH):
+ if level.wmap[0][x] == '#' or level.wmap[level.HEIGHT-1][x] == '#':
+ walls += "#"
+ else:
+ walls += " "
+ walls += "##\n"
+
+ # Build winds
+ winds = ""
+ for y in range(0,level.HEIGHT):
+ for x in range(0,level.WIDTH+4):
+ winds += level.winds[y][x]
+ winds += "\n"
+
+ for m in mons:
+ print " " + m + " = " + mons[m]
+
+ if level.letter:
+ print " letter = 1"
+ if level.fire:
+ print " fire = 1"
+ if level.lightning:
+ print " lightning = 1"
+ if level.water:
+ print " water = 1"
+ if level.top:
+ print " top = 1"
+
+ print " walls = \"\"\"\n" + walls + "\"\"\""
+ print " winds = \"\"\"\n" + winds + "\"\"\""
+
+
+for i in range(n_lvls):
+ print """
+import boarddef, mnstrmap, random
+from boarddef import LNasty, LMonky, LGhosty, LFlappy
+from boarddef import LSpringy, LOrcy, LGramy, LBlitzy
+from boarddef import RNasty, RMonky, RGhosty, RFlappy
+from boarddef import RSpringy, ROrcy, RGramy, RBlitzy
+"""
+
+ d = {'__name__': 'RandomLevels'}
+ execfile('levels/RandomLevels.py', d)
+
+ for i, Lvl in enumerate(d['GenerateLevels']()):
+ level = Lvl(i)
+
+ if level.monsters:
+ print "\n\nclass level%02d(boarddef.Level):" % (i+1)
+ else:
+ print "\n\nclass levelFinal(boarddef.Level):"
+
+ printlvl(level)
+ print
diff --git a/bubbob/setup.py b/bubbob/setup.py
new file mode 100755
index 0000000..577d3c5
--- /dev/null
+++ b/bubbob/setup.py
@@ -0,0 +1,22 @@
+#! /usr/bin/env python
+
+from distutils.core import setup
+from distutils.extension import Extension
+
+##setup ( name="gencopy",
+## version="0.1",
+## description="generator and iterator state",
+## author="Armin",
+## author_email="arigo@tunes.org",
+## ext_modules=[Extension(name = 'gencopy',
+## sources = ['gencopy.c'])]
+## )
+
+setup ( name="statesaver",
+ version="0.1",
+ description="object duplicator working on generators and iterators",
+ author="Armin",
+ author_email="arigo@tunes.org",
+ ext_modules=[Extension(name = 'statesaver',
+ sources = ['statesaver.c'])]
+ )
diff --git a/bubbob/sounds/die.wav b/bubbob/sounds/die.wav
new file mode 100644
index 0000000..bea33d9
--- /dev/null
+++ b/bubbob/sounds/die.wav
Binary files differ
diff --git a/bubbob/sounds/extra.wav b/bubbob/sounds/extra.wav
new file mode 100644
index 0000000..a95c01b
--- /dev/null
+++ b/bubbob/sounds/extra.wav
Binary files differ
diff --git a/bubbob/sounds/extralife.wav b/bubbob/sounds/extralife.wav
new file mode 100644
index 0000000..9afd8b2
--- /dev/null
+++ b/bubbob/sounds/extralife.wav
Binary files differ
diff --git a/bubbob/sounds/fruit.wav b/bubbob/sounds/fruit.wav
new file mode 100644
index 0000000..19eafd3
--- /dev/null
+++ b/bubbob/sounds/fruit.wav
Binary files differ
diff --git a/bubbob/sounds/hell.wav b/bubbob/sounds/hell.wav
new file mode 100644
index 0000000..aad531b
--- /dev/null
+++ b/bubbob/sounds/hell.wav
Binary files differ
diff --git a/bubbob/sounds/hurry.wav b/bubbob/sounds/hurry.wav
new file mode 100644
index 0000000..c566e54
--- /dev/null
+++ b/bubbob/sounds/hurry.wav
Binary files differ
diff --git a/bubbob/sounds/jump.wav b/bubbob/sounds/jump.wav
new file mode 100644
index 0000000..8d7c10d
--- /dev/null
+++ b/bubbob/sounds/jump.wav
Binary files differ
diff --git a/bubbob/sounds/letsgo.wav b/bubbob/sounds/letsgo.wav
new file mode 100644
index 0000000..3216cdf
--- /dev/null
+++ b/bubbob/sounds/letsgo.wav
Binary files differ
diff --git a/bubbob/sounds/pop.wav b/bubbob/sounds/pop.wav
new file mode 100644
index 0000000..efddf3d
--- /dev/null
+++ b/bubbob/sounds/pop.wav
Binary files differ
diff --git a/bubbob/sounds/shh.wav b/bubbob/sounds/shh.wav
new file mode 100644
index 0000000..8ea1ca9
--- /dev/null
+++ b/bubbob/sounds/shh.wav
Binary files differ
diff --git a/bubbob/sounds/yippee.wav b/bubbob/sounds/yippee.wav
new file mode 100644
index 0000000..1598140
--- /dev/null
+++ b/bubbob/sounds/yippee.wav
Binary files differ
diff --git a/bubbob/sprmap.py b/bubbob/sprmap.py
new file mode 100644
index 0000000..ce34264
--- /dev/null
+++ b/bubbob/sprmap.py
@@ -0,0 +1,533 @@
+sprmap = {
+ 10:('ice_cyan_big.ppm', (0, 0, 90, 90)),
+ 11:('ice_violet_big.ppm', (0, 0, 90, 90)),
+ 12:('peach_big.ppm', (0, 0, 90, 90)),
+ 13:('pastec_big.ppm', (0, 0, 90, 90)),
+ 14:('cream_pie_big.ppm', (0, 0, 90, 90)),
+ 15:('sugar_pie_big.ppm', (0, 0, 90, 90)),
+ 16:('diamond_big_purple.ppm', (0, 0, 90, 90)),
+ 17:('diamond_big_blue.ppm', (0, 0, 90, 90)),
+ 18:('diamond_big_red.ppm', (0, 0, 90, 90)),
+ 19:('diamond_big_yellow.ppm', (0, 0, 90, 90)),
+ 30:('lightning_large.ppm', (0, 0, 90, 66)),
+ 31:('yellow_Hurry_up.ppm', (0, 0, 136, 24)),
+ 32:('red_Hurry_up.ppm', (0, 0, 136, 24)),
+ 128:('extend.ppm', (0, 0, 32, 32)),
+ 129:('extend.ppm', (0, 32, 32, 32)),
+ 130:('extend.ppm', (0, 64, 32, 32)),
+ 131:('bubble.ppm', (0, 0, 32, 32)),
+ 132:('bubble.ppm', (0, 32, 32, 32)),
+ 133:('bubble.ppm', (0, 64, 32, 32)),
+ 134:('bubble.ppm', (0, 96, 32, 32)),
+ 135:('bubble.ppm', (0, 128, 32, 32)),
+ 136:('extend.ppm', (0, 96, 32, 32)),
+ 137:('extend.ppm', (0, 128, 32, 32)),
+ 138:('extend.ppm', (0, 160, 32, 32)),
+ 139:('door.ppm', (0, 0, 32, 32)),
+ 140:('water_surface.ppm', (0, 0, 16, 16)),
+ 141:('water_surface.ppm', (0, 16, 16, 16)),
+ 142:('water_surface.ppm', (0, 32, 16, 16)),
+ 143:('water_surface.ppm', (0, 48, 16, 16)),
+ 144:('extend.ppm', (0, 192, 32, 32)),
+ 145:('extend.ppm', (0, 224, 32, 32)),
+ 146:('extend.ppm', (0, 256, 32, 32)),
+ 152:('extend.ppm', (0, 288, 32, 32)),
+ 153:('extend.ppm', (0, 320, 32, 32)),
+ 154:('extend.ppm', (0, 352, 32, 32)),
+ 155:('bubble.ppm', (0, 352, 32, 32)),
+ 156:('bubble.ppm', (0, 384, 32, 32)),
+ 157:('bubble.ppm', (0, 416, 32, 32)),
+ 160:('extend.ppm', (0, 384, 32, 32)),
+ 161:('extend.ppm', (0, 416, 32, 32)),
+ 162:('extend.ppm', (0, 448, 32, 32)),
+ 163:('bubble.ppm', (0, 160, 32, 32)),
+ 164:('bubble.ppm', (0, 192, 32, 32)),
+ 165:('bubble.ppm', (0, 224, 32, 32)),
+ 168:('extend.ppm', (0, 480, 32, 32)),
+ 169:('extend.ppm', (0, 512, 32, 32)),
+ 170:('extend.ppm', (0, 544, 32, 32)),
+ 171:('bubble.ppm', (0, 256, 32, 32)),
+ 172:('bubble.ppm', (0, 288, 32, 32)),
+ 173:('bubble.ppm', (0, 320, 32, 32)),
+ 239:('nasty.ppm', (0, 0, 32, 32)),
+ 240:('nasty.ppm', (0, 32, 32, 32)),
+ 241:('nasty.ppm', (0, 64, 32, 32)),
+ 242:('nasty.ppm', (0, 96, 32, 32)),
+ 243:('nasty.ppm', (0, 128, 32, 32)),
+ 244:('nasty.ppm', (0, 160, 32, 32)),
+ 245:('nasty.ppm', (0, 192, 32, 32)),
+ 246:('nasty.ppm', (0, 224, 32, 32)),
+ 247:('nasty.ppm', (0, 256, 32, 32)),
+ 248:('nasty.ppm', (0, 288, 32, 32)),
+ 249:('nasty.ppm', (0, 320, 32, 32)),
+ 253:('nasty.ppm', (0, 352, 32, 32)),
+ 254:('nasty.ppm', (0, 384, 32, 32)),
+ 255:('nasty.ppm', (0, 416, 32, 32)),
+ 256:('nasty.ppm', (0, 448, 32, 32)),
+ 265:('monky.ppm', (0, 0, 32, 32)),
+ 266:('monky.ppm', (0, 32, 32, 32)),
+ 267:('monky.ppm', (0, 64, 32, 32)),
+ 268:('monky.ppm', (0, 96, 32, 32)),
+ 269:('monky.ppm', (0, 128, 32, 32)),
+ 270:('monky.ppm', (0, 160, 32, 32)),
+ 271:('monky.ppm', (0, 192, 32, 32)),
+ 272:('monky.ppm', (0, 224, 32, 32)),
+ 273:('monky.ppm', (0, 256, 32, 32)),
+ 274:('monky.ppm', (0, 288, 32, 32)),
+ 275:('monky.ppm', (0, 320, 32, 32)),
+ 279:('monky.ppm', (0, 352, 32, 32)),
+ 280:('monky.ppm', (0, 384, 32, 32)),
+ 281:('monky.ppm', (0, 416, 32, 32)),
+ 282:('monky.ppm', (0, 448, 32, 32)),
+ 291:('ghosty.ppm', (0, 0, 32, 32)),
+ 292:('ghosty.ppm', (0, 32, 32, 32)),
+ 293:('ghosty.ppm', (0, 64, 32, 32)),
+ 294:('ghosty.ppm', (0, 96, 32, 32)),
+ 295:('ghosty.ppm', (0, 128, 32, 32)),
+ 296:('ghosty.ppm', (0, 160, 32, 32)),
+ 297:('ghosty.ppm', (0, 192, 32, 32)),
+ 298:('ghosty.ppm', (0, 224, 32, 32)),
+ 299:('ghosty.ppm', (0, 256, 32, 32)),
+ 300:('ghosty.ppm', (0, 288, 32, 32)),
+ 301:('ghosty.ppm', (0, 320, 32, 32)),
+ 305:('ghosty.ppm', (0, 352, 32, 32)),
+ 306:('ghosty.ppm', (0, 384, 32, 32)),
+ 307:('ghosty.ppm', (0, 416, 32, 32)),
+ 308:('ghosty.ppm', (0, 448, 32, 32)),
+ 317:('flappy.ppm', (0, 0, 32, 32)),
+ 318:('flappy.ppm', (0, 32, 32, 32)),
+ 319:('flappy.ppm', (0, 64, 32, 32)),
+ 320:('flappy.ppm', (0, 96, 32, 32)),
+ 321:('flappy.ppm', (0, 128, 32, 32)),
+ 322:('flappy.ppm', (0, 160, 32, 32)),
+ 323:('flappy.ppm', (0, 192, 32, 32)),
+ 324:('flappy.ppm', (0, 224, 32, 32)),
+ 325:('flappy.ppm', (0, 256, 32, 32)),
+ 326:('flappy.ppm', (0, 288, 32, 32)),
+ 327:('flappy.ppm', (0, 320, 32, 32)),
+ 331:('flappy.ppm', (0, 352, 32, 32)),
+ 332:('flappy.ppm', (0, 384, 32, 32)),
+ 333:('flappy.ppm', (0, 416, 32, 32)),
+ 334:('flappy.ppm', (0, 448, 32, 32)),
+ 343:('springy.ppm', (0, 0, 32, 32)),
+ 344:('springy.ppm', (0, 32, 32, 32)),
+ 345:('springy.ppm', (0, 64, 32, 32)),
+ 346:('springy.ppm', (0, 96, 32, 32)),
+ 347:('springy.ppm', (0, 128, 32, 32)),
+ 348:('springy.ppm', (0, 160, 32, 32)),
+ 349:('springy.ppm', (0, 192, 32, 32)),
+ 350:('springy.ppm', (0, 224, 32, 32)),
+ 351:('springy.ppm', (0, 256, 32, 32)),
+ 352:('springy.ppm', (0, 288, 32, 32)),
+ 353:('springy.ppm', (0, 320, 32, 32)),
+ 357:('springy.ppm', (0, 352, 32, 32)),
+ 358:('springy.ppm', (0, 384, 32, 32)),
+ 359:('springy.ppm', (0, 416, 32, 32)),
+ 360:('springy.ppm', (0, 448, 32, 32)),
+ 369:('springy.ppm', (0, 480, 32, 32)),
+ 370:('springy.ppm', (0, 512, 32, 32)),
+ 371:('springy.ppm', (0, 544, 32, 32)),
+ 372:('springy.ppm', (0, 576, 32, 32)),
+ 373:('orcy.ppm', (0, 0, 32, 32)),
+ 374:('orcy.ppm', (0, 32, 32, 32)),
+ 375:('orcy.ppm', (0, 64, 32, 32)),
+ 376:('orcy.ppm', (0, 96, 32, 32)),
+ 377:('orcy.ppm', (0, 128, 32, 32)),
+ 378:('orcy.ppm', (0, 160, 32, 32)),
+ 379:('orcy.ppm', (0, 192, 32, 32)),
+ 380:('orcy.ppm', (0, 224, 32, 32)),
+ 381:('orcy.ppm', (0, 256, 32, 32)),
+ 382:('orcy.ppm', (0, 288, 32, 32)),
+ 383:('orcy.ppm', (0, 320, 32, 32)),
+ 387:('orcy.ppm', (0, 352, 32, 32)),
+ 388:('orcy.ppm', (0, 384, 32, 32)),
+ 389:('orcy.ppm', (0, 416, 32, 32)),
+ 390:('orcy.ppm', (0, 448, 32, 32)),
+ 399:('gramy.ppm', (0, 0, 32, 32)),
+ 400:('gramy.ppm', (0, 32, 32, 32)),
+ 401:('gramy.ppm', (0, 64, 32, 32)),
+ 402:('gramy.ppm', (0, 96, 32, 32)),
+ 403:('gramy.ppm', (0, 128, 32, 32)),
+ 404:('gramy.ppm', (0, 160, 32, 32)),
+ 405:('gramy.ppm', (0, 192, 32, 32)),
+ 406:('gramy.ppm', (0, 224, 32, 32)),
+ 407:('gramy.ppm', (0, 256, 32, 32)),
+ 408:('gramy.ppm', (0, 288, 32, 32)),
+ 409:('gramy.ppm', (0, 320, 32, 32)),
+ 413:('gramy.ppm', (0, 352, 32, 32)),
+ 414:('gramy.ppm', (0, 384, 32, 32)),
+ 415:('gramy.ppm', (0, 416, 32, 32)),
+ 416:('gramy.ppm', (0, 448, 32, 32)),
+ 425:('blitzy.ppm', (0, 0, 32, 32)),
+ 426:('blitzy.ppm', (0, 32, 32, 32)),
+ 427:('blitzy.ppm', (0, 64, 32, 32)),
+ 428:('blitzy.ppm', (0, 96, 32, 32)),
+ 429:('blitzy.ppm', (0, 128, 32, 32)),
+ 430:('blitzy.ppm', (0, 160, 32, 32)),
+ 431:('blitzy.ppm', (0, 192, 32, 32)),
+ 435:('blitzy.ppm', (0, 224, 32, 32)),
+ 436:('blitzy.ppm', (0, 256, 32, 32)),
+ 437:('blitzy.ppm', (0, 288, 32, 32)),
+ 438:('blitzy.ppm', (0, 320, 32, 32)),
+ 443:('ghost.ppm', (0, 0, 32, 32)),
+ 444:('ghost.ppm', (0, 32, 32, 32)),
+ 445:('ghost.ppm', (0, 64, 32, 32)),
+ 446:('ghost.ppm', (0, 96, 32, 32)),
+ 447:('ghost.ppm', (0, 128, 32, 32)),
+ 448:('ghost.ppm', (0, 160, 32, 32)),
+ 449:('ghost.ppm', (0, 192, 32, 32)),
+ 450:('ghost.ppm', (0, 224, 32, 32)),
+ 451:('monky.ppm', (0, 480, 32, 32)),
+ 452:('monky.ppm', (0, 512, 32, 32)),
+ 453:('monky.ppm', (0, 544, 32, 32)),
+ 454:('monky.ppm', (0, 576, 32, 32)),
+ 455:('monky.ppm', (0, 608, 32, 32)),
+ 456:('orcy.ppm', (0, 480, 32, 32)),
+ 457:('orcy.ppm', (0, 512, 32, 32)),
+ 458:('orcy.ppm', (0, 544, 32, 32)),
+ 459:('orcy.ppm', (0, 576, 32, 32)),
+ 460:('orcy.ppm', (0, 608, 32, 32)),
+ 461:('orcy.ppm', (0, 640, 32, 32)),
+ 462:('orcy.ppm', (0, 672, 32, 32)),
+ 463:('orcy.ppm', (0, 704, 32, 32)),
+ 464:('shot.ppm', (0, 0, 32, 32)),
+ 465:('shot.ppm', (0, 32, 32, 32)),
+ 466:('shot.ppm', (0, 64, 32, 32)),
+ 467:('shot.ppm', (0, 96, 32, 32)),
+ 468:('shot.ppm', (0, 128, 32, 32)),
+ 469:('shot.ppm', (0, 160, 32, 32)),
+ 470:('shot.ppm', (0, 192, 32, 32)),
+ 471:('shot.ppm', (0, 224, 32, 32)),
+ 472:('gramy.ppm', (0, 480, 32, 32)),
+ 473:('gramy.ppm', (0, 512, 32, 32)),
+ 474:('gramy.ppm', (0, 544, 32, 32)),
+ 475:('gramy.ppm', (0, 576, 32, 32)),
+ 476:('blitzy_shot.ppm', (0, 0, 16, 32)),
+ 477:('bonus_0.ppm', (0, 0, 32, 32)),
+ 478:('bonus_0.ppm', (0, 32, 32, 32)),
+ 479:('bonus_0.ppm', (0, 64, 32, 32)),
+ 480:('bonus_0.ppm', (0, 96, 32, 32)),
+ 481:('bonus_0.ppm', (0, 128, 32, 32)),
+ 482:('spinning_drop.ppm', (0, 0, 16, 16)),
+ 483:('spinning_drop.ppm', (0, 16, 16, 16)),
+ 484:('spinning_drop.ppm', (0, 32, 16, 16)),
+ 485:('spinning_drop.ppm', (0, 48, 16, 16)),
+ 486:('spinning_drop.ppm', (0, 64, 16, 16)),
+ 487:('spinning_drop.ppm', (0, 80, 16, 16)),
+ 488:('lightning_small.ppm', (0, 0, 24, 24)),
+ 489:('fire_drop.ppm', (0, 0, 9, 16)),
+ 490:('fire_surface.ppm', (0, 0, 16, 16)),
+ 491:('fire_surface.ppm', (0, 16, 16, 16)),
+ 492:('fire_surface.ppm', (0, 32, 16, 16)),
+ 493:('fire_surface.ppm', (0, 48, 16, 16)),
+ 495:('water_still.ppm', (0, 0, 16, 16)),
+ 496:('bonus_0.ppm', (0, 160, 32, 20)),
+ 519:('level_digits.ppm', (0, 0, 14, 20)),
+ 520:('level_digits.ppm', (0, 20, 14, 20)),
+ 521:('level_digits.ppm', (0, 40, 14, 20)),
+ 522:('level_digits.ppm', (0, 60, 14, 20)),
+ 523:('level_digits.ppm', (0, 80, 14, 20)),
+ 524:('level_digits.ppm', (0, 100, 14, 20)),
+ 525:('level_digits.ppm', (0, 120, 14, 20)),
+ 526:('level_digits.ppm', (0, 140, 14, 20)),
+ 527:('level_digits.ppm', (0, 160, 14, 20)),
+ 528:('level_digits.ppm', (0, 180, 14, 20)),
+ 593:('bonus_1.ppm', (0, 0, 32, 32)),
+ 594:('bonus_1.ppm', (0, 32, 32, 32)),
+ 595:('bonus_1.ppm', (0, 64, 32, 32)),
+ 596:('bonus_1.ppm', (0, 96, 32, 32)),
+ 597:('bonus_1.ppm', (0, 128, 32, 32)),
+ 598:('bonus_1.ppm', (0, 160, 32, 32)),
+ 599:('bonus_1.ppm', (0, 192, 32, 32)),
+ 600:('bonus_1.ppm', (0, 224, 32, 32)),
+ 601:('bonus_2.ppm', (0, 0, 32, 32)),
+ 602:('bonus_2.ppm', (0, 32, 32, 32)),
+ 603:('bonus_2.ppm', (0, 64, 32, 32)),
+ 604:('bonus_2.ppm', (0, 96, 32, 32)),
+ 605:('bonus_2.ppm', (0, 128, 32, 32)),
+ 606:('bonus_2.ppm', (0, 160, 32, 32)),
+ 607:('bonus_2.ppm', (0, 192, 32, 32)),
+ 608:('bonus_2.ppm', (0, 224, 32, 32)),
+ 609:('bonus_3.ppm', (0, 0, 32, 32)),
+ 610:('bonus_3.ppm', (0, 32, 32, 32)),
+ 611:('bonus_3.ppm', (0, 64, 32, 32)),
+ 612:('bonus_3.ppm', (0, 96, 32, 32)),
+ 613:('bonus_3.ppm', (0, 128, 32, 32)),
+ 614:('bonus_3.ppm', (0, 160, 32, 32)),
+ 615:('bonus_3.ppm', (0, 192, 32, 32)),
+ 616:('bonus_3.ppm', (0, 224, 32, 32)),
+ 617:('bonus_4.ppm', (0, 0, 32, 32)),
+ 618:('bonus_4.ppm', (0, 32, 32, 32)),
+ 619:('bonus_4.ppm', (0, 64, 32, 32)),
+ 620:('bonus_4.ppm', (0, 96, 32, 32)),
+ 621:('bonus_4.ppm', (0, 128, 32, 32)),
+ 622:('bonus_4.ppm', (0, 160, 32, 32)),
+ 623:('bonus_4.ppm', (0, 192, 32, 32)),
+ 624:('bonus_4.ppm', (0, 224, 32, 32)),
+ 625:('bonus_5.ppm', (0, 0, 32, 32)),
+ 626:('bonus_5.ppm', (0, 32, 32, 32)),
+ 627:('bonus_5.ppm', (0, 64, 32, 32)),
+ 628:('bonus_5.ppm', (0, 96, 32, 32)),
+ 629:('bonus_5.ppm', (0, 128, 32, 32)),
+ 630:('bonus_5.ppm', (0, 160, 32, 32)),
+ 631:('bonus_5.ppm', (0, 192, 32, 32)),
+ 632:('bonus_5.ppm', (0, 224, 32, 32)),
+ 633:('bonus_6.ppm', (0, 0, 32, 32)),
+ 634:('bonus_6.ppm', (0, 32, 32, 32)),
+ 635:('bonus_6.ppm', (0, 64, 32, 32)),
+ 636:('bonus_6.ppm', (0, 96, 32, 32)),
+ 637:('bonus_6.ppm', (0, 128, 32, 32)),
+ 638:('bonus_6.ppm', (0, 160, 32, 32)),
+ 639:('bonus_6.ppm', (0, 192, 32, 32)),
+ 640:('bonus_6.ppm', (0, 224, 32, 32)),
+ 641:('bonus_7.ppm', (0, 0, 32, 32)),
+ 642:('bonus_7.ppm', (0, 32, 32, 32)),
+ 643:('bonus_7.ppm', (0, 64, 32, 32)),
+ 644:('bonus_7.ppm', (0, 96, 32, 32)),
+ 645:('bonus_7.ppm', (0, 128, 32, 32)),
+ 646:('bonus_7.ppm', (0, 160, 32, 32)),
+ 647:('bonus_7.ppm', (0, 192, 32, 32)),
+ 648:('bonus_7.ppm', (0, 224, 32, 32)),
+ 649:('bonus_8.ppm', (0, 0, 32, 32)),
+ 650:('bonus_8.ppm', (0, 32, 32, 32)),
+ 651:('bonus_8.ppm', (0, 64, 32, 32)),
+ 652:('bonus_8.ppm', (0, 96, 32, 32)),
+ 653:('bonus_8.ppm', (0, 128, 32, 32)),
+ 654:('bonus_8.ppm', (0, 160, 32, 32)),
+ 655:('bonus_8.ppm', (0, 192, 32, 32)),
+ 656:('bonus_8.ppm', (0, 224, 32, 32)),
+ 657:('bonus_9.ppm', (0, 0, 32, 32)),
+ 658:('bonus_9.ppm', (0, 32, 32, 32)),
+ 659:('bonus_9.ppm', (0, 64, 32, 32)),
+ 660:('bonus_9.ppm', (0, 96, 32, 32)),
+ 661:('bonus_9.ppm', (0, 128, 32, 32)),
+ 662:('bonus_9.ppm', (0, 160, 32, 32)),
+ 663:('bonus_9.ppm', (0, 192, 32, 32)),
+ 664:('bonus_9.ppm', (0, 224, 32, 32)),
+ 665:('bonus_10.ppm', (0, 0, 32, 32)),
+ 666:('bonus_10.ppm', (0, 32, 32, 32)),
+ 667:('bonus_10.ppm', (0, 64, 32, 32)),
+ 668:('bonus_10.ppm', (0, 96, 32, 32)),
+ 669:('bonus_10.ppm', (0, 128, 32, 32)),
+ 670:('bonus_10.ppm', (0, 160, 32, 32)),
+ 671:('bonus_10.ppm', (0, 192, 32, 32)),
+ 672:('bonus_10.ppm', (0, 224, 32, 32)),
+ 673:('bonus_11.ppm', (0, 0, 32, 32)),
+ 674:('bonus_11.ppm', (0, 32, 32, 32)),
+ 675:('bonus_11.ppm', (0, 64, 32, 32)),
+ 676:('bonus_11.ppm', (0, 96, 32, 32)),
+ 677:('bonus_11.ppm', (0, 128, 32, 32)),
+ 678:('bonus_11.ppm', (0, 160, 32, 32)),
+ 679:('bonus_11.ppm', (0, 192, 32, 32)),
+ 680:('bonus_11.ppm', (0, 224, 32, 32)),
+ 681:('bonus_12.ppm', (0, 0, 32, 32)),
+ 682:('bonus_12.ppm', (0, 32, 32, 32)),
+ 691:('bonus_12.ppm', (0, 64, 32, 32)),
+ 692:('bonus_12.ppm', (0, 96, 32, 32)),
+ 800:('nasty_angry.ppm', (0, 0, 32, 32)),
+ 801:('nasty_angry.ppm', (0, 32, 32, 32)),
+ 802:('nasty_angry.ppm', (0, 64, 32, 32)),
+ 803:('nasty_angry.ppm', (0, 96, 32, 32)),
+ 804:('nasty_angry.ppm', (0, 128, 32, 32)),
+ 805:('nasty_angry.ppm', (0, 160, 32, 32)),
+ 806:('nasty_angry.ppm', (0, 192, 32, 32)),
+ 807:('nasty_angry.ppm', (0, 224, 32, 32)),
+ 808:('monky_angry.ppm', (0, 0, 32, 32)),
+ 809:('monky_angry.ppm', (0, 32, 32, 32)),
+ 810:('monky_angry.ppm', (0, 64, 32, 32)),
+ 811:('monky_angry.ppm', (0, 96, 32, 32)),
+ 812:('monky_angry.ppm', (0, 128, 32, 32)),
+ 813:('monky_angry.ppm', (0, 160, 32, 32)),
+ 814:('monky_angry.ppm', (0, 192, 32, 32)),
+ 815:('monky_angry.ppm', (0, 224, 32, 32)),
+ 816:('ghosty_angry.ppm', (0, 0, 32, 32)),
+ 817:('ghosty_angry.ppm', (0, 32, 32, 32)),
+ 818:('ghosty_angry.ppm', (0, 64, 32, 32)),
+ 819:('ghosty_angry.ppm', (0, 96, 32, 32)),
+ 820:('ghosty_angry.ppm', (0, 128, 32, 32)),
+ 821:('ghosty_angry.ppm', (0, 160, 32, 32)),
+ 822:('ghosty_angry.ppm', (0, 192, 32, 32)),
+ 823:('ghosty_angry.ppm', (0, 224, 32, 32)),
+ 824:('flapy_angry.ppm', (0, 0, 32, 32)),
+ 825:('flapy_angry.ppm', (0, 32, 32, 32)),
+ 826:('flapy_angry.ppm', (0, 64, 32, 32)),
+ 827:('flapy_angry.ppm', (0, 96, 32, 32)),
+ 828:('flapy_angry.ppm', (0, 128, 32, 32)),
+ 829:('flapy_angry.ppm', (0, 160, 32, 32)),
+ 830:('flapy_angry.ppm', (0, 192, 32, 32)),
+ 831:('flapy_angry.ppm', (0, 224, 32, 32)),
+ 832:('springy_angry.ppm', (0, 0, 32, 32)),
+ 833:('springy_angry.ppm', (0, 32, 32, 32)),
+ 834:('springy_angry.ppm', (0, 64, 32, 32)),
+ 835:('springy_angry.ppm', (0, 96, 32, 32)),
+ 836:('springy_angry.ppm', (0, 128, 32, 32)),
+ 837:('springy_angry.ppm', (0, 160, 32, 32)),
+ 838:('springy_angry.ppm', (0, 192, 32, 32)),
+ 839:('springy_angry.ppm', (0, 224, 32, 32)),
+ 840:('springy_angry.ppm', (0, 256, 32, 32)),
+ 841:('springy_angry.ppm', (0, 288, 32, 32)),
+ 842:('springy_angry.ppm', (0, 320, 32, 32)),
+ 843:('springy_angry.ppm', (0, 352, 32, 32)),
+ 844:('orcy_angry.ppm', (0, 0, 32, 32)),
+ 845:('orcy_angry.ppm', (0, 32, 32, 32)),
+ 846:('orcy_angry.ppm', (0, 64, 32, 32)),
+ 847:('orcy_angry.ppm', (0, 96, 32, 32)),
+ 848:('orcy_angry.ppm', (0, 128, 32, 32)),
+ 849:('orcy_angry.ppm', (0, 160, 32, 32)),
+ 850:('orcy_angry.ppm', (0, 192, 32, 32)),
+ 851:('orcy_angry.ppm', (0, 224, 32, 32)),
+ 852:('gramy_angry.ppm', (0, 0, 32, 32)),
+ 853:('gramy_angry.ppm', (0, 32, 32, 32)),
+ 854:('gramy_angry.ppm', (0, 64, 32, 32)),
+ 855:('gramy_angry.ppm', (0, 96, 32, 32)),
+ 856:('gramy_angry.ppm', (0, 128, 32, 32)),
+ 857:('gramy_angry.ppm', (0, 160, 32, 32)),
+ 858:('gramy_angry.ppm', (0, 192, 32, 32)),
+ 859:('gramy_angry.ppm', (0, 224, 32, 32)),
+ 860:('blitzy_angry.ppm', (0, 0, 32, 32)),
+ 861:('blitzy_angry.ppm', (0, 32, 32, 32)),
+ 862:('blitzy_angry.ppm', (0, 64, 32, 32)),
+ 863:('blitzy_angry.ppm', (0, 96, 32, 32)),
+ 900:('water_flow.ppm', (0, 0, 16, 16)),
+ 901:('water_flow.ppm', (0, 16, 16, 16)),
+ 902:('water_flow.ppm', (0, 32, 16, 16)),
+ 903:('water_flow.ppm', (0, 48, 16, 16)),
+ 904:('water_flow.ppm', (0, 64, 16, 16)),
+ 905:('water_flow.ppm', (0, 80, 16, 16)),
+ 906:('water_flow.ppm', (0, 96, 16, 16)),
+ 907:('water_flow.ppm', (0, 112, 16, 16)),
+ 908:('water_flow.ppm', (0, 128, 16, 16)),
+ 909:('water_flow.ppm', (0, 144, 16, 16)),
+ 910:('big_bubble.ppm', (0, 0, 64, 64)),
+ 911:('big_bubble.ppm', (0, 64, 64, 64)),
+ 912:('big_bubble.ppm', (0, 128, 64, 64)),
+ 913:('big_bubble.ppm', (0, 192, 64, 64)),
+ 914:('big_bubble.ppm', (0, 256, 64, 64)),
+ 915:('big_bubble.ppm', (0, 320, 64, 64)),
+ 920:('level_digits.ppm', (0, 200, 14, 20)),
+ 921:('level_digits.ppm', (0, 220, 14, 20)),
+ 922:('level_digits.ppm', (0, 240, 14, 20)),
+ 923:('level_digits.ppm', (0, 260, 14, 20)),
+ 924:('level_digits.ppm', (0, 280, 14, 20)),
+ 925:('level_digits.ppm', (0, 300, 14, 20)),
+ 926:('level_digits.ppm', (0, 320, 14, 20)),
+ 927:('level_digits.ppm', (0, 340, 14, 20)),
+ 928:('level_digits.ppm', (0, 360, 14, 20)),
+ 929:('level_digits.ppm', (0, 380, 14, 20)),
+ 930:('level_digits.ppm', (0, 400, 14, 20)),
+ 931:('level_digits.ppm', (0, 420, 14, 20)),
+ 932:('level_digits.ppm', (0, 440, 14, 20)),
+ 933:('level_digits.ppm', (0, 460, 14, 20)),
+ 934:('level_digits.ppm', (0, 480, 14, 20)),
+ 935:('level_digits.ppm', (0, 500, 14, 20)),
+ 936:('level_digits.ppm', (0, 520, 14, 20)),
+ 937:('level_digits.ppm', (0, 540, 14, 20)),
+ 938:('level_digits.ppm', (0, 560, 14, 20)),
+ 939:('level_digits.ppm', (0, 580, 14, 20)),
+ 940:('star_large.ppm', (0, 0, 32, 32)),
+ 941:('star_large.ppm', (0, 32, 32, 32)),
+ 942:('star_large.ppm', (0, 64, 32, 32)),
+ 943:('star_large.ppm', (0, 96, 32, 32)),
+ 944:('star_large.ppm', (0, 128, 32, 32)),
+ 945:('star_large.ppm', (0, 160, 32, 32)),
+ 946:('star_large.ppm', (0, 192, 32, 32)),
+ 947:('star_large.ppm', (0, 224, 32, 32)),
+ 948:('star_large.ppm', (0, 256, 32, 32)),
+ 949:('star_large.ppm', (0, 288, 32, 32)),
+ 950:('star_large.ppm', (0, 320, 32, 32)),
+ 951:('star_large.ppm', (0, 352, 32, 32)),
+ 952:('big_bubble_2.ppm', (0, 0, 64, 64)),
+ 953:('big_bubble_2.ppm', (0, 64, 64, 64)),
+ 954:('big_bubble_2.ppm', (0, 128, 64, 64)),
+ 955:('big_bubble_2.ppm', (0, 192, 64, 64)),
+ 956:('big_bubble_2.ppm', (0, 256, 64, 64)),
+
+ 20:('10000_%d.ppm', (0, 0, 90, 40)),
+ 21:('20000_%d.ppm', (0, 0, 98, 45)),
+ 22:('30000_%d.ppm', (0, 0, 98, 45)),
+ 23:('40000_%d.ppm', (0, 0, 98, 45)),
+ 24:('50000_%d.ppm', (0, 0, 95, 45)),
+ 25:('60000_%d.ppm', (0, 0, 96, 45)),
+ 26:('70000_%d.ppm', (0, 0, 96, 45)),
+ 176:('dragon_bubble_%d.ppm', (0, 0, 32, 32)),
+ 177:('dragon_bubble_%d.ppm', (0, 32, 32, 32)),
+ 178:('dragon_bubble_%d.ppm', (0, 64, 32, 32)),
+ 179:('dragon_bubble_%d.ppm', (0, 96, 32, 32)),
+ 180:('dragon_bubble_%d.ppm', (0, 128, 32, 32)),
+ 181:('dragon_bubble_%d.ppm', (0, 160, 32, 32)),
+ 182:('dragon_bubble_%d.ppm', (0, 192, 32, 32)),
+ 183:('dragon_bubble_%d.ppm', (0, 224, 32, 32)),
+ 184:('dragon_bubble_%d.ppm', (0, 256, 32, 32)),
+ 188:('dragon_bubble_%d.ppm', (0, 288, 32, 32)),
+ 189:('dragon_bubble_%d.ppm', (0, 320, 32, 32)),
+ 190:('dragon_bubble_%d.ppm', (0, 352, 32, 32)),
+ 191:('dragon_bubble_%d.ppm', (0, 384, 32, 32)),
+ 192:('dragon_bubble_%d.ppm', (0, 416, 32, 32)),
+ 193:('dragon_bubble_%d.ppm', (0, 448, 32, 32)),
+ 194:('dragon_bubble_%d.ppm', (0, 480, 32, 32)),
+ 210:('dragon_%d.ppm', (0, 0, 32, 32)),
+ 211:('dragon_%d.ppm', (0, 32, 32, 32)),
+ 212:('dragon_%d.ppm', (0, 64, 32, 32)),
+ 213:('dragon_%d.ppm', (0, 96, 32, 32)),
+ 214:('dragon_%d.ppm', (0, 128, 32, 32)),
+ 215:('dragon_%d.ppm', (0, 160, 32, 32)),
+ 216:('dragon_%d.ppm', (0, 192, 32, 32)),
+ 217:('dragon_%d.ppm', (0, 224, 32, 32)),
+ 218:('dragon_%d.ppm', (0, 256, 32, 32)),
+ 219:('dragon_%d.ppm', (0, 288, 32, 32)),
+ 220:('dragon_%d.ppm', (0, 320, 32, 32)),
+ 221:('dragon_%d.ppm', (0, 352, 32, 32)),
+ 222:('dragon_%d.ppm', (0, 384, 32, 32)),
+ 497:('game_over_%d.ppm', (0, 0, 64, 32)),
+ 499:('digits_%d.ppm', (0, 0, 14, 17)),
+ 500:('digits_%d.ppm', (0, 17, 14, 17)),
+ 501:('digits_%d.ppm', (0, 34, 14, 17)),
+ 502:('digits_%d.ppm', (0, 51, 14, 17)),
+ 503:('digits_%d.ppm', (0, 68, 14, 17)),
+ 504:('digits_%d.ppm', (0, 85, 14, 17)),
+ 505:('digits_%d.ppm', (0, 102, 14, 17)),
+ 506:('digits_%d.ppm', (0, 119, 14, 17)),
+ 507:('digits_%d.ppm', (0, 136, 14, 17)),
+ 508:('digits_%d.ppm', (0, 153, 14, 17)),
+ 529:('point_%d.ppm', (0, 0, 48, 24)),
+ 530:('point_%d.ppm', (0, 24, 48, 24)),
+ 531:('point_%d.ppm', (0, 48, 48, 24)),
+ 532:('point_%d.ppm', (0, 72, 48, 24)),
+ 533:('point_%d.ppm', (0, 96, 48, 24)),
+ 534:('point_%d.ppm', (0, 120, 48, 24)),
+ 535:('point_%d.ppm', (0, 144, 48, 24)),
+ 536:('point_%d.ppm', (0, 168, 48, 24)),
+ 537:('point_%d.ppm', (0, 192, 48, 24)),
+ 538:('point_%d.ppm', (0, 216, 48, 24)),
+ 539:('point_%d.ppm', (0, 240, 48, 24)),
+ 540:('point_%d.ppm', (0, 264, 48, 24)),
+ 541:('point_%d.ppm', (0, 288, 48, 24)),
+ 542:('point_%d.ppm', (0, 312, 48, 24)),
+ 543:('point_%d.ppm', (0, 336, 48, 24)),
+ 544:('point_%d.ppm', (0, 360, 48, 24)),
+ 545:('point_%d.ppm', (0, 384, 48, 24)),
+ 546:('point_%d.ppm', (0, 408, 48, 24)),
+ 547:('point_%d.ppm', (0, 432, 48, 24)),
+ 548:('point_%d.ppm', (0, 456, 48, 24)),
+ 549:('point_%d.ppm', (0, 480, 48, 24)),
+ 550:('point_%d.ppm', (0, 504, 48, 24)),
+ 551:('point_%d.ppm', (0, 528, 48, 24)),
+ 552:('point_%d.ppm', (0, 552, 48, 24)),
+ 553:('point_%d.ppm', (0, 576, 48, 24)),
+ 683:('dragon_%d.ppm', (0, 416, 32, 32)),
+ 684:('dragon_%d.ppm', (0, 448, 32, 32)),
+ 685:('dragon_%d.ppm', (0, 480, 32, 32)),
+ 686:('dragon_%d.ppm', (0, 512, 32, 32)),
+ 693:('dragon_%d.ppm', (0, 544, 32, 32)),
+ 694:('dragon_%d.ppm', (0, 576, 32, 32)),
+ 695:('dragon_%d.ppm', (0, 608, 32, 32)),
+
+ # custom images
+ 700:('fish_%d.ppm', (0, 0, 32, 32)),
+ 701:('fish_%d.ppm', (0, 32, 32, 32)),
+ 702:('fish_%d.ppm', (0, 64, 32, 32)),
+ 703:('fish_%d.ppm', (0, 96, 32, 32)),
+ 704:('fish_%d.ppm', (0, 128, 32, 32)),
+ 705:('fish_%d.ppm', (0, 160, 32, 32)),
+ 706:('fish_%d.ppm', (0, 192, 32, 32)),
+ }
diff --git a/bubbob/statesaver.c b/bubbob/statesaver.c
new file mode 100644
index 0000000..5baf033
--- /dev/null
+++ b/bubbob/statesaver.c
@@ -0,0 +1,610 @@
+/** High-performance deep copy.
+
+ This one can copy running generators and their frames!
+
+ statesaver.copy(x) -> recursive copy of x
+
+ You have precise control over what is copied and what should be shared.
+ By default, *only* common built-in types are copied. Unrecognized
+ object types are shared. The copied built-in types are:
+
+ - tuple
+ - list
+ - dict
+ - functions, for possibly mutable func_defaults (func_globals is shared)
+ - methods, for im_self and im_func (im_class is shared)
+ - running or stopped generators (yeah!)
+ - sequence iterators
+
+ Old-style class instances are only copied if they have an
+ inst_build() method, which is called with no argument and must
+ return a new instance whose __dict__ is not filled (it will be
+ filled by the copying mecanisms). Suggested implementation:
+
+ def inst_build(self):
+ return new.instance(self.__class__)
+
+ New-style class instances are not supported (i.e. always shared).
+**/
+
+#include <Python.h>
+#include <compile.h>
+#include <frameobject.h>
+#include <eval.h>
+
+
+static PyObject* copyrec(PyObject* o); /* forward */
+
+static PyObject* empty_iterator;
+
+
+static PyObject* genbuild(PyObject* g)
+{
+ PyObject* x;
+ PyFrameObject* f;
+ PyCodeObject* co;
+ PyObject** dummy;
+ int i, res, ncells, nfrees;
+
+ x = PyObject_GetAttrString(g, "gi_running");
+ if (x == NULL)
+ return NULL;
+ res = PyObject_IsTrue(x);
+ Py_DECREF(x);
+ if (res < 0)
+ return NULL;
+ if (res) {
+ PyErr_SetString(PyExc_ValueError, "generator is running");
+ return NULL;
+ }
+
+ x = PyObject_GetAttrString(g, "gi_frame");
+ if (x == NULL)
+ return NULL;
+ if (!PyFrame_Check(x)) {
+ if (x == Py_None) {
+ /* Python 2.5 only: exhausted generators have g.gi_frame == None */
+ Py_DECREF(x);
+ Py_INCREF(empty_iterator);
+ return empty_iterator;
+ }
+ PyErr_SetString(PyExc_TypeError, "g.gi_frame must be a frame object");
+ goto error;
+ }
+ f = (PyFrameObject*) x;
+ co = f->f_code;
+
+ if (!(co->co_flags & CO_GENERATOR)) {
+ PyErr_SetString(PyExc_ValueError, "the frame is not from a generator");
+ goto error;
+ }
+ if (f->f_stacktop == NULL) {
+ Py_DECREF(f);
+ Py_INCREF(g); /* exhausted -- can return 'g' itself */
+ return g;
+ }
+ ncells = PyTuple_GET_SIZE(co->co_cellvars);
+ nfrees = PyTuple_GET_SIZE(co->co_freevars);
+ if (nfrees || ncells) {
+ PyErr_SetString(PyExc_ValueError, "generator has cell or free vars");
+ goto error;
+ }
+
+ if (co->co_argcount == 0)
+ dummy = NULL;
+ else
+ {
+ dummy = (PyObject**) malloc(co->co_argcount * sizeof(PyObject*));
+ if (dummy == NULL)
+ {
+ PyErr_NoMemory();
+ goto error;
+ }
+ for (i=0; i<co->co_argcount; i++)
+ dummy[i] = Py_None;
+ }
+ x = PyEval_EvalCodeEx(co, f->f_globals, f->f_locals,
+ dummy, co->co_argcount, NULL, 0,
+ NULL, 0, NULL);
+ if (dummy)
+ free(dummy);
+ Py_DECREF(f);
+ return x;
+
+ error:
+ Py_DECREF(x);
+ return NULL;
+}
+
+static int gencopy(PyObject* g2, PyObject* g)
+{
+ PyObject* x;
+ PyFrameObject* f = NULL;
+ PyFrameObject* f2 = NULL;
+ PyCodeObject* co;
+ int i, res;
+
+ if (g != g2)
+ {
+ if (g2->ob_type != g->ob_type)
+ {
+ if (g2 == empty_iterator)
+ return 0;
+ PyErr_SetString(PyExc_TypeError, "type mismatch");
+ return -1;
+ }
+
+ x = PyObject_GetAttrString(g, "gi_frame");
+ if (x == NULL)
+ return -1;
+ if (!PyFrame_Check(x)) {
+ PyErr_SetString(PyExc_TypeError, "g.gi_frame must be a frame object");
+ Py_DECREF(x);
+ goto error;
+ }
+ f = (PyFrameObject*) x;
+ co = f->f_code;
+
+ x = PyObject_GetAttrString(g2, "gi_frame");
+ if (x == NULL)
+ return -1;
+ if (!PyFrame_Check(x)) {
+ PyErr_SetString(PyExc_TypeError, "returned gi_frame");
+ Py_DECREF(x);
+ goto error;
+ }
+ f2 = (PyFrameObject*) x;
+
+ if (f2->f_code != co) {
+ PyErr_SetString(PyExc_TypeError, "generator code mismatch");
+ goto error;
+ }
+
+ if (f2->f_stacktop != NULL)
+ while (f2->f_stacktop != f2->f_localsplus)
+ {
+ f2->f_stacktop--;
+ Py_XDECREF(*f2->f_stacktop);
+ }
+
+ res = f->f_stacktop - f->f_localsplus;
+ f2->f_lasti = f->f_lasti;
+ f2->f_iblock = f->f_iblock;
+ memcpy(f2->f_blockstack, f->f_blockstack, sizeof(PyTryBlock)*f->f_iblock);
+ f2->f_stacktop = f2->f_localsplus;
+ for (i=0; i<res; i++)
+ {
+ x = f->f_localsplus[i];
+ if (x != NULL)
+ x = copyrec(x);
+ *f2->f_stacktop++ = x;
+ }
+ }
+ return 0;
+
+ error:
+ Py_XDECREF(f);
+ Py_XDECREF(f2);
+ return -1;
+}
+
+
+typedef struct {
+ PyObject_HEAD
+ long it_index;
+ PyObject *it_seq; /* Set to NULL when iterator is exhausted */
+} seqiterobject;
+static PyObject* seqiterbuild(PyObject* o)
+{
+ seqiterobject* iter = (seqiterobject*) o;
+ if (iter->it_seq == NULL)
+ {
+ Py_INCREF(iter); /* exhausted */
+ return (PyObject*) iter;
+ }
+ else
+ return PySeqIter_New(iter->it_seq);
+}
+static int seqitercopy(PyObject* o2, PyObject* o)
+{
+ PyObject* x;
+ seqiterobject* iter = (seqiterobject*) o;
+ seqiterobject* iter2 = (seqiterobject*) o2;
+
+ iter2->it_index = iter->it_index;
+ if (iter->it_seq != NULL)
+ {
+ x = copyrec(iter->it_seq);
+ Py_XDECREF(iter2->it_seq);
+ iter2->it_seq = x;
+ }
+ return 0;
+}
+
+#if PY_VERSION_HEX >= 0x02030000 /* 2.3 */
+/* pff */
+typedef struct {
+ PyObject_HEAD
+ long it_index;
+ PyListObject *it_seq; /* Set to NULL when iterator is exhausted */
+} listiterobject;
+static PyTypeObject* PyListIter_TypePtr;
+static PyObject* listiterbuild(PyObject* o)
+{
+ listiterobject* iter = (listiterobject*) o;
+ if (iter->it_seq == NULL)
+ {
+ Py_INCREF(iter); /* exhausted */
+ return (PyObject*) iter;
+ }
+ else
+ return PyList_Type.tp_iter((PyObject*) iter->it_seq);
+}
+static int listitercopy(PyObject* o2, PyObject* o)
+{
+ PyObject* x;
+ listiterobject* iter = (listiterobject*) o;
+ listiterobject* iter2 = (listiterobject*) o2;
+
+ iter2->it_index = iter->it_index;
+ if (iter->it_seq != NULL)
+ {
+ x = copyrec((PyObject*) iter->it_seq);
+ Py_XDECREF(iter2->it_seq);
+ iter2->it_seq = (PyListObject*) x;
+ }
+ return 0;
+}
+
+typedef struct {
+ PyObject_HEAD
+ long it_index;
+ PyTupleObject *it_seq; /* Set to NULL when iterator is exhausted */
+} tupleiterobject;
+static PyTypeObject* PyTupleIter_TypePtr;
+static PyObject* tupleiterbuild(PyObject* o)
+{
+ tupleiterobject* iter = (tupleiterobject*) o;
+ if (iter->it_seq == NULL)
+ {
+ Py_INCREF(iter); /* exhausted */
+ return (PyObject*) iter;
+ }
+ else
+ return PyTuple_Type.tp_iter((PyObject*) iter->it_seq);
+}
+static int tupleitercopy(PyObject* o2, PyObject* o)
+{
+ PyObject* x;
+ tupleiterobject* iter = (tupleiterobject*) o;
+ tupleiterobject* iter2 = (tupleiterobject*) o2;
+
+ iter2->it_index = iter->it_index;
+ if (iter->it_seq != NULL)
+ {
+ x = copyrec((PyObject*) iter->it_seq);
+ Py_XDECREF(iter2->it_seq);
+ iter2->it_seq = (PyTupleObject*) x;
+ }
+ return 0;
+}
+#endif /* PY_VERSION_HEX >= 0x02030000 */
+
+
+/* HACKS HACKS HACKS */
+
+typedef struct {
+ PyObject_HEAD
+ PyObject* o;
+} KeyObject;
+
+#define KEYS_BY_BLOCK 1024
+
+struct key_block {
+ KeyObject keys[KEYS_BY_BLOCK];
+ struct key_block* next;
+};
+
+static long key_hash(KeyObject* k)
+{
+ return (long)(k->o);
+}
+
+static PyObject* key_richcmp(KeyObject* k1, KeyObject* k2, int op)
+{
+ PyObject* r;
+ assert(op == 2 /*PyCmp_EQ*/ );
+ r = k1->o == k2->o ? Py_True : Py_False;
+ Py_INCREF(r);
+ return r;
+}
+
+static PyTypeObject keytype = {
+ PyObject_HEAD_INIT(NULL)
+ 0,
+ "key",
+ sizeof(KeyObject),
+ 0,
+ 0, /* tp_dealloc */
+ 0, /* tp_print */
+ 0, /* tp_getattr */
+ 0, /* tp_setattr */
+ 0, /* tp_compare */
+ 0, /* tp_repr */
+ 0, /* tp_as_number */
+ 0, /* tp_as_sequence */
+ 0, /* tp_as_mapping */
+ (hashfunc)key_hash, /* tp_hash */
+ 0, /* tp_call */
+ 0, /* tp_str */
+ 0, /* tp_getattro */
+ 0, /* tp_setattro */
+ 0, /* tp_as_buffer */
+ Py_TPFLAGS_DEFAULT, /* tp_flags */
+ 0, /* tp_doc */
+ 0, /* tp_traverse */
+ 0, /* tp_clear */
+ (richcmpfunc)key_richcmp, /* tp_richcompare */
+};
+
+
+/* global state */
+static PyObject* ss_memo;
+static struct key_block* ss_block;
+static int ss_next_in_block;
+static PyObject *ss_error, *ss_errinst, *ss_errtb;
+
+static PyObject* str_inst_build;
+static PyTypeObject* GeneratorType;
+
+/* never returns NULL, and never returns with a Python exception set! */
+static PyObject* copyrec(PyObject* o)
+{
+ PyTypeObject* t;
+ PyObject* n;
+ PyObject* key;
+ KeyObject* fkey;
+
+ if (o == Py_None || o->ob_type == &PyInt_Type ||
+ o->ob_type == &PyString_Type || o->ob_type == &PyFloat_Type ||
+ o == empty_iterator)
+ {
+ Py_INCREF(o);
+ return o;
+ }
+ if (ss_next_in_block < 0)
+ {
+ struct key_block* b = (struct key_block*) malloc(sizeof(struct key_block));
+ if (!b) { PyErr_NoMemory(); goto fail1; }
+ b->next = ss_block;
+ ss_block = b;
+ ss_next_in_block = KEYS_BY_BLOCK - 1;
+ }
+ fkey = ss_block->keys + ss_next_in_block;
+ fkey->ob_refcnt = 1;
+ fkey->ob_type = &keytype;
+ fkey->o = o;
+ key = (PyObject*) fkey;
+ n = PyDict_GetItem(ss_memo, key);
+ if (n)
+ {
+ Py_INCREF(n);
+ return n;
+ }
+ ss_next_in_block--;
+ Py_INCREF(o); /* reference stored in 'fkey->o' */
+ t = o->ob_type;
+ if (t == &PyTuple_Type)
+ {
+ int i, count = PyTuple_GET_SIZE(o);
+ n = PyTuple_New(count);
+ if (!n || PyDict_SetItem(ss_memo, key, n)) goto fail;
+ for (i=0; i<count; i++)
+ PyTuple_SET_ITEM(n, i, copyrec(PyTuple_GET_ITEM(o, i)));
+ return n;
+ }
+ if (t == &PyList_Type)
+ {
+ int i, count = PyList_GET_SIZE(o);
+ n = PyList_New(count);
+ if (!n || PyDict_SetItem(ss_memo, key, n)) goto fail;
+ for (i=0; i<count; i++)
+ PyList_SET_ITEM(n, i, copyrec(PyList_GET_ITEM(o, i)));
+ return n;
+ }
+ if (t == &PyDict_Type)
+ {
+ int i = 0;
+ PyObject* dictkey;
+ PyObject* dictvalue;
+ n = PyDict_New();
+ if (!n || PyDict_SetItem(ss_memo, key, n)) goto fail;
+ while (PyDict_Next(o, &i, &dictkey, &dictvalue))
+ if (PyDict_SetItem(n, copyrec(dictkey), copyrec(dictvalue)))
+ goto fail;
+ return n;
+ }
+ if (t == &PyInstance_Type)
+ {
+ int i = 0;
+ PyObject* dictkey;
+ PyObject* dictvalue;
+ PyObject* dsrc;
+ PyObject* ddest;
+ PyObject* inst_build = PyObject_GetAttr(o, str_inst_build);
+ if (inst_build == NULL)
+ {
+ PyErr_Clear();
+ goto unmodified;
+ }
+ n = PyObject_CallObject(inst_build, NULL);
+ Py_DECREF(inst_build);
+ if (!n || PyDict_SetItem(ss_memo, key, n)) goto fail;
+ dsrc = ((PyInstanceObject*) o)->in_dict;
+ ddest = ((PyInstanceObject*) n)->in_dict;
+ while (PyDict_Next(dsrc, &i, &dictkey, &dictvalue))
+ if (PyDict_SetItem(ddest, copyrec(dictkey), copyrec(dictvalue)))
+ goto fail;
+ return n;
+ }
+ if (t == &PyFunction_Type)
+ {
+ int i, count;
+ PyObject* tsrc = PyFunction_GET_DEFAULTS(o);
+ PyObject* tdest;
+ if (!tsrc) goto unmodified;
+ count = PyTuple_GET_SIZE(tsrc);
+ if (count == 0) goto unmodified;
+ n = PyFunction_New(PyFunction_GET_CODE(o), PyFunction_GET_GLOBALS(o));
+ if (!n || PyDict_SetItem(ss_memo, key, n)) goto fail;
+ tdest = PyTuple_New(count);
+ if (!tdest) goto fail;
+ for (i=0; i<count; i++)
+ PyTuple_SET_ITEM(tdest, i, copyrec(PyTuple_GET_ITEM(tsrc, i)));
+ i = PyFunction_SetDefaults(n, tdest);
+ Py_DECREF(tdest);
+ if (i) goto fail;
+ return n;
+ }
+ if (t == &PyMethod_Type)
+ {
+ PyObject* x;
+ n = PyMethod_New(PyMethod_GET_FUNCTION(o),
+ PyMethod_GET_SELF(o),
+ PyMethod_GET_CLASS(o));
+ if (!n || PyDict_SetItem(ss_memo, key, n)) goto fail;
+ x = copyrec(PyMethod_GET_FUNCTION(n));
+ Py_DECREF(PyMethod_GET_FUNCTION(n));
+ PyMethod_GET_FUNCTION(n) = x;
+ x = copyrec(PyMethod_GET_SELF(n));
+ Py_DECREF(PyMethod_GET_SELF(n));
+ PyMethod_GET_SELF(n) = x;
+ return n;
+ }
+ if (t == GeneratorType)
+ {
+ n = genbuild(o);
+ if (!n || PyDict_SetItem(ss_memo, key, n)) goto fail;
+ if (gencopy(n, o)) goto fail;
+ return n;
+ }
+ if (t == &PySeqIter_Type)
+ {
+ n = seqiterbuild(o);
+ if (!n || PyDict_SetItem(ss_memo, key, n)) goto fail;
+ if (seqitercopy(n, o)) goto fail;
+ return n;
+ }
+ #if PY_VERSION_HEX >= 0x02030000 /* 2.3 */
+ if (t == PyListIter_TypePtr)
+ {
+ n = listiterbuild(o);
+ if (!n || PyDict_SetItem(ss_memo, key, n)) goto fail;
+ if (listitercopy(n, o)) goto fail;
+ return n;
+ }
+ if (t == PyTupleIter_TypePtr)
+ {
+ n = tupleiterbuild(o);
+ if (!n || PyDict_SetItem(ss_memo, key, n)) goto fail;
+ if (tupleitercopy(n, o)) goto fail;
+ return n;
+ }
+ #endif
+
+ ss_next_in_block++;
+ return o; /* reference no longer stored in 'fkey->o' */
+
+ unmodified:
+ PyDict_SetItem(ss_memo, key, o);
+ Py_INCREF(o);
+ return o;
+
+ fail1:
+ n = NULL;
+ fail:
+ Py_INCREF(o);
+ Py_XDECREF(n);
+ if (ss_error == NULL)
+ PyErr_Fetch(&ss_error, &ss_errinst, &ss_errtb);
+ else
+ PyErr_Clear();
+ return o;
+}
+
+static PyObject* sscopy(PyObject* self, PyObject* o)
+{
+ PyObject* n;
+ ss_memo = PyDict_New();
+ if (!ss_memo)
+ return NULL;
+
+ ss_block = NULL;
+ ss_next_in_block = -1;
+ ss_error = NULL;
+ ss_errinst = NULL;
+ ss_errtb = NULL;
+ n = copyrec(o);
+ Py_DECREF(ss_memo);
+ while (ss_block)
+ {
+ int i;
+ struct key_block* b = ss_block;
+ ss_block = b->next;
+ for (i=ss_next_in_block+1; i<KEYS_BY_BLOCK; i++)
+ Py_DECREF(b->keys[i].o);
+ free(b);
+ ss_next_in_block = -1;
+ }
+ if (ss_error && !PyErr_Occurred())
+ PyErr_Restore(ss_error, ss_errinst, ss_errtb);
+ else
+ {
+ Py_XDECREF(ss_error);
+ Py_XDECREF(ss_errinst);
+ Py_XDECREF(ss_errtb);
+ }
+ if (PyErr_Occurred())
+ {
+ Py_DECREF(n);
+ n = NULL;
+ }
+ return n;
+}
+
+
+static PyMethodDef StateSaverMethods[] = {
+ {"copy", sscopy, METH_O},
+ {NULL, NULL} /* Sentinel */
+};
+
+void initstatesaver(void)
+{
+ PyObject* m;
+ PyObject* x, *y;
+ m = Py_InitModule("statesaver", StateSaverMethods);
+ if (m == NULL)
+ return;
+ keytype.ob_type = &PyType_Type;
+ str_inst_build = PyString_InternFromString("inst_build");
+
+ m = PyImport_ImportModule("types");
+ if (!m) return;
+ GeneratorType = (PyTypeObject*) PyObject_GetAttrString(m, "GeneratorType");
+ if (!GeneratorType) return;
+
+ x = PyTuple_New(0);
+ if (!x) return;
+ empty_iterator = PyObject_GetIter(x);
+ Py_DECREF(x);
+ if (!empty_iterator) return;
+ PyTupleIter_TypePtr = empty_iterator->ob_type;
+
+ x = PyList_New(0);
+ if (!x) return;
+ y = PyList_Type.tp_iter(x);
+ if (y) PyListIter_TypePtr = y->ob_type;
+ Py_XDECREF(y);
+ Py_DECREF(x);
+ if (!y) return;
+}
diff --git a/bubbob/statesaver.py b/bubbob/statesaver.py
new file mode 100644
index 0000000..85d3425
--- /dev/null
+++ b/bubbob/statesaver.py
@@ -0,0 +1,135 @@
+"""
+A pure Python implementation of statesaver.c that runs on top of PyPy.
+See description in statesaver.c.
+Difference: this supports new-style instances too.
+Use statesaver.standard_build() as the inst_build() if you want.
+"""
+
+from _pickle_support import generator_new
+import types
+
+def standard_build(self):
+ if type(self) is types.InstanceType:
+ # old-style instance
+ return types.InstanceType(self.__class__)
+ else:
+ # new-style instance
+ return type(self).__new__(type(self))
+
+# ____________________________________________________________
+
+def not_copied(x, memo):
+ return x
+
+def copy_custom_instance(x, memo):
+ try:
+ return memo[id(x)]
+ except KeyError:
+ y = x.inst_build()
+ memo[id(x)] = y
+ for key, value in x.__dict__.items():
+ y.__dict__[key] = copyrec(value, memo)
+ return y
+
+def copy_tuple(x, memo):
+ return tuple([copyrec(item, memo) for item in x])
+
+def copy_list(x, memo):
+ try:
+ return memo[id(x)]
+ except KeyError:
+ y = []
+ memo[id(x)] = y
+ for item in x:
+ y.append(copyrec(item, memo))
+ return y
+
+def copy_dict(x, memo):
+ try:
+ return memo[id(x)]
+ except KeyError:
+ y = {}
+ memo[id(x)] = y
+ for key, value in x.items():
+ y[copyrec(key, memo)] = copyrec(value, memo)
+ return y
+
+def copy_function(x, memo):
+ if not x.func_defaults:
+ return x # not copied
+ try:
+ return memo[id(x)]
+ except KeyError:
+ y = types.FunctionType(x.func_code, x.func_globals, x.func_name)
+ memo[id(x)] = y
+ y.func_defaults = copyrec(x.func_defaults, memo)
+ return y
+
+def copy_method(x, memo):
+ return types.MethodType(copyrec(x.im_func, memo),
+ copyrec(x.im_self, memo),
+ x.im_class)
+
+def copy_generator(x, memo):
+ try:
+ return memo[id(x)]
+ except KeyError:
+ y = generator_new(copyrec(x.gi_frame, memo), x.gi_running)
+ memo[id(x)] = y
+ return y
+
+def copy_frame(x, memo):
+ try:
+ return memo[id(x)]
+ except KeyError:
+ frame_new, args, state = x.__reduce__()
+ y = frame_new(*args)
+ memo[id(x)] = y
+ newstate = []
+ for item in state:
+ if not (item is x.f_globals or item is x.f_builtins):
+ item = copyrec(item, memo)
+ newstate.append(item)
+ y.__setstate__(newstate)
+ return y
+
+def copy_seqiter(x, memo):
+ try:
+ return memo[id(x)]
+ except KeyError:
+ # XXX self-recursion is not correctly handled here
+ seqiter_new, args = x.__reduce__()
+ args = [copyrec(item, memo) for item in args]
+ y = seqiter_new(*args)
+ memo[id(x)] = y
+ return y
+
+# ____________________________________________________________
+
+type_handlers = {tuple: copy_tuple,
+ list: copy_list,
+ dict: copy_dict,
+ types.FunctionType: copy_function,
+ types.MethodType: copy_method,
+ types.GeneratorType: copy_generator,
+ types.FrameType: copy_frame,
+ type(iter([])): copy_seqiter,
+ }
+
+def no_handler_found(x, memo):
+ if hasattr(x, '__dict__') and hasattr(x.__class__, 'inst_build'):
+ handler = copy_custom_instance
+ else:
+ handler = not_copied
+ type_handlers[x.__class__] = handler
+ return handler(x, memo)
+
+def copyrec(x, memo):
+ try:
+ cls = x.__class__
+ except AttributeError:
+ return x # 'cls' is likely an old-style class object
+ return type_handlers.get(cls, no_handler_found)(x, memo)
+
+def copy(x):
+ return copyrec(x, {})
diff --git a/bubbob/statesaver.so b/bubbob/statesaver.so
new file mode 100755
index 0000000..bf805e5
--- /dev/null
+++ b/bubbob/statesaver.so
Binary files differ
diff --git a/bubbob/test_rnglevel.py b/bubbob/test_rnglevel.py
new file mode 100644
index 0000000..6b65514
--- /dev/null
+++ b/bubbob/test_rnglevel.py
@@ -0,0 +1,64 @@
+#
+# This test generates 100 times 25 random levels and checks
+# that it doesn't crash, and that it gives levels that are
+# possible (in the limited sense of not having any full-
+# column walls)
+#
+# this test accepts the following parameters:
+# -wall show the level layout
+# -wind show the level wind pattern
+# -seed N use random seed N for the generation
+#
+
+import sys
+import random
+sys.path.append('..')
+sys.path.append('../common')
+
+n_lvls = 100
+show_lvl = 0
+
+idx = 0
+while idx < len(sys.argv):
+ arg = sys.argv[idx]
+ idx += 1
+ if arg == '-wall':
+ show_lvl |= 1
+ n_lvls = 2
+ if arg == '-wind':
+ show_lvl |= 2
+ n_lvls = 2
+ if arg == '-seed':
+ arg = sys.argv[idx]
+ idx += 1
+ print "Using seed: " + arg + "\n"
+ random.seed(arg)
+
+def printlvl(level):
+ if show_lvl:
+ print "\n\n"
+ for y in range(level.HEIGHT):
+ str = ""
+ if show_lvl & 1:
+ str = level.walls[y]
+ if show_lvl & 2:
+ if str:
+ str += " | "
+ str += level.winds[y]
+ print str
+
+for i in range(n_lvls):
+ print '%4d:' % i,
+ d = {'__name__': 'RandomLevels'}
+ execfile('levels/RandomLevels.py', d)
+ for i, Lvl in enumerate(d['GenerateLevels']()):
+ level = Lvl(i)
+ printlvl(level)
+ for x in range(2, level.width-2):
+ for y in range(0, level.height):
+ if level.walls[y][x] == ' ':
+ break
+ else:
+ for line in level.walls:
+ print line
+ raise AssertionError("full height wall in column %d" % x)
diff --git a/bubbob/test_statesaver.py b/bubbob/test_statesaver.py
new file mode 100644
index 0000000..3fadcd7
--- /dev/null
+++ b/bubbob/test_statesaver.py
@@ -0,0 +1,127 @@
+import py
+import statesaver
+import new
+
+def test_list():
+ lst = [None, 12, "hello", 3.4, ("foo", (), [])]
+ lst1 = statesaver.copy(lst)
+ assert lst1 == lst
+ assert lst1 is not lst
+ assert lst1[-1][-1] is not lst[-1][-1]
+
+def test_dict():
+ dct = {1: "hi", 2: {}}
+ dct1 = statesaver.copy(dct)
+ assert dct1 == dct
+ assert dct1 is not dct
+ assert dct1[2] is not dct[2]
+
+def test_instance():
+ class Foo:
+ def inst_build(self):
+ return Bar()
+ class Bar:
+ pass
+ x = Foo()
+ x.attr = [1, 2, 3]
+ y = statesaver.copy(x)
+ assert y.__class__ is Bar
+ assert y.attr == [1, 2, 3]
+ assert y.attr is not x.attr
+
+glob = 2
+def test_function():
+ # XXX closures not supported
+ def func(x, y=[]):
+ assert glob == 2
+ y.append(x)
+ return y
+ l = func(5)
+ l = func(6)
+ assert l == [5, 6]
+ func1 = statesaver.copy(func)
+ l = func(7)
+ l = func(8)
+ assert l == [5, 6, 7, 8]
+ l = func1(9)
+ l = func1(10)
+ assert l == [5, 6, 9, 10]
+
+def test_method():
+ def func(x, y=[]):
+ assert glob == 2
+ y.append(x)
+ return y
+ m = new.instancemethod(func, {})
+ assert m() == [{}]
+ m1 = statesaver.copy(m)
+ assert m() == [{}, {}]
+ assert m() == [{}, {}, {}]
+ assert m1() == [{}, {}]
+ assert m1() == [{}, {}, {}]
+ l = m1()
+ assert l[0] is l[1] is l[2] is l[3]
+
+def test_generator():
+ def gfunc():
+ lst = [5, 6]
+ yield lst.pop()
+ yield lst.pop()
+ g = gfunc()
+ assert g.next() == 6
+ g1 = statesaver.copy(g)
+ assert g.next() == 5
+ py.test.raises(StopIteration, g.next)
+ assert g1.next() == 5
+ py.test.raises(StopIteration, g1.next)
+
+def test_exhausted_gen():
+ def gfunc():
+ yield 5
+ g = gfunc()
+ for i in g:
+ print i
+ g1 = statesaver.copy(g)
+ assert iter(g1) is g1
+ py.test.raises(StopIteration, g1.next)
+ g2 = statesaver.copy(g1)
+ assert iter(g2) is g2
+ py.test.raises(StopIteration, g2.next)
+
+def test_seqiter():
+ from UserList import UserList
+ seq = UserList([2, 4, 6, 8])
+ it = iter(seq)
+ assert it.next() == 2
+ assert it.next() == 4
+ it1 = statesaver.copy(it)
+ assert list(it) == [6, 8]
+ assert list(it1) == [6, 8]
+
+def test_tupleiter():
+ tup = (2, 4, 6, 8)
+ it = iter(tup)
+ assert it.next() == 2
+ assert it.next() == 4
+ it1 = statesaver.copy(it)
+ assert list(it) == [6, 8]
+ assert list(it1) == [6, 8]
+
+def test_listiter():
+ lst = [2, 4, 6, 8]
+ it = iter(lst)
+ assert it.next() == 2
+ assert it.next() == 4
+ it1 = statesaver.copy(it)
+ lst.append(10)
+ assert list(it) == [6, 8, 10]
+ assert list(it1) == [6, 8]
+
+def test_stringiter():
+ s = "hello"
+ it = iter(s)
+ assert it.next() == 'h'
+ assert it.next() == 'e'
+ it1 = statesaver.copy(it)
+ assert list(it) == ['l', 'l', 'o']
+ assert list(it1) == ['l', 'l', 'o']
diff --git a/bubbob/tmp/pat00.ppm b/bubbob/tmp/pat00.ppm
new file mode 100644
index 0000000..9b8ce4d
--- /dev/null
+++ b/bubbob/tmp/pat00.ppm
Binary files differ
diff --git a/bubbob/tmp/pat01.ppm b/bubbob/tmp/pat01.ppm
new file mode 100644
index 0000000..236b6ea
--- /dev/null
+++ b/bubbob/tmp/pat01.ppm
Binary files differ
diff --git a/bubbob/tmp/pat02.ppm b/bubbob/tmp/pat02.ppm
new file mode 100644
index 0000000..341ecac
--- /dev/null
+++ b/bubbob/tmp/pat02.ppm
Binary files differ
diff --git a/bubbob/tmp/pat03.ppm b/bubbob/tmp/pat03.ppm
new file mode 100644
index 0000000..ad98a6f
--- /dev/null
+++ b/bubbob/tmp/pat03.ppm
Binary files differ
diff --git a/bubbob/tmp/pat04.ppm b/bubbob/tmp/pat04.ppm
new file mode 100644
index 0000000..c35f525
--- /dev/null
+++ b/bubbob/tmp/pat04.ppm
Binary files differ
diff --git a/bubbob/tmp/pat05.ppm b/bubbob/tmp/pat05.ppm
new file mode 100644
index 0000000..a7745ca
--- /dev/null
+++ b/bubbob/tmp/pat05.ppm
Binary files differ
diff --git a/bubbob/tmp/pat06.ppm b/bubbob/tmp/pat06.ppm
new file mode 100644
index 0000000..ed4e844
--- /dev/null
+++ b/bubbob/tmp/pat06.ppm
Binary files differ
diff --git a/bubbob/tmp/pat07.ppm b/bubbob/tmp/pat07.ppm
new file mode 100644
index 0000000..fbe1e7f
--- /dev/null
+++ b/bubbob/tmp/pat07.ppm
Binary files differ
diff --git a/bubbob/tmp/pat08.ppm b/bubbob/tmp/pat08.ppm
new file mode 100644
index 0000000..40c750a
--- /dev/null
+++ b/bubbob/tmp/pat08.ppm
Binary files differ
diff --git a/bubbob/tmp/pat09.ppm b/bubbob/tmp/pat09.ppm
new file mode 100644
index 0000000..5821f76
--- /dev/null
+++ b/bubbob/tmp/pat09.ppm
Binary files differ
diff --git a/bubbob/tmp/pat10.ppm b/bubbob/tmp/pat10.ppm
new file mode 100644
index 0000000..651c51b
--- /dev/null
+++ b/bubbob/tmp/pat10.ppm
Binary files differ
diff --git a/bubbob/tmp/pat11.ppm b/bubbob/tmp/pat11.ppm
new file mode 100644
index 0000000..4854c3e
--- /dev/null
+++ b/bubbob/tmp/pat11.ppm
Binary files differ
diff --git a/bubbob/tmp/pat12.ppm b/bubbob/tmp/pat12.ppm
new file mode 100644
index 0000000..7482e32
--- /dev/null
+++ b/bubbob/tmp/pat12.ppm
Binary files differ
diff --git a/bubbob/tmp/pat13.ppm b/bubbob/tmp/pat13.ppm
new file mode 100644
index 0000000..8fbe9a4
--- /dev/null
+++ b/bubbob/tmp/pat13.ppm
Binary files differ
diff --git a/bubbob/tmp/pat14.ppm b/bubbob/tmp/pat14.ppm
new file mode 100644
index 0000000..54625b2
--- /dev/null
+++ b/bubbob/tmp/pat14.ppm
Binary files differ
diff --git a/bubbob/tmp/pat15.ppm b/bubbob/tmp/pat15.ppm
new file mode 100644
index 0000000..d50dd6f
--- /dev/null
+++ b/bubbob/tmp/pat15.ppm
Binary files differ
diff --git a/bubbob/tmp/pat16.ppm b/bubbob/tmp/pat16.ppm
new file mode 100644
index 0000000..f3a1881
--- /dev/null
+++ b/bubbob/tmp/pat16.ppm
@@ -0,0 +1,4 @@
+P6
+40 256
+255
+ªªªªªªªªªªªªªªªªªªÝÝÝ»»»wwwwwwwwwUUUwwwUUUDDDˆˆˆ»»»UUUwwwˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆUUUwwwˆˆˆªªªˆˆˆªªªˆˆˆªªª•••‚‚‚ooo]]]JJJ777%%%ªªªªªªªªªªªªªªªÝÝÝ»»»wwwwwwwwwUUUwwwUUUDDDˆˆˆîîª»»»UUUwwwˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆUUUwwwˆˆˆªªªˆˆˆªªªˆˆˆ•••‚‚‚ooo]]]JJJ777%%%ªªªªªªªªªªªªÝÝÝ»»»wwwwwwwwwUUUwwwUUUDDDˆˆˆîîîÝÝ݈ˆˆªªª»»»UUUwwwˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆUUUwwwˆˆˆªªªˆˆˆªªª•••‚‚‚ooo]]]JJJ777%%%ªªªªªªªªªÝÝÝ»»»wwwwwwwwwUUUwwwUUUDDDˆˆˆîîîÝÝÝ»»»ªªªˆˆˆªªª»»»UUUwwwˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆUUUwwwˆˆˆªªªˆˆˆ•••‚‚‚ooo]]]JJJ777%%%ªªªªªªÝÝÝ»»»wwwwwwwwwUUUwwwUUUDDDˆˆˆîîîÝÝÝ»»»»»»ˆˆˆªªªˆˆˆªªª»»»UUUwwwˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆUUUwwwˆˆˆªªª•••‚‚‚ooo]]]JJJ777%%%ªªªÝÝÝ»»»wwwwwwwwwUUUwwwUUUDDDˆˆˆîîîÝÝÝ»»»»»»ÝÝ݈ˆˆˆˆˆªªªˆˆˆªªª»»»UUUwwwˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆUUUwwwˆˆˆ•••‚‚‚ooo]]]JJJ777%%%ÝÝÝ»»»wwwwwwwwwUUUwwwUUUDDDˆˆˆîîîÝÝÝ»»»»»»ÝÝÝ»»»ˆˆˆˆˆˆˆˆˆªªªˆˆˆªªª»»»UUUwwwˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆUUUwww•••‚‚‚ooo]]]JJJ777%%%»»»wwwwwwwwwUUUwwwUUUDDDˆˆˆîîîÝÝÝ»»»»»»ÝÝÝ»»»»»»ˆˆˆˆˆˆˆˆˆˆˆˆªªªˆˆˆªªª»»»UUUwwwˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆUUU•••‚‚‚ooo]]]JJJ777%%%wwwwwwwwwUUUwwwUUUDDDˆˆˆîîîÝÝÝ»»»»»»ÝÝÝ»»»»»»ˆˆˆUUUˆˆˆˆˆˆˆˆˆˆˆˆªªªˆˆˆªªª»»»UUUwwwˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ•••‚‚‚ooo]]]JJJ777%%%wwwwwwUUUwwwUUUDDDˆˆˆîîîÝÝÝ»»»»»»ÝÝÝ»»»»»»ˆˆˆªªªwwwUUUˆˆˆˆˆˆˆˆˆˆˆˆªªªˆˆˆªªª»»»UUUwwwˆˆˆˆˆˆˆˆˆˆˆˆ•••‚‚‚ooo]]]JJJ777%%%wwwUUUwwwUUUDDDˆˆˆîîîÝÝÝ»»»»»»ÝÝÝ»»»»»»ˆˆˆªªªÝÝ݈ˆˆwwwUUUˆˆˆˆˆˆˆˆˆˆˆˆªªªˆˆˆªªª»»»UUUwwwˆˆˆˆˆˆˆˆˆ•••‚‚‚ooo]]]JJJ777%%%UUUwwwUUUDDDˆˆˆîîîÝÝÝ»»»»»»ÝÝÝ»»»»»»ˆˆˆªªªÝÝÝ»»»ˆˆˆˆˆˆwwwUUUˆˆˆˆˆˆˆˆˆˆˆˆªªªˆˆˆªªª»»»UUUwwwˆˆˆˆˆˆ•••‚‚‚ooo]]]JJJ777%%%wwwUUUDDDˆˆˆîîîÝÝÝ»»»»»»ÝÝÝ»»»»»»ˆˆˆªªªÝÝÝ»»»»»»ˆˆˆˆˆˆˆˆˆwwwUUUˆˆˆˆˆˆˆˆˆˆˆˆªªªˆˆˆªªª»»»UUUwwwˆˆˆ•••‚‚‚ooo]]]JJJ777%%%UUUDDDˆˆˆîîîÝÝÝ»»»»»»ÝÝÝ»»»»»»ˆˆˆªªªÝÝÝ»»»ÝÝÝ»»»ˆˆˆˆˆˆˆˆˆˆˆˆwwwUUUˆˆˆˆˆˆˆˆˆˆˆˆªªªˆˆˆªªª»»»UUUwww•••‚‚‚ooo]]]JJJ777%%%DDDˆˆˆîîîÝÝÝ»»»»»»ÝÝÝ»»»»»»ˆˆˆªªªÝÝÝ»»»ÝÝÝ»»»ÝÝ݈ˆˆˆˆˆˆˆˆˆˆˆˆˆˆwwwUUUˆˆˆˆˆˆˆˆˆˆˆˆªªªˆˆˆªªª»»»UUU•••‚‚‚ooo]]]JJJ777%%%ˆˆˆîîîÝÝÝ»»»»»»ÝÝÝ»»»»»»ˆˆˆªªªÝÝÝ»»»ÝÝÝ»»»ÝÝÝîîªˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆwwwUUUˆˆˆˆˆˆˆˆˆˆˆˆªªªˆˆˆªªªˆˆˆ•••‚‚‚ooo]]]JJJ777%%%ˆˆˆUUUwwwªªªˆˆˆwwwªªªªªªwwwˆˆˆªªªˆˆˆªªªˆˆˆªªªˆˆˆDDDwwwUUUwwwUUUwwwUUUDDDwwwwwwUUUwwwUUUUUUDDDªªª•••‚‚‚ooo]]]JJJ777%%%ÝÝ݈ˆˆUUUwwwªªªˆˆˆwwwªªªªªªwwwˆˆˆªªªˆˆˆªªªˆˆˆªªªwwwUUUwwwUUUwwwUUUDDDwwwwwwUUUwwwUUUUUUDDDªªªîîî•••‚‚‚ooo]]]JJJ777%%%»»»ÝÝ݈ˆˆUUUwwwªªªˆˆˆwwwªªªªªªwwwˆˆˆªªªˆˆˆªªªˆˆˆUUUwwwUUUwwwUUUDDDwwwwwwUUUwwwUUUUUUDDDªªªîîîÝÝÝ•••‚‚‚ooo]]]JJJ777%%%»»»»»»ÝÝ݈ˆˆUUUwwwªªªˆˆˆwwwªªªªªªwwwˆˆˆªªªˆˆˆªªªwwwUUUwwwUUUDDDwwwwwwUUUwwwUUUUUUDDDªªªîîîÝÝÝ»»»•••‚‚‚ooo]]]JJJ777%%%ªªª»»»»»»ÝÝ݈ˆˆUUUwwwªªªˆˆˆwwwªªªªªªwwwˆˆˆªªªˆˆˆUUUwwwUUUDDDwwwwwwUUUwwwUUUUUUDDDªªªîîîÝÝÝ»»»»»»•••‚‚‚ooo]]]JJJ777%%%»»»ªªª»»»»»»ÝÝ݈ˆˆUUUwwwªªªˆˆˆwwwªªªªªªwwwˆˆˆªªªwwwUUUDDDwwwwwwUUUwwwUUUUUUDDDªªªîîîÝÝÝ»»»»»»»»»•••‚‚‚ooo]]]JJJ777%%%ªªª»»»ªªª»»»»»»ÝÝ݈ˆˆUUUwwwªªªˆˆˆwwwªªªªªªwwwˆˆˆUUUDDDwwwwwwUUUwwwUUUUUUDDDªªªîîîÝÝÝ»»»»»»»»»ˆˆˆ•••‚‚‚ooo]]]JJJ777%%%ªªªªªª»»»ªªª»»»»»»ÝÝ݈ˆˆUUUwwwªªªˆˆˆwwwªªªªªªwwwDDDwwwwwwUUUwwwUUUUUUDDDªªªîîîÝÝÝ»»»»»»»»»ˆˆˆ»»»•••‚‚‚ooo]]]JJJ777%%%ÝÝݪªªªªª»»»ªªª»»»»»»ÝÝ݈ˆˆUUUwwwªªªˆˆˆwwwªªªªªªwwwwwwUUUwwwUUUUUUDDDªªªîîîÝÝÝ»»»»»»»»»ˆˆˆ»»»www•••‚‚‚ooo]]]JJJ777%%%»»»ÝÝݪªªªªª»»»ªªª»»»»»»ÝÝ݈ˆˆUUUwwwªªªˆˆˆwwwªªªwwwUUUwwwUUUUUUDDDªªªîîîÝÝÝ»»»»»»»»»ˆˆˆ»»»wwwUUU•••‚‚‚ooo]]]JJJ777%%%ªªª»»»ÝÝݪªªªªª»»»ªªª»»»»»»ÝÝ݈ˆˆUUUwwwªªªˆˆˆwwwUUUwwwUUUUUUDDDªªªîîîÝÝÝ»»»»»»»»»ˆˆˆ»»»wwwUUUªªª•••‚‚‚ooo]]]JJJ777%%%ªªªªªª»»»ÝÝݪªªªªª»»»ªªª»»»»»»ÝÝ݈ˆˆUUUwwwªªªˆˆˆwwwUUUUUUDDDªªªîîîÝÝÝ»»»»»»»»»ˆˆˆ»»»wwwUUUªªªˆˆˆ•••‚‚‚ooo]]]JJJ777%%%ªªªªªªªªª»»»ÝÝݪªªªªª»»»ªªª»»»»»»ÝÝ݈ˆˆUUUwwwªªªUUUUUUDDDªªªîîîÝÝÝ»»»»»»»»»ˆˆˆ»»»wwwUUUªªªˆˆˆªªª•••‚‚‚ooo]]]JJJ777%%%ªªªªªªªªªªªª»»»ÝÝݪªªªªª»»»ªªª»»»»»»ÝÝ݈ˆˆUUUwwwUUUDDDªªªîîîÝÝÝ»»»»»»»»»ˆˆˆ»»»wwwUUUªªªˆˆˆªªªˆˆˆ•••‚‚‚ooo]]]JJJ777%%%ªªªªªªªªªªªªªªª»»»ÝÝݪªªªªª»»»ªªª»»»»»»ÝÝ݈ˆˆUUUDDDªªªîîîÝÝÝ»»»»»»»»»ˆˆˆ»»»wwwUUUªªªˆˆˆªªªˆˆˆªªª•••‚‚‚ooo]]]JJJ777%%%ªªªªªªªªªªªªªªªªªª»»»ÝÝݪªªªªª»»»ªªª»»»»»»ÝÝ݈ˆˆªªªîîîÝÝÝ»»»»»»»»»ˆˆˆ»»»wwwUUUªªªˆˆˆªªªˆˆˆªªªwww•••‚‚‚ooo]]]JJJ777%%%ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff£££ŽŽŽzzzeeeQQQ===(((ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™™™™™™™™™™ªªª™™™ªªªªªªªªª»»»»»»»»»ÝÝÝ»»»ÝÝÝÝÝÝÝÝÝîîîîîîîîîÿÿÿîîîÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™™™™™™™™™™™™™ªªªªªªªªª»»»ªªª»»»»»»»»»ÝÝÝÝÝÝÝÝÝîîîÝÝÝîîîîîîîîîÿÿÿîîîÿÿÿÿÿÿÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™™™™™™™™™™ªªª™™™ªªªªªªªªª»»»™™™™™™™™™™™™wwwwwwÝÝÝîîîîîîîîîÿÿÿîîîÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™™™™™™™™™™™™™ªªªªªª™™™ªªªªªªªªªªªªªªªªªªªªªªªªªªªwwwwwwîîîîîîÿÿÿîîîÿÿÿÿÿÿÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™™™™™™™™™™ªªª™™™™™™ªªª»»»»»»»»»»»»»»»»»»»»»»»»ªªªªªª™™™wwwÿÿÿîîîÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™™™™™™™™™™™™™™™™ªªª»»»ÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝ»»»»»»ªªªªªª™™™wwwÿÿÿîîîÿÿÿÿÿÿÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™™™™™™™™™™™™™ªªª»»»ÝÝÝÝÝÝîîîîîîîîîÝÝÝÝÝÝÝÝÝÝÝÝ»»»»»»ªªªªªª™™™wwwÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™™™™™™™™™™ªªª»»»ÝÝÝÝÝÝîîîîîîÿÿÿîîîîîîÝÝÝÝÝÝÝÝÝ»»»»»»»»»ªªªªªª™™™wwwÿÿÿÿÿÿÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™™™™™™™™™™ªªª»»»ÝÝÝÝÝÝÝÝÝîîîîîîîîîÝÝÝÝÝÝÝÝÝÝÝÝ»»»»»»»»»ªªªªªª™™™™™™wwwÿÿÿÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™™™™™™™ªªªªªª»»»ÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝ»»»»»»»»»»»»ªªªªªª™™™™™™wwwÿÿÿÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™™™™™™™ªªªªªª»»»ÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝ»»»»»»»»»»»»»»»ªªªªªª™™™™™™wwwÿÿÿÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™ˆˆˆ™™™ªªªªªª»»»»»»ÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝ»»»»»»»»»»»»»»»ªªªªªªªªª™™™™™™ˆˆˆwwwÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™ˆˆˆ™™™ªªªªªªªªª»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»ªªªªªª™™™™™™™™™ˆˆˆwwwÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™ˆˆˆ™™™™™™ªªªªªªªªª»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»ªªªªªªªªª™™™™™™™™™ˆˆˆwwwÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™ˆˆˆ™™™™™™ªªªªªªªªªªªª»»»»»»»»»»»»»»»»»»»»»ªªªªªªªªªªªª™™™™™™™™™™™™ˆˆˆwwwÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™wwwˆˆˆ™™™™™™ªªªªªªªªªªªªªªªªªªªªªªªªªªªªªªªªªªªªªªª™™™™™™™™™™™™ˆˆˆˆˆˆwwwÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™wwwˆˆˆ™™™™™™™™™™™™ªªªªªªªªªªªªªªªªªªªªªªªªªªªªªª™™™™™™™™™™™™ˆˆˆˆˆˆˆˆˆwwwÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™™™™ˆˆˆˆˆˆ™™™™™™™™™™™™™™™ªªªªªªªªªªªªªªª™™™™™™™™™™™™™™™™™™ˆˆˆˆˆˆˆˆˆwwwÿÿÿÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™™™™wwwˆˆˆˆˆˆ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ˆˆˆˆˆˆˆˆˆˆˆˆwwwÿÿÿÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™™™™wwwˆˆˆˆˆˆˆˆˆ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆwwwÿÿÿÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™™™™™™™wwwˆˆˆˆˆˆˆˆˆˆˆˆ™™™™™™™™™™™™™™™™™™™™™™™™ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆwwwÿÿÿÿÿÿÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™™™™™™™™™™wwwˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆwwwîîîÿÿÿÿÿÿÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™™™™™™™™™™ªªªwwwˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆwwwîîîÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™™™™™™™™™™™™™ªªªwwwˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆwwwîîîÿÿÿîîîÿÿÿÿÿÿÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™™™™™™™™™™ªªª™™™ªªªwwwwwwwwwˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆwwwwwwwwwîîîÿÿÿîîîÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™™™™™™™™™™™™™ªªªªªªªªª»»»ªªªwwwwwwwwwwwwwwwwwwîîîÝÝÝîîîîîîîîîÿÿÿîîîÿÿÿÿÿÿÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™™™™™™™™™™ªªª™™™ªªªªªªªªª»»»»»»»»»ÝÝÝ»»»ÝÝÝÝÝÝÝÝÝîîîîîîîîîÿÿÿîîîÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffff™™™™™™™™™™™™™™™™™™ªªªªªªªªª»»»ªªª»»»»»»»»»ÝÝÝÝÝÝÝÝÝîîîÝÝÝîîîîîîîîîÿÿÿîîîÿÿÿÿÿÿÿÿÿÿÿÿffffff£££ŽŽŽzzzeeeQQQ===(((ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff£££ŽŽŽzzzeeeQQQ===(((ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff£££ŽŽŽzzzeeeQQQ===(((ÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌfffÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww™™Ì™™ÌfffÌÌÌwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌwwwˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ»»»ÝÝÝ™™Ì™™Ì™™ÌfffÌÌÌwwwˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ»»»ÝÝÝ™™Ì™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌwwwˆˆˆªªªªªªªªªªªªªªªªªª»»»ÝÝÝ™™Ì™™Ì™™Ì™™ÌfffÌÌÌwwwˆˆˆªªªªªªªªªªªªªªªªªª»»»ÝÝÝ™™Ì™™Ì™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌwwwˆˆˆªªªªªªªªªªªªªªª»»»ÝÝÝ™™Ì™™Ì™™Ì™™Ì™™ÌfffÌÌÌwwwˆˆˆªªªªªªªªªªªªªªª»»»ÝÝÝ™™Ì™™Ì™™Ì™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌwwwˆˆˆªªªªªªªªªªªª»»»ÝÝÝ™™Ì™™Ì™™Ì™™Ì™™Ì™™ÌfffÌÌÌwwwˆˆˆªªªªªªªªªªªª»»»ÝÝÝ™™Ì™™Ì™™Ì™™Ì™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌwwwˆˆˆªªªªªªªªª»»»ÝÝÝ™™Ì™™Ì™™ÌfffÌÌÌ™™Ì™™ÌfffÌÌÌwwwˆˆˆªªªªªªªªª»»»ÝÝÝ™™Ì™™Ì™™ÌfffÌÌÌ™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌwwwˆˆˆªªªªªª»»»ÝÝÝ™™Ì™™Ì™™Ìffff™ÌÌÌÌ™™Ì™™ÌfffÌÌÌwwwˆˆˆªªªªªª»»»ÝÝÝ™™Ì™™Ì™™Ìfffff™ÌÌÌ™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌwwwˆˆˆªªª»»»ÝÝÝ™™Ì™™Ì™™Ìffff™Ìf™ÌÌÌÌ™™Ì™™ÌfffÌÌÌwwwˆˆˆªªª»»»ÝÝÝ™™Ì™™Ì™™Ìfffff™™ÌÌÌÌÌ™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌwwwˆˆˆ»»»ÝÝÝ™™Ì™™Ì™™Ìffff™Ìf™Ìf™ÌÌÌÌ™™Ì™™ÌfffÌÌÌwwwˆˆˆ»»»ÝÝÝ™™Ì™™Ì™™Ìfffff™f™Ì™ÌÌÌÌÌ™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌwww»»»ÝÝÝ™™Ì™™Ì™™Ìffff™Ìf™Ìf™Ìf™ÌÌÌÌ™™Ì™™ÌfffÌÌÌwww»»»ÝÝÝ™™Ì™™Ì™™Ìfffff™f™Ìf™Ì™ÌÌÌÌÌ™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌwwwÝÝÝ™™Ì™™Ì™™Ìffff™Ìf™Ìf™Ìf™Ìf™ÌÌÌÌ™™Ì™™ÌfffÌÌÌwwwÝÝÝ™™Ì™™Ì™™Ìfffff™f™Ìf™Ìf™Ì™ÌÌÌÌÌ™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌwww™™Ì™™Ì™™Ì™™ÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌ™™Ì™™ÌfffÌÌÌwww™™Ì™™Ì™™Ìfffff™f™Ìf™Ìf™Ìf™Ì™ÌÌÌÌÌ™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌ™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™ÌfffÌÌÌ™™Ì™™Ì™™Ìfffff™f™Ìf™Ìf™Ìf™Ìf™Ì™ÌÌÌÌÌ™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌ™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ìfff™™Ì™™Ì™™Ìfffff™f™Ìf™Ìf™Ìf™Ìf™Ìf™Ì™ÌÌÌÌÌ™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-fffffffffffffffffffffffffffffffffffffffffffff™™Ì™™Ì™™Ìfffff™f™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ì™ÌÌÌÌÌ™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌ™™Ì™™Ì™™Ìfffff™f™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ì™ÌÌÌÌÌ™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww™™Ì™™Ì™™Ìfffff™f™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ì™ÌÌÌÌÌ™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌwwwˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ»»»ÝÝÝ™™Ì™™Ì™™Ìfffff™f™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ì™ÌÌÌÌÌ™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌwwwˆˆˆªªªªªªªªªªªªªªªªªª»»»ÝÝÝ™™Ì™™Ì™™Ìfffff™f™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ì™ÌÌÌÌÌ™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌwwwˆˆˆªªªªªªªªªªªªªªª»»»ÝÝÝ™™Ì™™Ì™™Ìfffff™f™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ì™ÌÌÌÌÌ™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌwwwˆˆˆªªªªªªªªªªªª»»»ÝÝÝ™™Ì™™Ì™™Ìfffff™f™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ì™ÌÌÌÌÌ™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌwwwˆˆˆªªªªªªªªª»»»ÝÝÝ™™Ì™™Ì™™Ìfffff™f™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ì™ÌÌÌÌÌ™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌwwwˆˆˆªªªªªª»»»ÝÝÝ™™Ì™™Ì™™Ìfffff™f™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ì™ÌÌÌÌÌ™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌwwwˆˆˆªªª»»»ÝÝÝ™™Ì™™Ì™™Ìfffff™f™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ì™ÌÌÌÌÌ™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌwwwˆˆˆ»»»ÝÝÝ™™Ì™™Ì™™Ìfffff™f™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ì™ÌÌÌÌÌ™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌwww»»»ÝÝÝ™™Ì™™Ì™™Ìfffff™f™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ìf™Ì™ÌÌÌÌÌ™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌwwwÝÝÝ™™Ì™™Ì™™Ìfffff™™ÌÌ™ÌÌ™ÌÌ™ÌÌ™ÌÌ™ÌÌ™ÌÌ™ÌÌ™ÌÌ™ÌÌ™ÌÌ™ÌÌ™ÌÌ™ÌÌ™ÌÌ™ÌÌ™ÌÌ™ÌÌ™ÌÌ™ÌÌÌÌÌ™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌwww™™Ì™™Ì™™Ì™™ÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌ™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌ™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-ÌÌÌ™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ì™™Ìfff‘œ´ˆžlu‡ZaqHNZ6:C$'-fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffwww‘œ´ˆžlu‡ZaqHNZ6:C$'-
diff --git a/bubbob/tmp/pat17.ppm b/bubbob/tmp/pat17.ppm
new file mode 100644
index 0000000..dcc159e
--- /dev/null
+++ b/bubbob/tmp/pat17.ppm
Binary files differ
diff --git a/bubbob/tmp/pat18.ppm b/bubbob/tmp/pat18.ppm
new file mode 100644
index 0000000..f36ff3d
--- /dev/null
+++ b/bubbob/tmp/pat18.ppm
Binary files differ
diff --git a/bubbob/tmp/pat19.ppm b/bubbob/tmp/pat19.ppm
new file mode 100644
index 0000000..e44e5c7
--- /dev/null
+++ b/bubbob/tmp/pat19.ppm
Binary files differ
diff --git a/bubbob/tmp/pat20.ppm b/bubbob/tmp/pat20.ppm
new file mode 100644
index 0000000..eba7c3b
--- /dev/null
+++ b/bubbob/tmp/pat20.ppm
Binary files differ
diff --git a/common/.cvsignore b/common/.cvsignore
new file mode 100644
index 0000000..539da74
--- /dev/null
+++ b/common/.cvsignore
@@ -0,0 +1 @@
+*.py[co]
diff --git a/common/__init__.py b/common/__init__.py
new file mode 100644
index 0000000..ea30561
--- /dev/null
+++ b/common/__init__.py
@@ -0,0 +1 @@
+#empty
diff --git a/common/gamesrv.py b/common/gamesrv.py
new file mode 100644
index 0000000..bb27d66
--- /dev/null
+++ b/common/gamesrv.py
@@ -0,0 +1,1378 @@
+from __future__ import generators
+from socket import *
+from select import select
+from struct import pack, unpack
+import zlib, os, random, struct, md5, sys
+from time import time, ctime
+from msgstruct import *
+from errno import EWOULDBLOCK
+
+
+SERVER_TIMEOUT = 7200 # 2 hours without any connection or port activity
+
+
+def protofilepath(filename):
+ dirpath = filename
+ path = []
+ while dirpath:
+ dirpath, component = os.path.split(dirpath)
+ assert component, "invalid file path %r" % (filename,)
+ path.insert(0, component)
+ path.insert(0, game.FnBasePath)
+ return '/'.join(path)
+
+
+class Icon:
+ count = 0
+
+ def __init__(self, bitmap, code, x,y,w,h, alpha=255):
+ self.w = w
+ self.h = h
+ self.origin = (bitmap, x, y)
+ self.code = code
+ if alpha == 255:
+ self.msgdef = message(MSG_DEF_ICON, bitmap.code, code, x,y,w,h)
+ else:
+ self.msgdef = message(MSG_DEF_ICON, bitmap.code, code, x,y,w,h, alpha)
+ framemsgappend(self.msgdef)
+
+ def getimage(self):
+ import pixmap
+ bitmap, x, y = self.origin
+ image = pixmap.decodepixmap(bitmap.read())
+ return pixmap.cropimage(image, (x, y, self.w, self.h))
+
+ def getorigin(self):
+ bitmap, x, y = self.origin
+ return bitmap, (x, y, self.w, self.h)
+
+
+class DataChunk:
+
+ def __init__(self):
+ for c in clients:
+ if c.initialized == 2:
+ self.defall(c)
+ if recording and game:
+ self.defall(recording)
+
+ def read(self, slice=None):
+ f = open(self.filename, "rb")
+ data = f.read()
+ f.close()
+ if slice:
+ start, length = slice
+ data = data[start:start+length]
+ return data
+
+ def defall(self, client):
+ if client.proto == 1 or not self.filename:
+ # protocol 1
+ try:
+ msgdef = self.msgdef
+ except AttributeError:
+ data = zlib.compress(self.read())
+ msgdef = self.msgdef = self.getmsgdef(data)
+ else:
+ # protocol >= 2
+ try:
+ msgdef = self.sendmsgdef
+ except AttributeError:
+ fileid = len(filereaders)
+ filereaders[fileid] = self.read
+ data = self.read()
+ msgdef = self.sendmsgdef = (self.getmd5def(fileid, data) +
+ self.getmsgdef(fileid))
+ client.msgl.append(msgdef)
+
+ def getmd5def(self, fileid, data, offset=0):
+ checksum = md5.new(data).digest()
+ return message(MSG_MD5_FILE, fileid, protofilepath(self.filename),
+ offset, len(data), checksum)
+
+
+class Bitmap(DataChunk):
+
+ def __init__(self, code, filename, colorkey=None):
+ self.code = code
+ self.filename = filename
+ self.icons = {}
+ self.colorkey = colorkey
+ DataChunk.__init__(self)
+
+ def geticon(self, x,y,w,h, alpha=255):
+ rect = (x,y,w,h)
+ try:
+ return self.icons[rect]
+ except:
+ ico = Icon(self, Icon.count, x,y,w,h, alpha)
+ Icon.count += 1
+ self.icons[rect] = ico
+ return ico
+
+ def geticonlist(self, w, h, count):
+ return map(lambda i, fn=self.geticon, w=w, h=h: fn(i*w, 0, w, h), range(count))
+
+ def getmsgdef(self, data):
+ if self.colorkey is not None:
+ return message(MSG_DEF_BITMAP, self.code, data, self.colorkey)
+ else:
+ return message(MSG_DEF_BITMAP, self.code, data)
+
+ def defall(self, client):
+ DataChunk.defall(self, client)
+ for i in self.icons.values():
+ client.msgl.append(i.msgdef)
+
+
+class MemoryBitmap(Bitmap):
+
+ def __init__(self, code, data, colorkey=None):
+ self.data = data
+ Bitmap.__init__(self, code, None, colorkey)
+
+ def read(self, slice=None):
+ data = self.data
+ if slice:
+ start, length = slice
+ data = data[start:start+length]
+ return data
+
+
+class Sample(DataChunk):
+
+ def __init__(self, code, filename, freqfactor=1):
+ self.code = code
+ self.filename = filename
+ self.freqfactor = freqfactor
+ DataChunk.__init__(self)
+
+ def defall(self, client):
+ if client.has_sound > 0:
+ DataChunk.defall(self, client)
+
+ def getmsgdef(self, data):
+ return message(MSG_DEF_SAMPLE, self.code, data)
+
+ def read(self, slice=None):
+ f = open(self.filename, "rb")
+ data = f.read()
+ f.close()
+ if self.freqfactor != 1:
+ freq, = unpack("<i", data[24:28])
+ freq = int(freq * self.freqfactor)
+ data = data[:24] + pack("<i", freq) + data[28:]
+ if slice:
+ start, length = slice
+ data = data[start:start+length]
+ return data
+
+ def getmd5def(self, fileid, data):
+ if self.freqfactor == 1:
+ return DataChunk.getmd5def(self, fileid, data)
+ else:
+ datahead = data[:28]
+ datatail = data[28:]
+ return (message(MSG_PATCH_FILE, fileid, 0, datahead) +
+ DataChunk.getmd5def(self, fileid, datatail, offset=28))
+
+ def play(self, lvolume=1.0, rvolume=None, pad=0.5, singleclient=None):
+ if rvolume is None:
+ rvolume = lvolume
+ lvolume *= 2.0*(1.0-pad)
+ rvolume *= 2.0*pad
+ if lvolume < 0.0:
+ lvolume = 0.0
+ elif lvolume > 1.0:
+ lvolume = 1.0
+ if rvolume < 0.0:
+ rvolume = 0.0
+ elif rvolume > 1.0:
+ rvolume = 1.0
+ message = pack("!hBBh", self.code, int(lvolume*255.0),
+ int(rvolume*255.0), -1)
+ if singleclient is None:
+ clist = clients[:]
+ else:
+ clist = [singleclient]
+ for c in clist:
+ if c.has_sound:
+ c.sounds.setdefault(message, 4)
+
+
+class Music(DataChunk):
+
+ def __init__(self, filename, filerate=44100):
+ self.filename = filename
+ self.filerate = filerate
+ self.f = open(filename, 'rb')
+ self.f.seek(0, 2)
+ filesize = self.f.tell()
+ self.endpos = max(self.filerate, filesize - self.filerate)
+ self.fileid = len(filereaders)
+ filereaders[self.fileid] = self.read
+ self.md5msgs = {}
+ DataChunk.__init__(self)
+
+ def read(self, (start, length)):
+ self.f.seek(start)
+ return self.f.read(length)
+
+ def msgblock(self, position, limited=1):
+ blocksize = self.filerate
+ if limited and position+blocksize > self.endpos:
+ blocksize = self.endpos-position
+ if blocksize <= 0:
+ return ''
+ #self.f.seek(position)
+ #return message(MSG_DEF_MUSIC, self.code, position, self.f.read(blocksize))
+ try:
+ msg = self.md5msgs[position]
+ except KeyError:
+ data = self.read((position, blocksize))
+ checksum = md5.new(data).digest()
+ msg = message(MSG_MD5_FILE, self.fileid, protofilepath(self.filename),
+ position, blocksize, checksum)
+ self.md5msgs[position] = msg
+ return msg
+
+ def clientsend(self, clientpos):
+ msg = self.msgblock(clientpos)
+ #print 'clientsend:', self.code, len(msg), clientpos
+ if msg:
+ return [msg], clientpos + self.filerate
+ else:
+ return [], None
+
+ def initialsend(self, c):
+ return [self.msgblock(0), self.msgblock(self.endpos, 0)], self.filerate
+
+ def defall(self, client):
+ pass
+
+
+def clearsprites():
+ sprites_by_n.clear()
+ sprites[:] = ['']
+
+def compactsprites(insert_new=None, insert_before=None):
+ global sprites, sprites_by_n
+ if insert_before is not None:
+ if insert_new.alive:
+ insert_before = insert_before.alive
+ else:
+ insert_before = None
+ newsprites = ['']
+ newd = {}
+ l = sprites_by_n.items()
+ l.sort()
+ for n, s in l:
+ if n == insert_before:
+ prevn = insert_new.alive
+ newn = insert_new.alive = len(newsprites)
+ newsprites.append(sprites[prevn])
+ newd[newn] = insert_new
+ l.remove((prevn, insert_new))
+ newn = s.alive = len(newsprites)
+ newsprites.append(sprites[n])
+ newd[newn] = s
+ sprites = newsprites
+ sprites_by_n = newd
+
+
+class Sprite:
+
+## try:
+## import psyco.classes
+## except ImportError:
+## pass
+## else:
+## __slots__ = ['x', 'y', 'ico', 'alive']
+## __metaclass__ = psyco.classes.psymetaclass
+
+ def __init__(self, ico, x,y):
+ self.x = x
+ self.y = y
+ self.ico = ico
+ self.alive = len(sprites)
+ if (-ico.w < x < game.width and
+ -ico.h < y < game.height):
+ sprites.append(pack("!hhh", x, y, ico.code))
+ else:
+ sprites.append('') # starts off-screen
+ sprites_by_n[self.alive] = self
+
+ def move(self, x,y, ico=None):
+ self.x = x
+ self.y = y
+ if ico is not None:
+ self.ico = ico
+ sprites[self.alive] = pack("!hhh", x, y, self.ico.code)
+
+ def setdisplaypos(self, x, y):
+ # special use only (self.x,y are not updated)
+ s = sprites[self.alive]
+ if len(s) == 6:
+ sprites[self.alive] = pack("!hh", x, y) + s[4:]
+
+ def setdisplayicon(self, ico):
+ # special use only (self.ico is not updated)
+ s = sprites[self.alive]
+ if len(s) == 6:
+ sprites[self.alive] = s[:4] + pack("!h", ico.code)
+
+ #sizeof_displaypos = struct.calcsize("!hh")
+ def getdisplaypos(self):
+ # special use only (normally, read self.x,y,ico directly)
+ s = sprites[self.alive]
+ if self.alive and len(s) == 6:
+ return unpack("!hh", s[:4])
+ else:
+ return None, None
+
+ def step(self, dx,dy):
+ x = self.x = self.x + dx
+ y = self.y = self.y + dy
+ sprites[self.alive] = pack("!hhh", x, y, self.ico.code)
+
+ def seticon(self, ico):
+ self.ico = ico
+ sprites[self.alive] = pack("!hhh", self.x, self.y, ico.code)
+
+ def hide(self):
+ sprites[self.alive] = ''
+
+ def kill(self):
+ if self.alive:
+ del sprites_by_n[self.alive]
+ sprites[self.alive] = ''
+ self.alive = 0
+
+ def prefix(self, n, m=0):
+ pass #sprites[self.alive] = pack("!hhh", n, m, 32767) + sprites[self.alive]
+
+ def to_front(self):
+ if self.alive and self.alive < len(sprites)-1:
+ self._force_to_front()
+
+ def _force_to_front(self):
+ info = sprites[self.alive]
+ sprites[self.alive] = ''
+ del sprites_by_n[self.alive]
+ self.alive = len(sprites)
+ sprites_by_n[self.alive] = self
+ sprites.append(info)
+
+ def to_back(self, limit=None):
+ assert self is not limit
+ if limit:
+ n1 = limit.alive + 1
+ else:
+ n1 = 1
+ if self.alive > n1:
+ if n1 in sprites_by_n:
+ keys = sprites_by_n.keys()
+ keys.remove(self.alive)
+ keys.sort()
+ keys = keys[keys.index(n1):]
+ reinsert = [sprites_by_n[n] for n in keys]
+ for s1 in reinsert:
+ s1._force_to_front()
+ assert n1 not in sprites_by_n
+ info = sprites[self.alive]
+ sprites[self.alive] = ''
+ del sprites_by_n[self.alive]
+ self.alive = n1
+ sprites_by_n[n1] = self
+ sprites[n1] = info
+
+ def __repr__(self):
+ if self.alive:
+ return "<sprite %d at %d,%d>" % (self.alive, self.x, self.y)
+ else:
+ return "<killed sprite>"
+
+
+class Player:
+ standardplayericon = None
+
+ def playerjoin(self):
+ pass
+
+ def playerleaves(self):
+ pass
+
+ def _playerleaves(self):
+ if self.isplaying():
+ self._client.killplayer(self)
+ del self._client
+ self.playerleaves()
+
+ def isplaying(self):
+ return hasattr(self, "_client")
+
+
+class Client:
+ SEND_BOUND_PER_FRAME = 0x6000 # bytes
+ KEEP_ALIVE = 2.2 # seconds
+
+ def __init__(self, socket, addr):
+ socket.setblocking(0)
+ self.socket = socket
+ self.addr = addr
+ self.udpsocket = None
+ self.udpsockcounter = 0
+ self.initialdata = MSG_WELCOME
+ self.initialized = 0
+ self.msgl = [message(MSG_PING)]
+ self.buf = ""
+ self.players = { }
+ self.sounds = None
+ self.has_sound = 0
+ self.has_music = 0
+ self.musicpos = { }
+ self.proto = 1
+ self.dyncompress = None
+ addsocket('CLIENT', self.socket, self.input_handler)
+ clients.append(self)
+ self.log('connected')
+ self.send_buffer(self.initialdata)
+
+ def opengame(self, game):
+ if self.initialized == 0:
+ self.initialdata += game.FnDesc + '\n'
+ self.initialized = 1
+ if self.initialized == 1:
+ if game.broadcast_port:
+ self.initialdata += message(MSG_BROADCAST_PORT, game.broadcast_port)
+ game.trigger_broadcast()
+ self.initialdata += game.deffieldmsg()
+ else:
+ self.msgl.append(game.deffieldmsg())
+ self.activity = self.last_ping = time()
+ self.force_ping_delay = 0.6
+ for c in clients:
+ for id in c.players.keys():
+ self.msgl.append(message(MSG_PLAYER_JOIN, id, c is self))
+
+ def emit(self, udpdata, broadcast_extras):
+ if self.initialdata:
+ self.send_buffer(self.initialdata)
+ elif self.initialized == 2:
+ buffer = ''.join(self.msgl)
+ if buffer:
+ self.send_buffer(buffer)
+ if self.udpsocket is not None:
+ if self.sounds:
+ if broadcast_extras is None or self not in broadcast_clients:
+ udpdata = ''.join(self.sounds.keys() + [udpdata])
+ else:
+ broadcast_extras.update(self.sounds)
+ for key, value in self.sounds.items():
+ if value:
+ self.sounds[key] = value-1
+ else:
+ del self.sounds[key]
+ if broadcast_extras is None or self not in broadcast_clients:
+ if self.dyncompress is not None:
+ udpdatas = self.dynamic_compress(udpdata)
+ else:
+ udpdatas = [udpdata]
+ for udpdata in udpdatas:
+ try:
+ self.udpsockcounter += self.udpsocket.send(udpdata)
+ except error, e:
+ print >> sys.stderr, 'ignored:', str(e)
+ pass # ignore UDP send errors (buffer full, etc.)
+ if self.has_music > 1 and NOW >= self.musicstreamer:
+ self.musicstreamer += 0.99
+ self.sendmusicdata()
+ if not self.msgl:
+ if abs(NOW - self.activity) <= self.KEEP_ALIVE:
+ if abs(NOW - self.last_ping) <= self.force_ping_delay:
+ return
+ if self.udpsockcounter < 1024:
+ return
+ self.force_ping_delay += 0.2
+ self.msgl.append(message(MSG_PING, self.udpsockcounter>>10))
+ self.last_ping = NOW
+
+ def setup_dyncompress(self):
+ def dyncompress():
+ # See comments in pclient.Playfield.dynamic_decompress().
+ threads = []
+ for t in range(3):
+ co = zlib.compressobj(6)
+ threads.append((chr(0x88 + t) + chr(t), co))
+ frame = 0
+ globalsync = 0
+
+ while 1:
+ # write three normal packets, one on each thread
+ for t in range(3):
+ head, co = threads.pop(0)
+ yield head + chr(frame), co
+ threads.append((chr(ord(head[0]) & 0x87) + chr(frame), co))
+ yield None, None
+ frame = (frame + 1) & 0xFF
+
+ # sync frame, write two packets (on two threads)
+ # and restart compression at the current frame for these threads
+ head, co = threads.pop(0)
+ yield head + chr(frame), co
+ co1 = zlib.compressobj(6)
+ co2 = zlib.compressobj(6)
+ globalsync += 1
+ if globalsync == 4:
+ # next on this thread will be a global sync packet
+ nextframe = (frame + 2) & 0xFF
+ globalsync = 0
+ else:
+ # next of this thread will be a local sync packet
+ yield None, co1
+ nextframe = frame
+ threads.append((chr(ord(head[0]) | 8) + chr(nextframe), co1))
+
+ # 2nd packet of the current frame
+ head, co = threads.pop(0)
+ yield head + chr(frame), co
+ yield None, co2
+ threads.append((chr(ord(head[0]) | 8) + chr(frame), co2))
+
+ yield None, None
+ frame = (frame + 1) & 0xFF
+
+ self.dyncompress = dyncompress()
+
+ def dynamic_compress(self, framedata):
+ result = []
+ for head, co in self.dyncompress:
+ if not co:
+ return result
+ data = [head, co.compress(framedata), co.flush(zlib.Z_SYNC_FLUSH)]
+ if head:
+ result.append(''.join(data))
+
+ def send_can_mix(self):
+ return not self.msgl and self.socket is not None
+
+ def send_buffer(self, buffer):
+ try:
+ count = self.socket.send(buffer[:self.SEND_BOUND_PER_FRAME])
+ except error, e:
+ if e.args[0] != EWOULDBLOCK:
+ self.msgl = []
+ self.initialdata = ""
+ self.disconnect(e, 'emit')
+ return
+ else:
+ #g = open('log', 'ab'); g.write(buffer[:count]); g.close()
+ buffer = buffer[count:]
+ self.activity = NOW
+ if self.initialdata:
+ self.initialdata = buffer
+ elif buffer:
+ self.msgl = [buffer]
+ else:
+ self.msgl = []
+
+ def receive(self, data):
+ #print "receive:", `data`
+ try:
+ data = self.buf + data
+ while data:
+ values, data = decodemessage(data)
+ if not values:
+ break # incomplete message
+ fn = self.MESSAGES.get(values[0])
+ if fn:
+ fn(self, *values[1:])
+ else:
+ print "unknown message from", self.addr, ":", values
+ self.buf = data
+ except struct.error:
+ import traceback
+ traceback.print_exc()
+ self.socket.send('\n\n<h1>Protocol Error</h1>\n')
+ hs = findsocket('HTTP')
+ if hs is not None:
+ url = 'http://%s:%s' % (HOSTNAME, displaysockport(hs))
+ self.socket.send('''
+If you meant to point your web browser to this server,
+then use the following address:
+
+<a href="%s">%s</a>
+''' % (url, url))
+ self.disconnect('protocol error', 'receive')
+
+ def input_handler(self):
+ try:
+ data = self.socket.recv(2048)
+ except error, e:
+ self.disconnect(e, "socket.recv")
+ else:
+ if data:
+ self.activity = NOW
+ self.receive(data)
+ elif not hasattr(self.socket, 'RECV_CAN_RETURN_EMPTY'):
+ # safecheck that this means disconnected
+ iwtd, owtd, ewtd = select([self.socket], [], [], 0.0)
+ if self.socket in iwtd:
+ self.disconnect('end of data', 'socket.recv')
+
+ def disconnect(self, err=None, infn=None):
+ removesocket('CLIENT', self.socket)
+ if err:
+ extra = ": " + str(err)
+ else:
+ extra = ""
+ if infn:
+ extra += " in " + infn
+ print 'Disconnected by', self.addr, extra
+ self.log('disconnected' + extra)
+ for p in self.players.values():
+ p._playerleaves()
+ try:
+ del broadcast_clients[self]
+ except KeyError:
+ pass
+ clients.remove(self)
+ try:
+ self.socket.close()
+ except:
+ pass
+ self.socket = None
+ if not clients and game is not None:
+ game.FnDisconnected()
+
+ def killplayer(self, player):
+ for id, p in self.players.items():
+ if p is player:
+ framemsgappend(message(MSG_PLAYER_KILL, id))
+ del self.players[id]
+ if game:
+ game.updateplayers()
+
+ def joinplayer(self, id, *rest):
+ if self.players.has_key(id):
+ print "Note: player %s is already playing" % (self.addr+(id,),)
+ return
+ if game is None:
+ return # refusing new player before the game starts
+ p = game.FnPlayers()[id]
+ if p is None:
+ print "Too many players. New player %s refused." % (self.addr+(id,),)
+ self.msgl.append(message(MSG_PLAYER_KILL, id))
+ elif p.isplaying():
+ print "Note: player %s is already played by another client" % (self.addr+(id,),)
+ else:
+ print "New player %s" % (self.addr+(id,),)
+ p._client = self
+ p.playerjoin()
+ p.setplayername('')
+ self.players[id] = p
+ game.updateplayers()
+ for c in clients:
+ c.msgl.append(message(MSG_PLAYER_JOIN, id, c is self))
+
+ def remove_player(self, id, *rest):
+ try:
+ p = self.players[id]
+ except KeyError:
+ print "Note: player %s is not playing" % (self.addr+(id,),)
+ else:
+ p._playerleaves()
+
+ def set_player_name(self, id, name, *rest):
+ p = game.FnPlayers()[id]
+ p.setplayername(name)
+
+ def set_udp_port(self, port, addr=None, *rest):
+ if port == MSG_BROADCAST_PORT:
+ self.log('set_udp_port: broadcast')
+ broadcast_clients[self] = 1
+ #print "++++ Broadcasting ++++ to", self.addr
+ else:
+ try:
+ del broadcast_clients[self]
+ except KeyError:
+ pass
+ if port == MSG_INLINE_FRAME or port == 0:
+ # client requests data in-line on the TCP stream
+ self.dyncompress = None
+ import udpovertcp
+ self.udpsocket = udpovertcp.SocketMarshaller(self.socket, self)
+ s = self.udpsocket.tcpsock
+ self.log('set_udp_port: udp-over-tcp')
+ else:
+ try:
+ if hasattr(self.socket, 'udp_over_udp_mixer'):
+ # for SocketOverUdp
+ self.udpsocket = self.socket.udp_over_udp_mixer()
+ else:
+ self.udpsocket = socket(AF_INET, SOCK_DGRAM)
+ self.udpsocket.setblocking(0)
+ addr = addr or self.addr[0]
+ self.udpsocket.connect((addr, port))
+ except error, e:
+ print >> sys.stderr, "Cannot set UDP socket to", addr, str(e)
+ self.udpsocket = None
+ self.udpsockcounter = sys.maxint
+ else:
+ if self.proto >= 3:
+ self.setup_dyncompress()
+ s = self.udpsocket
+ self.log('set_udp_port: %s:%d' % (addr, port))
+ if s:
+ try:
+ s.setsockopt(SOL_IP, IP_TOS, 0x10) # IPTOS_LOWDELAY
+ except error, e:
+ print >> sys.stderr, "Cannot set IPTOS_LOWDELAY:", str(e)
+
+ def enable_sound(self, sound_mode=1, *rest):
+ if sound_mode != self.has_sound:
+ self.sounds = {}
+ self.has_sound = sound_mode
+ if self.has_sound > 0:
+ for snd in samples.values():
+ snd.defall(self)
+ #self.log('enable_sound %s' % sound_mode)
+
+ def enable_music(self, mode, *rest):
+ if mode != self.has_music:
+ self.has_music = mode
+ self.startmusic()
+ #self.log('enable_music')
+
+ def startmusic(self):
+ if self.has_music:
+ self.musicstreamer = time()
+ for cde in currentmusics[1:]:
+ if cde not in self.musicpos:
+ msgl, self.musicpos[cde] = music_by_id[cde].initialsend(self)
+ self.msgl += msgl
+ if self.has_music > 1:
+ self.sendmusicdata()
+ self.msgl.append(message(MSG_PLAY_MUSIC, *currentmusics))
+
+ def sendmusicdata(self):
+ for cde in currentmusics[1:]:
+ if self.musicpos[cde] is not None:
+ msgl, self.musicpos[cde] = music_by_id[cde].clientsend(self.musicpos[cde])
+ self.msgl += msgl
+ return
+
+ def ping(self, *rest):
+ if self.initialized < 2:
+ # send all current bitmap data
+ self.initialized = 2
+ for b in bitmaps.values():
+ b.defall(self)
+ self.finishinit(game)
+ for id, p in game.FnPlayers().items():
+ if p.standardplayericon is not None:
+ self.msgl.append(message(MSG_PLAYER_ICON, id, p.standardplayericon.code))
+ self.msgl.append(message(MSG_PONG, *rest))
+
+ def finishinit(self, game):
+ pass
+
+ def pong(self, *rest):
+ pass
+
+ def log(self, message):
+ print self.addr, message
+
+ def protocol_version(self, version, *rest):
+ self.proto = version
+
+ def md5_data_request(self, fileid, position, size, *rest):
+ data = filereaders[fileid]((position, size))
+ data = zlib.compress(data)
+ self.msgl.append(message(MSG_ZPATCH_FILE, fileid, position, data))
+
+## def def_file(self, filename, md5sum):
+## fnp = []
+## while filename:
+## filename, tail = os.path.split(filename)
+## fnp.insert(0, tail)
+## if fnp[:len(FnBasePath)] == FnBasePath:
+## filename = os.path.join(*fnp[len(FnBasePath):])
+## self.known_files[filename] = md5sum
+
+ MESSAGES = {
+ CMSG_PROTO_VERSION: protocol_version,
+ CMSG_ADD_PLAYER : joinplayer,
+ CMSG_REMOVE_PLAYER: remove_player,
+ CMSG_UDP_PORT : set_udp_port,
+ CMSG_ENABLE_SOUND : enable_sound,
+ CMSG_ENABLE_MUSIC : enable_music,
+ CMSG_PING : ping,
+ CMSG_PONG : pong,
+ CMSG_DATA_REQUEST : md5_data_request,
+ CMSG_PLAYER_NAME : set_player_name,
+## CMSG_DEF_FILE : def_file,
+ }
+
+
+class SimpleClient(Client):
+
+ def finishinit(self, game):
+ num = 0
+ for keyname, icolist, fn in game.FnKeys:
+ self.msgl.append(message(MSG_DEF_KEY, keyname, num,
+ *[ico.code for ico in icolist]))
+ num += 1
+
+ def cmsg_key(self, pid, keynum):
+ if game is not None:
+ try:
+ player = self.players[pid]
+ fn = game.FnKeys[keynum][2]
+ except (KeyError, IndexError):
+ game.FnUnknown()
+ else:
+ getattr(player, fn) ()
+
+ MESSAGES = Client.MESSAGES.copy()
+ MESSAGES.update({
+ CMSG_KEY: cmsg_key,
+ })
+
+
+MAX_CLIENTS = 32
+
+clients = []
+FnClient = SimpleClient
+broadcast_clients = {}
+filereaders = {}
+bitmaps = {}
+samples = {}
+music_by_id = {}
+currentmusics = [0]
+sprites = ['']
+sprites_by_n = {}
+recording = None
+game = None
+serversockets = {}
+socketsbyrole = {}
+socketports = {}
+
+def framemsgappend(msg):
+ for c in clients:
+ c.msgl.append(msg)
+ if recording:
+ recording.write(msg)
+
+##def sndframemsgappend(msg):
+## for c in clients:
+## if c.has_sound:
+## c.msgl.append(msg)
+
+def set_udp_port(port):
+ hostchooser.UDP_PORT = port
+
+def has_loop_music():
+ return currentmusics[0] < len(currentmusics)-1
+
+def finalsegment(music1, music2):
+ intro1 = music1[1:1+music1[0]]
+ intro2 = music2[1:1+music2[0]]
+ loop1 = music1[1+music1[0]:]
+ loop2 = music2[1+music2[0]:]
+ return loop1 == loop2 and intro1 == intro2[len(intro2)-len(intro1):]
+
+def set_musics(musics_intro, musics_loop, reset=1):
+ mlist = []
+ loop_from = len(musics_intro)
+ mlist.append(loop_from)
+ for m in musics_intro + musics_loop:
+ mlist.append(m.fileid)
+ reset = reset or not finalsegment(mlist, currentmusics)
+ currentmusics[:] = mlist
+ if reset:
+ for c in clients:
+ c.startmusic()
+
+def fadeout(time=1.0):
+ msg = message(MSG_FADEOUT, int(time*1000))
+ for c in clients:
+ if c.has_music > 1:
+ c.msgl.append(msg)
+ currentmusics[:] = [0]
+
+
+def getbitmap(filename, colorkey=None):
+ try:
+ return bitmaps[filename]
+ except:
+ bmp = Bitmap(len(bitmaps), filename, colorkey)
+ bitmaps[filename] = bmp
+ return bmp
+
+def getsample(filename, freqfactor=1):
+ try:
+ return samples[filename, freqfactor]
+ except:
+ snd = Sample(len(samples), filename, freqfactor)
+ samples[filename, freqfactor] = snd
+ return snd
+
+def getmusic(filename, filerate=44100):
+ try:
+ return samples[filename]
+ except:
+ mus = Music(filename, filerate)
+ samples[filename] = mus
+ music_by_id[mus.fileid] = mus
+ return mus
+
+def newbitmap(data, colorkey=None):
+ bmp = MemoryBitmap(len(bitmaps), data, colorkey)
+ bitmaps[bmp] = bmp
+ return bmp
+
+
+def addsocket(role, socket, handler=None, port=None):
+ if port is None:
+ host, port = socket.getsockname()
+ if handler is not None:
+ serversockets[socket] = handler
+ socketsbyrole.setdefault(role, []).append(socket)
+ socketports[socket] = port
+
+def findsockets(role):
+ return socketsbyrole.get(role, [])
+
+def findsocket(role):
+ l = findsockets(role)
+ if l:
+ return l[-1]
+ else:
+ return None
+
+def removesocket(role, socket=None):
+ if socket is None:
+ for socket in socketsbyrole.get(role, [])[:]:
+ removesocket(role, socket)
+ return
+ try:
+ del serversockets[socket]
+ except KeyError:
+ pass
+ try:
+ socketsbyrole.get(role, []).remove(socket)
+ except ValueError:
+ pass
+ try:
+ del socketports[socket]
+ except KeyError:
+ pass
+
+def opentcpsocket(port=None):
+ port = port or PORTS.get('LISTEN', INADDR_ANY)
+ s = findsocket('LISTEN')
+ if s is None:
+ s = socket(AF_INET, SOCK_STREAM)
+ try:
+ s.bind(('', port))
+ s.listen(1)
+ except error:
+ if port == INADDR_ANY:
+ for i in range(10):
+ port = random.choice(xrange(8000, 12000))
+ try:
+ s.bind(('', port))
+ s.listen(1)
+ except error:
+ pass
+ else:
+ break
+ else:
+ raise error, "server cannot find a free TCP socket port"
+ else:
+ raise
+
+ def tcpsocket_handler(s=s):
+ conn, addr = s.accept()
+ game.newclient(conn, addr)
+
+ addsocket('LISTEN', s, tcpsocket_handler)
+ return s
+
+def openpingsocket(only_port=None):
+ only_port = only_port or PORTS.get('PING', None)
+ s = findsocket('PING')
+ if s is None:
+ import hostchooser
+ s = hostchooser.serverside_ping(only_port)
+ if s is None:
+ return None
+ def pingsocket_handler(s=s):
+ global game
+ import hostchooser
+ if game is not None:
+ args = game.FnDesc, ('', game.address[1]), game.FnExtraDesc()
+ else:
+ ts = findsocket('LISTEN')
+ if ts:
+ address = '', displaysockport(ts)
+ else:
+ address = '', ''
+ args = 'Not playing', address, ''
+ hs = findsocket('HTTP')
+ args = args + (displaysockport(hs),)
+ hostchooser.answer_ping(s, *args)
+ addsocket('PING', s, pingsocket_handler)
+ return s
+
+def openhttpsocket(ServerClass=None, HandlerClass=None,
+ port=None):
+ port = port or PORTS.get('HTTP', None)
+ s = findsocket('HTTP')
+ if s is None:
+ if ServerClass is None:
+ from BaseHTTPServer import HTTPServer as ServerClass
+ if HandlerClass is None:
+ import javaserver
+ from httpserver import MiniHandler as HandlerClass
+ server_address = ('', port or 8000)
+ try:
+ httpd = ServerClass(server_address, HandlerClass)
+ except error:
+ if port is None:
+ server_address = ('', INADDR_ANY)
+ try:
+ httpd = ServerClass(server_address, HandlerClass)
+ except error, e:
+ print >> sys.stderr, "cannot start HTTP server", str(e)
+ return None
+ else:
+ raise
+ s = httpd.socket
+ addsocket('HTTP', s, httpd.handle_request)
+ return s
+
+BROADCAST_PORT_RANGE = xrange(18000, 19000)
+#BROADCAST_MESSAGE comes from msgstruct
+BROADCAST_DELAY = 0.6180
+BROADCAST_DELAY_INCR = 2.7183
+
+def openbroadcastsocket(broadcastport=None):
+ s = findsocket('BROADCAST')
+ if s is None:
+ try:
+ s = socket(AF_INET, SOCK_DGRAM)
+ s.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
+ except error, e:
+ print >> sys.stderr, "Cannot broadcast", str(e)
+ return None
+ port = broadcastport or random.choice(BROADCAST_PORT_RANGE)
+ addsocket('BROADCAST', s, port=port)
+ return s
+
+def displaysockport(s):
+ return socketports.get(s, 'off')
+
+
+class Game:
+ width = 640
+ height = 480
+ backcolor = 0x000000
+
+ FnDesc = "NoName"
+ FnFrame = lambda self: 1.0
+ FnExcHandler=lambda self, k: 0
+ FnServerInfo=lambda self, s: None
+ FnPlayers = lambda self: {}
+ FnKeys = []
+ FnUnknown = lambda self: None
+ FnDisconnected = lambda self: None
+
+ def __init__(self):
+ global game
+ s = opentcpsocket()
+ self.address = HOSTNAME, socketports[s]
+ bs = self.broadcast_s = openbroadcastsocket()
+ self.broadcast_port = socketports.get(bs)
+ self.broadcast_next = None
+ self.nextframe = time()
+ clearsprites()
+ game = self
+ if recording:
+ for b in bitmaps.values():
+ b.defall(recording)
+ self.pendingclients = []
+
+ def openserver(self):
+ ps = openpingsocket()
+ print '%s server at %s:%d, Broadcast %d, UDP %d' % (
+ self.FnDesc, self.address[0], self.address[1],
+ displaysockport(self.broadcast_s), displaysockport(ps))
+
+ hs = openhttpsocket()
+ if hs:
+ print 'HTTP server: http://%s:%d' % (
+ self.address[0], displaysockport(hs))
+
+ try:
+ from localmsg import autonotify
+ except ImportError:
+ pass
+ else:
+ autonotify(self.FnDesc, *self.address)
+
+ if clients:
+ for c in clients:
+ c.opengame(self)
+ if recording:
+ recording.start()
+ recording.write(self.deffieldmsg())
+
+ def trigger_broadcast(self):
+ assert self.broadcast_s is not None
+ game.broadcast_delay = BROADCAST_DELAY
+ game.broadcast_next = time() + self.broadcast_delay
+
+ def deffieldmsg(self):
+ return message(MSG_DEF_PLAYFIELD,
+ self.width, self.height, self.backcolor,
+ self.FnDesc)
+
+ def socketerrors(self, ewtd):
+ for c in clients[:]:
+ if c.socket in ewtd:
+ del ewtd[c.socket]
+ c.disconnect("error", "select")
+
+ def mainstep(self):
+ global NOW
+ if self.pendingclients:
+ self.newclient(*self.pendingclients.pop())
+ NOW = time()
+ delay = self.nextframe - NOW
+ if delay<=0.0:
+ self.nextframe += self.FnFrame()
+ self.sendudpdata()
+ NOW = time()
+ delay = self.nextframe - NOW
+ if delay<0.0:
+ self.nextframe = NOW
+ delay = 0.0
+ if self.broadcast_next is not None and NOW >= self.broadcast_next:
+ if not clients:
+ self.broadcast_next = None
+ else:
+ try:
+ self.broadcast_s.sendto(BROADCAST_MESSAGE,
+ ('<broadcast>', self.broadcast_port))
+ #print "Broadcast ping"
+ except error:
+ pass # ignore failed broadcasts
+ self.broadcast_next = time() + self.broadcast_delay
+ self.broadcast_delay *= BROADCAST_DELAY_INCR
+ return delay
+
+ def sendudpdata(self):
+ sprites[0] = ''
+ udpdata = ''.join(sprites)
+ if len(broadcast_clients) >= 2:
+ broadcast_extras = {}
+ else:
+ broadcast_extras = None
+ for c in clients[:]:
+ c.emit(udpdata, broadcast_extras)
+ if recording:
+ recording.udpdata(udpdata)
+ if broadcast_extras is not None:
+ udpdata = ''.join(broadcast_extras.keys() + [udpdata])
+ try:
+ self.broadcast_s.sendto(udpdata,
+ ('<broadcast>', self.broadcast_port))
+ #print "Broadcast UDP data"
+ except error:
+ pass # ignore failed broadcasts
+
+ def FnExtraDesc(self):
+ players = 0
+ for c in clients:
+ players += len(c.players)
+ if players == 0:
+ return 'no player'
+ elif players == 1:
+ return 'one player'
+ else:
+ return '%d players' % players
+
+ def updateplayers(self):
+ pass
+
+ def updateboard(self):
+ pass
+
+ def newclient(self, conn, addr):
+ if len(clients)==MAX_CLIENTS:
+ print "Too many connections; refusing new connection from", addr
+ conn.close()
+ else:
+ try:
+ addrname = (gethostbyaddr(addr[0])[0],) + addr[1:]
+ except:
+ addrname = addr
+ print 'Connected by', addrname
+ try:
+ c = FnClient(conn, addrname)
+ except error, e:
+ print 'Connexion already lost!', e
+ else:
+ if game is not None:
+ c.opengame(game)
+
+ def newclient_threadsafe(self, conn, addr):
+ self.pendingclients.insert(0, (conn, addr))
+
+
+def recursiveloop(endtime, extra_sockets):
+ global game
+ timediff = 1
+ while timediff:
+ if game is not None:
+ delay = game.mainstep()
+ else:
+ delay = 5.0
+ iwtd = extra_sockets + serversockets.keys()
+ timediff = max(0.0, endtime - time())
+ iwtd, owtd, ewtd = select(iwtd, [], iwtd, min(delay, timediff))
+ if ewtd:
+ if game:
+ game.socketerrors(ewtd)
+ if ewtd:
+ print >> sys.stderr, "Unexpected socket error reported"
+ for s in iwtd:
+ if s in serversockets:
+ serversockets[s]() # call handler
+ elif s in extra_sockets:
+ return s
+ if not extra_sockets and timediff:
+ return 1
+ return None
+
+SERVER_SHUTDOWN = 0.0
+
+def mainloop():
+ global game, SERVER_SHUTDOWN
+ servertimeout = None
+ try:
+ while serversockets:
+ try:
+ if game is not None:
+ delay = game.mainstep()
+ else:
+ delay = SERVER_SHUTDOWN or 5.0
+ iwtd = serversockets.keys()
+ try:
+ iwtd, owtd, ewtd = select(iwtd, [], iwtd, delay)
+ except Exception, e:
+ from select import error as select_error
+ if not isinstance(e, select_error):
+ raise
+ iwtd, owtd, ewtd = [], [], []
+ if ewtd:
+ if game:
+ game.socketerrors(ewtd)
+ if ewtd:
+ print >> sys.stderr, "Unexpected socket error reported"
+ servertimeout = None
+ if iwtd:
+ for s in iwtd:
+ if s in serversockets:
+ serversockets[s]() # call handler
+ servertimeout = None
+ elif SERVER_SHUTDOWN and not ewtd and not owtd:
+ SERVER_SHUTDOWN -= delay
+ if SERVER_SHUTDOWN <= 0.001:
+ raise SystemExit, "Server shutdown requested."
+ elif clients or getattr(game, 'autoreset', 0):
+ servertimeout = None
+ elif servertimeout is None:
+ servertimeout = time() + SERVER_TIMEOUT
+ elif time() > servertimeout:
+ raise SystemExit, "No more server activity, timing out."
+ except KeyboardInterrupt:
+ if game is None or not game.FnExcHandler(1):
+ raise
+ except SystemExit:
+ raise
+ except:
+ if game is None or not game.FnExcHandler(0):
+ raise
+ finally:
+ removesocket('LISTEN')
+ removesocket('PING')
+ if clients:
+ print "Server crash -- waiting for clients to terminate..."
+ while clients:
+ iwtd = [c.socket for c in clients]
+ try:
+ iwtd, owtd, ewtd = select(iwtd, [], iwtd, 120.0)
+ except KeyboardInterrupt:
+ break
+ if not (iwtd or owtd or ewtd):
+ break # timeout - give up
+ for c in clients[:]:
+ if c.socket in ewtd:
+ c.disconnect("select reported an error")
+ elif c.socket in iwtd:
+ try:
+ data = c.socket.recv(2048)
+ except error, e:
+ c.disconnect(e)
+ else:
+ if not data and not hasattr(c.socket, 'RECV_CAN_RETURN_EMPTY'):
+ c.disconnect("end of data")
+ print "Server closed."
+
+def closeeverything():
+ global SERVER_SHUTDOWN
+ SERVER_SHUTDOWN = 2.5
+ if game is not None:
+ game.FnServerInfo("Server is stopping!")
+
+# ____________________________________________________________
+
+try:
+ from localmsg import recordfilename
+except ImportError:
+ pass
+else:
+
+ class RecordFile:
+ proto = 2
+ has_sound = 0
+
+ def __init__(self, filename, sampling=1/7.77):
+ self.filename = filename
+ self.f = None
+ self.sampling = sampling
+ self.msgl = []
+ self.write = self.msgl.append
+
+ def start(self):
+ if not self.f:
+ import gzip, atexit
+ self.f = gzip.open(self.filename, 'wb')
+ atexit.register(self.f.close)
+ self.recnext = time() + self.sampling
+
+ def udpdata(self, udpdata):
+ if self.f:
+ now = time()
+ if now >= self.recnext:
+ while now >= self.recnext:
+ self.recnext += self.sampling
+ self.write(message(MSG_RECORDED, udpdata))
+ self.f.write(''.join(self.msgl))
+ del self.msgl[:]
+
+ recording = RecordFile(recordfilename)
+ del recordfilename
diff --git a/common/hostchooser.py b/common/hostchooser.py
new file mode 100644
index 0000000..3243e06
--- /dev/null
+++ b/common/hostchooser.py
@@ -0,0 +1,192 @@
+from socket import *
+import time, select, sys
+from errno import ETIMEDOUT
+
+UDP_PORT = 8056
+PING_MESSAGE = "pclient-game-ping"
+PONG_MESSAGE = "server-game-pong"
+
+
+def serverside_ping(only_port=None):
+ port = only_port or UDP_PORT
+ s = socket(AF_INET, SOCK_DGRAM)
+ try:
+ s.bind(('', port))
+ s.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
+ return s
+ except error, e:
+ if only_port is None:
+ s = socket(AF_INET, SOCK_DGRAM)
+ s.bind(('', INADDR_ANY))
+ return s
+ else:
+ return None
+
+def answer_ping(s, descr, addr, extra='', httpport=''):
+ try:
+ data, source = s.recvfrom(100)
+ except error, e:
+ print >> sys.stderr, 'ping error:', str(e)
+ return
+ if data == PING_MESSAGE:
+ print >> sys.stderr, "ping by", source
+ answer = '%s:%s:%s:%s:%s:%s' % (PONG_MESSAGE, descr,
+ addr[0], addr[1], extra, httpport)
+ s.sendto(answer, source)
+ else:
+ print >> sys.stderr, \
+ "unexpected data on UDP port %d by" % UDP_PORT, source
+
+
+def pick(hostlist, delay=1):
+ s = socket(AF_INET, SOCK_DGRAM)
+ s.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
+ for host in hostlist:
+ print >> sys.stderr, "* Looking for a server on %s... " % host
+ try:
+ s.sendto(PING_MESSAGE, (host, UDP_PORT))
+ except error, e:
+ print >> sys.stderr, 'send:', str(e)
+ continue
+ while 1:
+ iwtd, owtd, ewtd = select.select([s], [], [], delay)
+ if not iwtd:
+ break
+ try:
+ data, answer_from = s.recvfrom(200)
+ except error, e:
+ if e.args[0] != ETIMEDOUT:
+ print >> sys.stderr, 'recv:', str(e)
+ continue
+ break
+ data = data.split(':')
+ if len(data) >= 4 and data[0] == PONG_MESSAGE:
+ hostname = data[2] or answer_from[0]
+ try:
+ port = int(data[3])
+ except ValueError:
+ pass
+ else:
+ result = (hostname, port)
+ print >> sys.stderr, "* Picking %r at" % data[1], result
+ return result
+ print >> sys.stderr, "got an unexpected answer", data, "from", answer_from
+ print >> sys.stderr, "no server found."
+ raise SystemExit
+
+def find_servers(hostlist=[('127.0.0.1', None), ('<broadcast>', None)],
+ tries=2, delay=0.5, verbose=1, port_needed=1):
+ import gamesrv
+ if verbose:
+ print >> sys.stderr, 'Looking for servers in the following list:'
+ for host, udpport in hostlist:
+ print >> sys.stderr, ' %s, UDP port %s' % (
+ host, udpport or ("%s (default)" % UDP_PORT))
+ events = {}
+ replies = []
+ s = socket(AF_INET, SOCK_DGRAM)
+ s.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
+ for trynum in range(tries):
+ for host, udpport in hostlist:
+ try:
+ ipaddr = host
+ if host != '<broadcast>':
+ try:
+ ipaddr = gethostbyname(host)
+ except error, e:
+ print >> sys.stderr, 'gethostbyname:', str(e)
+ s.sendto(PING_MESSAGE, (ipaddr, udpport or UDP_PORT))
+ hostsend, hostrecv = events.setdefault(ipaddr, ([], []))
+ hostsend.append(time.time())
+ except error, e:
+ print >> sys.stderr, 'send:', str(e)
+ continue
+ endtime = time.time() + delay
+ while gamesrv.recursiveloop(endtime, [s]):
+ try:
+ data, answer_from = s.recvfrom(200)
+ except error, e:
+ if e.args[0] != ETIMEDOUT:
+ print >> sys.stderr, 'recv:', str(e)
+ continue
+ break
+ try:
+ ipaddr = gethostbyname(answer_from[0])
+ except error:
+ ipaddr = answer_from[0]
+ else:
+ hostsend, hostrecv = events.setdefault(ipaddr, ([], []))
+ hostrecv.append(time.time())
+ data = data.split(':')
+ if len(data) >= 4 and data[0] == PONG_MESSAGE:
+ try:
+ port = int(data[3])
+ except ValueError:
+ if port_needed:
+ continue
+ port = ''
+ if data[2]:
+ hostname = data[2]
+ realhostname = [hostname]
+ else:
+ hostname = answer_from[0]
+ realhostname = lazy_gethostbyaddr(hostname)
+ server = ':'.join(data[1:2]+data[4:])
+ replies.append((hostname, realhostname, port, server, ipaddr))
+ else:
+ print >> sys.stderr, "got an unexpected answer from", answer_from
+ servers = {}
+ aliases = {}
+ timeout = time.time() + 2.0 # wait for gethostbyaddr() for 2 seconds
+ while replies:
+ i = 0
+ now = time.time()
+ while i < len(replies):
+ hostname, realhostname, port, server, ipaddr = replies[i]
+ if realhostname:
+ hostname = realhostname[0] # got an answer
+ elif now < timeout:
+ i += 1 # must wait some more time
+ continue
+ result = (hostname, port)
+ servers[result] = server
+ aliases[hostname] = ipaddr
+ del replies[i]
+ if replies:
+ time.sleep(0.08) # time for gethostbyaddr() to finish
+ if verbose:
+ print >> sys.stderr, "%d answer(s):" % len(servers), servers.keys()
+ for host, port in servers.keys():
+ ping = None
+ ipaddr = aliases[host]
+ if ipaddr in events:
+ hostsend, hostrecv = events[ipaddr]
+ if len(hostsend) == len(hostrecv) == tries:
+ ping = min([t2-t1 for t1, t2 in zip(hostsend, hostrecv)])
+ servers[host, port] = (servers[host, port], ping)
+ sys.setcheckinterval(4096)
+ return servers
+
+# ____________________________________________________________
+
+HOSTNAMECACHE = {}
+
+def _lazygetter(hostname, resultlst):
+ try:
+ try:
+ hostname = gethostbyaddr(hostname)[0]
+ if hostname == 'localhost':
+ from msgstruct import HOSTNAME as hostname
+ except error:
+ pass
+ finally:
+ resultlst.append(hostname)
+
+def lazy_gethostbyaddr(hostname):
+ try:
+ return HOSTNAMECACHE[hostname]
+ except KeyError:
+ resultlst = HOSTNAMECACHE[hostname] = []
+ import thread
+ thread.start_new_thread(_lazygetter, (hostname, resultlst))
+ return resultlst
diff --git a/common/httpserver.py b/common/httpserver.py
new file mode 100644
index 0000000..a39f854
--- /dev/null
+++ b/common/httpserver.py
@@ -0,0 +1,192 @@
+from __future__ import generators
+from __future__ import nested_scopes
+import BaseHTTPServer
+from SimpleHTTPServer import SimpleHTTPRequestHandler
+import urlparse, cgi, htmlentitydefs
+import sys, os, time
+from cStringIO import StringIO
+
+
+class Translator:
+ """For use with format strings.
+
+ 'formatstring % translator' will evaluate all %(xxx)s expressions
+ found in the format string in the given globals/locals.
+
+ Multiline expressions are assumed to be one or several complete
+ statements; they are executed and whatever they print is inserted back
+ into the format string."""
+
+ def __init__(self, globals, locals):
+ self.globals = globals
+ self.locals = locals
+
+ def __getitem__(self, expr):
+ if '\n' in expr:
+ if not expr.endswith('\n'):
+ expr += '\n'
+ prevstdout = sys.stdout
+ try:
+ sys.stdout = f = StringIO()
+ exec expr in self.globals, self.locals
+ finally:
+ sys.stdout = prevstdout
+ return f.getvalue()
+ else:
+ return eval(expr, self.globals, self.locals)
+
+class TranslatorIO:
+ "Lazy version of Translator."
+
+ def __init__(self, fmt, d):
+ self.gen = self.generate(fmt, d)
+
+ def read(self, ignored=None):
+ for text in self.gen:
+ if text:
+ return text
+ return ''
+
+ def close(self):
+ self.gen = ()
+
+ def generate(self, fmt, d):
+ t = Translator(d, d)
+ for data in fmt.split('\x0c'):
+ yield data % t
+
+
+# HTML quoting
+
+text_to_html = {}
+for key, value in htmlentitydefs.entitydefs.items():
+ text_to_html[value] = '&' + key + ';'
+
+def htmlquote(s):
+ return ''.join([text_to_html.get(c, c) for c in s])
+
+
+# HTTP Request Handler
+
+pathloaders = {}
+
+def canonicalpath(url):
+ if url.startswith('/'):
+ url = url[1:]
+ return url.lower()
+
+def register(url, loader):
+ pathloaders[canonicalpath(url)] = loader
+
+def is_registered(url):
+ return canonicalpath(url) in pathloaders
+
+def load(filename, mimetype=None, locals=None):
+ if mimetype and mimetype.startswith('text/'):
+ mode = 'r'
+ else:
+ mode = 'rb'
+ f = open(filename, mode)
+ if locals is not None:
+ data = f.read()
+ f.close()
+ #data = data.replace('%"', '%%"')
+ d = globals().copy()
+ d.update(locals)
+ f = TranslatorIO(data, d)
+ return f, mimetype
+
+def fileloader(filename, mimetype=None):
+ def loader(**options):
+ return load(filename, mimetype)
+ return loader
+
+class HTTPRequestError(Exception):
+ pass
+
+class MiniHandler(SimpleHTTPRequestHandler):
+
+ def send_head(self, query=''):
+ addr, host, path, query1, fragment = urlparse.urlsplit(self.path)
+ path = canonicalpath(path)
+ if path not in pathloaders:
+ if path + '/' in pathloaders:
+ return self.redirect(path + '/')
+ self.send_error(404)
+ return None
+ kwds = {}
+ for q in [query1, query]:
+ if q:
+ kwds.update(cgi.parse_qs(q))
+ loader = pathloaders[path]
+ try:
+ hdr = self.headers
+ hdr['remote host'] = self.client_address[0]
+ f, ctype = loader(headers=hdr, **kwds)
+ except IOError, e:
+ self.send_error(404, "I/O error: " + str(e))
+ return None
+ except HTTPRequestError, e:
+ self.send_error(500, str(e))
+ return None
+ except:
+ f = StringIO()
+ import traceback
+ traceback.print_exc(file=f)
+ data = htmlquote(f.getvalue())
+ data = data.replace('\n', '<br>\n')
+ self.send_error(500)
+ return StringIO('<hr><p>'+data+'</p>')
+ if ctype is None:
+ ctype = self.guess_type(self.translate_path(self.path))
+ elif f is None:
+ return self.redirect(ctype)
+
+ self.send_response(200)
+ self.send_header("Content-type", ctype)
+ self.end_headers()
+ return f
+
+ def redirect(self, url):
+ self.send_response(302)
+ self.send_header("Content-type", 'text/html')
+ self.send_header("Location", url)
+ self.end_headers()
+ return StringIO('''<html><head></head><body>
+Please <a href="%s">click here</a> to continue.
+</body></html>
+''' % url)
+
+ def do_POST(self):
+ try:
+ nbytes = int(self.headers.getheader('content-length'))
+ except:
+ nbytes = 0
+ query = self.rfile.read(nbytes).strip()
+ f = self.send_head(query)
+ if f:
+ self.copyfile(f, self.wfile)
+ f.close()
+
+ def parse_request(self):
+ if self.raw_requestline == '':
+ return False
+ else:
+ return SimpleHTTPRequestHandler.parse_request(self)
+
+ def address_string(self):
+ """Override to avoid DNS lookups"""
+ return "%s:%d" % self.client_address
+
+ def finish(self):
+ SimpleHTTPRequestHandler.finish(self)
+ self.connection.close()
+ while actions_when_finished:
+ actions_when_finished.pop(0)()
+
+actions_when_finished = []
+
+def my_host():
+ import gamesrv
+ port = gamesrv.socketports[gamesrv.openhttpsocket()]
+ return '127.0.0.1:%d' % port
diff --git a/common/javaserver.py b/common/javaserver.py
new file mode 100644
index 0000000..15a586b
--- /dev/null
+++ b/common/javaserver.py
@@ -0,0 +1,157 @@
+import sys, os
+from cStringIO import StringIO
+import httpserver
+
+PLAYERNAMES = ['Bub', 'Bob', 'Boob', 'Beb',
+ 'Biob', 'Bab', 'Bib',
+ 'Baub', 'Beab', 'Biab']
+LOCALDIR = os.path.abspath(os.path.dirname(__file__))
+DATADIR = os.path.join(LOCALDIR, os.pardir, 'http2', 'data')
+
+EMPTY_PAGE = '''<html>
+<head><title>No server is running</title></head>
+<body><h1>No server is running at the moment.</h1>
+</body>
+</html>
+'''
+
+INDEX_PAGE = '''<html>
+<head><title>%(title)s</title></head>
+<body><h1>%(title)s</h1>
+ <applet code=pclient.class width=%(width)s height=%(height)s>
+ <param name="gameport" value="%(gameport)d">
+ %(names1)s
+ </applet>
+<br>
+<p align="center"><a href="name.html?%(names2)s">Player Names &amp; Teams</a></p>
+</body>
+</html>
+'''
+
+NAME_LINE1 = '<param name="%s" value="%s">'
+NAME_SEP1 = '\n'
+NAME_LINE2 = '%s=%s'
+NAME_SEP2 = '&'
+
+def playernames(options):
+ NUM_PLAYERS = len(PLAYERNAMES)
+ result = {}
+ anyname = None
+ for id in range(NUM_PLAYERS):
+ keyid = 'player%d' % id
+ if keyid in options:
+ value = options[keyid][0]
+ anyname = anyname or value
+ teamid = 'team%d' % id
+ if teamid in options:
+ team = options[teamid][0]
+ if len(team) == 1:
+ value = '%s (%s)' % (value, team)
+ result[keyid] = value
+ if 'c' in options:
+ for id in range(NUM_PLAYERS):
+ keyid = 'player%d' % id
+ try:
+ del result[keyid]
+ except KeyError:
+ pass
+ if 'f' in options:
+ for id in range(NUM_PLAYERS):
+ keyid = 'player%d' % id
+ if not result.get(keyid):
+ result[keyid] = anyname or PLAYERNAMES[id]
+ else:
+ anyname = result[keyid]
+ return result
+
+def indexloader(**options):
+ if 'cheat' in options:
+ for opt in options.pop('cheat'):
+ __cheat(opt)
+ import gamesrv
+ if gamesrv.game is None:
+ indexdata = EMPTY_PAGE
+ else:
+ names = playernames(options).items()
+ indexdata = INDEX_PAGE % {
+ 'title': gamesrv.game.FnDesc,
+ 'width': gamesrv.game.width,
+ 'height': gamesrv.game.height,
+ 'gameport': gamesrv.game.address[1],
+ 'names1': NAME_SEP1.join([NAME_LINE1 % kv for kv in names]),
+ 'names2': NAME_SEP2.join([NAME_LINE2 % kv for kv in names]),
+ }
+ return StringIO(indexdata), 'text/html'
+
+def nameloader(**options):
+ if 's' in options:
+ return indexloader(**options)
+ locals = {
+ 'options': playernames(options),
+ }
+ return httpserver.load(os.path.join(DATADIR, 'name.html'),
+ 'text/html', locals=locals)
+
+
+wave_cache = {}
+
+def wav2au(data):
+ # Very limited! Assumes a standard 8-bit mono .wav as input
+ import audioop, struct
+ freq, = struct.unpack("<i", data[24:28])
+ data = data[44:]
+ data = audioop.bias(data, 1, -128)
+ data, ignored = audioop.ratecv(data, 1, 1, freq, 8000, None)
+ data = audioop.lin2ulaw(data, 1)
+ data = struct.pack('>4siiiii8s',
+ '.snd', # header
+ struct.calcsize('>4siiiii8s'), # header size
+ len(data), # data size
+ 1, # encoding
+ 8000, # sample rate
+ 1, # channels
+ 'magic.au') + data
+ return data
+
+def sampleloader(code=[], **options):
+ import gamesrv
+ try:
+ data = wave_cache[code[0]]
+ except KeyError:
+ for key, snd in gamesrv.samples.items():
+ if str(getattr(snd, 'code', '')) == code[0]:
+ data = wave_cache[code[0]] = wav2au(snd.read())
+ break
+ else:
+ raise KeyError, code[0]
+ return StringIO(data), 'audio/wav'
+
+
+def setup():
+ dir = os.path.abspath(os.path.join(os.path.dirname(__file__),
+ os.pardir,
+ 'java'))
+ if not os.path.isdir(dir):
+ return
+
+ # register all '.class' files
+ for name in os.listdir(dir):
+ if name.endswith('.class'):
+ httpserver.register(name, httpserver.fileloader(os.path.join(dir, name)))
+
+ # register a '', an 'index.html', and a 'name.html' file
+ httpserver.register('', indexloader)
+ httpserver.register('index.html', indexloader)
+ httpserver.register('name.html', nameloader)
+
+ # 'name.html' has a few images, list the .png files in DATADIR
+ for fn in os.listdir(DATADIR):
+ fn = fn.lower()
+ if fn.endswith('.png'):
+ httpserver.register(fn, httpserver.fileloader(
+ os.path.join(DATADIR, fn)))
+
+ # register the sample loader
+ httpserver.register('sample.wav', sampleloader)
+
+setup()
diff --git a/common/msgstruct.py b/common/msgstruct.py
new file mode 100644
index 0000000..26389f0
--- /dev/null
+++ b/common/msgstruct.py
@@ -0,0 +1,75 @@
+from struct import pack, unpack, calcsize
+
+try:
+ from localmsg import PORTS
+except ImportError:
+ PORTS = {}
+try:
+ from localmsg import HOSTNAME
+except ImportError:
+ from socket import gethostname
+ HOSTNAME = gethostname()
+
+
+MSG_WELCOME = "Welcome to gamesrv.py(3) !\n"
+MSG_BROADCAST_PORT= "*"
+MSG_DEF_PLAYFIELD = "p"
+MSG_DEF_KEY = "k"
+MSG_DEF_ICON = "r"
+MSG_DEF_BITMAP = "m"
+MSG_DEF_SAMPLE = "w"
+MSG_DEF_MUSIC = "z"
+MSG_PLAY_MUSIC = "Z"
+MSG_FADEOUT = "f"
+MSG_PLAYER_JOIN = "+"
+MSG_PLAYER_KILL = "-"
+MSG_PLAYER_ICON = "i"
+MSG_PING = "g"
+MSG_PONG = "G"
+MSG_INLINE_FRAME = "\\"
+MSG_PATCH_FILE = MSG_DEF_MUSIC
+MSG_ZPATCH_FILE = "P"
+MSG_MD5_FILE = "M"
+MSG_RECORDED = "\x00"
+
+CMSG_PROTO_VERSION= "v"
+CMSG_KEY = "k"
+CMSG_ADD_PLAYER = "+"
+CMSG_REMOVE_PLAYER= "-"
+CMSG_UDP_PORT = "<"
+CMSG_ENABLE_SOUND = "s"
+CMSG_ENABLE_MUSIC = "m"
+CMSG_PING = "g"
+CMSG_PONG = "G"
+CMSG_DATA_REQUEST = "M"
+CMSG_PLAYER_NAME = "n"
+
+BROADCAST_MESSAGE = "game!" # less than 6 bytes
+
+
+def message(tp, *values):
+ strtype = type('')
+ typecodes = ['']
+ for v in values:
+ if type(v) is strtype:
+ typecodes.append('%ds' % len(v))
+ elif 0 <= v < 256:
+ typecodes.append('B')
+ else:
+ typecodes.append('l')
+ typecodes = ''.join(typecodes)
+ assert len(typecodes) < 256
+ return pack(("!B%dsc" % len(typecodes)) + typecodes,
+ len(typecodes), typecodes, tp, *values)
+
+def decodemessage(data):
+ if data:
+ limit = ord(data[0]) + 1
+ if len(data) >= limit:
+ typecodes = "!c" + data[1:limit]
+ end = limit + calcsize(typecodes)
+ if len(data) >= end:
+ return unpack(typecodes, data[limit:end]), data[end:]
+ elif end > 1000000:
+ raise OverflowError
+ return None, data
diff --git a/common/pixmap.py b/common/pixmap.py
new file mode 100644
index 0000000..13142d5
--- /dev/null
+++ b/common/pixmap.py
@@ -0,0 +1,163 @@
+from __future__ import generators
+import cStringIO
+
+def decodepixmap(data):
+ f = cStringIO.StringIO(data)
+ sig = f.readline().strip()
+ assert sig == "P6"
+ while 1:
+ line = f.readline().strip()
+ if not line.startswith('#'):
+ break
+ wh = line.split()
+ w, h = map(int, wh)
+ sig = f.readline().strip()
+ assert sig == "255"
+ data = f.read()
+ f.close()
+ return w, h, data
+
+def encodepixmap(w, h, data):
+ return 'P6\n%d %d\n255\n%s' % (w, h, data)
+
+def cropimage((w, h, data), (x1, y1, w1, h1)):
+ assert 0 <= x1 <= x1+w1 <= w
+ assert 0 <= y1 <= y1+h1 <= h
+ scanline = w*3
+ lines = [data[p:p+w1*3]
+ for p in range(y1*scanline + x1*3,
+ (y1+h1)*scanline + x1*3,
+ scanline)]
+ return w1, h1, ''.join(lines)
+
+def vflip(w, h, data):
+ scanline = w*3
+ lines = [data[p:p+scanline] for p in range(0, len(data), scanline)]
+ lines.reverse()
+ return ''.join(lines)
+
+def hflip(w, h, data):
+ scanline = w*3
+ lines = [''.join([data[p:p+3] for p in range(p1+scanline-3, p1-3, -3)])
+ for p1 in range(0, len(data), scanline)]
+ return ''.join(lines)
+
+def rotate_cw(w, h, data):
+ scanline = w*3
+ lastline = len(data) - scanline
+ lines = [''.join([data[p:p+3] for p in range(lastline + p1, -1, -scanline)])
+ for p1 in range(0, scanline, 3)]
+ return ''.join(lines)
+
+def rotate_ccw(w, h, data):
+ scanline = w*3
+ lines = [''.join([data[p:p+3] for p in range(p1, len(data), scanline)])
+ for p1 in range(scanline-3, -3, -3)]
+ return ''.join(lines)
+
+def rotate_180(w, h, data):
+ scanline = w*3
+ lines = [''.join([data[p:p+3] for p in range(p1+scanline-3, p1-3, -3)])
+ for p1 in range(0, len(data), scanline)]
+ lines.reverse()
+ return ''.join(lines)
+
+def makebkgnd(w, h, data):
+ scanline = 3*w
+ result = []
+ for position in range(0, scanline*h, scanline):
+ line = []
+ for p in range(position, position+scanline, 3):
+ line.append(2 * (chr(ord(data[p ]) >> 3) +
+ chr(ord(data[p+1]) >> 3) +
+ chr(ord(data[p+2]) >> 3)))
+ line = ''.join(line)
+ result.append(line)
+ result.append(line)
+ return w*2, h*2, ''.join(result)
+
+translation_darker = ('\x00\x01' + '\x00'*126 +
+ ''.join([chr(n//4) for n in range(0,128)]))
+translation_dragon = translation_darker[:255] + '\xC0'
+
+def make_dark((w, h, data), translation):
+ return w, h, data.translate(translation)
+
+def col((r, g, b)):
+ r = ord(r)
+ g = ord(g)
+ b = ord(b)
+ return ((g>>2 + r>>3) << 24) | (b << 16) | (g << 8) | r
+
+def imagezoomer(w, h, data):
+ "Zoom a cartoon image by a factor of three, progressively."
+ scale = 3
+ scanline = 3*w
+ rw = (w-1)*scale+1
+ rh = (h-1)*scale+1
+ pixels = []
+ colcache = {}
+ revcache = {}
+ for base in range(0, scanline*h, scanline):
+ line = []
+ for x in range(w):
+ key = data[base + 3*x : base + 3*(x+1)]
+ try:
+ c = colcache[key]
+ except KeyError:
+ c = colcache[key] = col(key)
+ revcache[c] = key
+ line.append(c)
+ pixels.append(line)
+ yield None
+
+ Pairs = {
+ (0, 0): [(0, 0, 0, 0),
+ (-1,0, 1, 0),
+ (0,-1, 0, 1)],
+ (1, 0): [(0, 0, 1, 0),
+ (0, 1, 1,-1),
+ (0,-1, 1, 1)],
+ (2, 0): [(0, 0, 1, 0),
+ (0, 1, 1,-1),
+ (0,-1, 1, 1)],
+ (0, 1): [(0, 0, 0, 1),
+ (-1,0, 1, 1),
+ (1, 0,-1, 1)],
+ (1, 1): [(0, 0, 1, 1),
+ (0, 1, 1, -1),
+ (1, 0,-1, 1)],
+ (2, 1): [(1, 0, 0, 1),
+ (0,-1, 1, 1),
+ (0, 0, 2, 1)],
+ (0, 2): [(0, 0, 0, 1),
+ (-1,0, 1, 1),
+ (1, 0,-1, 1)],
+ (1, 2): [(0, 1, 1, 0),
+ (-1,0, 1, 1),
+ (0, 0, 1, 2)],
+ (2, 2): [(0, 0, 1, 1),
+ (0, 1, 2, 0),
+ (1, 0, 0, 2)],
+ }
+ result = []
+ for y in range(rh):
+ yield None
+ for x in range(rw):
+ # ______________________________
+
+ i = x//scale
+ j = y//scale
+ ps = []
+ for dx1, dy1, dx2, dy2 in Pairs[x%scale, y%scale]:
+ if (0 <= i+dx1 < w and 0 <= i+dx2 < w and
+ 0 <= j+dy1 < h and 0 <= j+dy2 < h):
+ p1 = pixels[j+dy1][i+dx1]
+ p2 = pixels[j+dy2][i+dx2]
+ ps.append(max(p1, p2))
+ p1 = min(ps)
+
+ # ______________________________
+ result.append(revcache[p1])
+ data = ''.join(result)
+ yield (rw, rh, data)
diff --git a/common/stdlog.py b/common/stdlog.py
new file mode 100644
index 0000000..3d8063c
--- /dev/null
+++ b/common/stdlog.py
@@ -0,0 +1,106 @@
+import sys, os
+from time import localtime, ctime
+
+
+class LogFile:
+
+ def __init__(self, filename=None, limitsize=32768):
+ if filename is None:
+ filename = sys.argv[0]
+ if filename.endswith('.py'):
+ filename = filename[:-3]
+ filename += '.log'
+ self.limitsize = limitsize
+ if not self._open(filename):
+ import tempfile
+ filename = os.path.join(tempfile.gettempdir(),
+ os.path.basename(filename))
+ if not self._open(filename):
+ self.f = self.filename = None
+ self.lasttime = None
+
+ def close(self):
+ if self.f is not None:
+ self.f.close()
+ self.f = None
+
+ def __nonzero__(self):
+ return self.f is not None
+
+ def _open(self, filename):
+ try:
+ self.f = open(filename, 'r+', 1)
+ self.f.seek(0, 2)
+ except IOError:
+ # The open r+ might have failed simply because the file
+ # does not exist. Try to create it.
+ try:
+ self.f = open(filename, 'w+', 1)
+ except (OSError, IOError):
+ return 0
+ except OSError:
+ return 0
+ self.filename = filename
+ if self.f.tell() > 0:
+ print >> self.f
+ print >> self.f, '='*44
+ return 1
+
+ def _check(self):
+ if self.f is None:
+ return 0
+ lt = localtime()
+ if lt[:4] != self.lasttime:
+ self.lasttime = lt[:4]
+ if self.f.tell() >= self.limitsize:
+ self.f.seek(-(self.limitsize>>1), 1)
+ data = self.f.read()
+ self.f.seek(0)
+ self.f.write('(...)' + data)
+ self.f.truncate()
+ self.f.write('========= %s =========\n' % ctime())
+ return 1
+
+ def write(self, data):
+ if self._check():
+ self.f.write(data)
+
+ def writelines(self, data):
+ if self._check():
+ self.f.writelines(data)
+
+ def flush(self):
+ if self._check():
+ self.f.flush()
+
+
+class Logger:
+ stdout_captured = 0
+ stderr_captured = 0
+
+ def __init__(self, f):
+ self.targets = [f]
+
+ def capture_stdout(self):
+ if not Logger.stdout_captured:
+ self.targets.append(sys.stdout)
+ sys.stdout = self
+ Logger.stdout_captured = 1
+
+ def capture_stderr(self):
+ if not Logger.stderr_captured:
+ self.targets.append(sys.stderr)
+ sys.stderr = self
+ Logger.stderr_captured = 1
+
+ def write(self, data):
+ for f in self.targets:
+ f.write(data)
+
+ def writelines(self, data):
+ for f in self.targets:
+ f.writelines(data)
+
+ def flush(self):
+ for f in self.targets:
+ f.flush()
diff --git a/common/udpovertcp.py b/common/udpovertcp.py
new file mode 100644
index 0000000..eae3d6b
--- /dev/null
+++ b/common/udpovertcp.py
@@ -0,0 +1,46 @@
+from socket import *
+from msgstruct import *
+#from fcntl import ioctl
+#from termios import TIOCOUTQ
+from zlib import compressobj, Z_SYNC_FLUSH
+import struct
+
+ZeroBuffer = struct.pack("i", 0)
+
+
+class SocketMarshaller:
+
+ def __init__(self, tcpsock, mixer):
+ self.tcpsock = tcpsock
+ self.mixer = mixer
+ self.mixer_can_mix = mixer.send_can_mix
+ self.mixer_send = mixer.send_buffer
+ self.tcpsock_fd = tcpsock.fileno()
+ # try to reduce TCP latency
+ try:
+ tcpsock.setsockopt(SOL_IP, IP_TOS, 0x10) # IPTOS_LOWDELAY
+ except error, e:
+ print "Cannot set IPTOS_LOWDELAY for client:", str(e)
+ try:
+ tcpsock.setsockopt(SOL_TCP, TCP_NODELAY, 1)
+ except error, e:
+ print "Cannot set TCP_NODELAY for client:", str(e)
+ compressor = compressobj(6)
+ self.compress = compressor.compress
+ self.compress_flush = compressor.flush
+
+ def send(self, data):
+ if self.mixer_can_mix():
+ # discard all packets if there is still data waiting in tcpsock
+ # --- mmmh, works much better without this check ---
+ #try:
+ # if ioctl(self.tcpsock_fd, TIOCOUTQ, ZeroBuffer) != ZeroBuffer:
+ # return
+ #except IOError, e:
+ # print "ioctl(TIOCOUTQ) failed, disconnecting client"
+ # self.mixer.disconnect(e)
+ #else:
+ data = self.compress(data) + self.compress_flush(Z_SYNC_FLUSH)
+ self.mixer_send(message(MSG_INLINE_FRAME, data))
+ return len(data)
+ return 0
diff --git a/display/.cvsignore b/display/.cvsignore
new file mode 100644
index 0000000..687980a
--- /dev/null
+++ b/display/.cvsignore
@@ -0,0 +1,4 @@
+*.py[co]
+build
+*.so
+*.pyd
diff --git a/display/Client.py b/display/Client.py
new file mode 100644
index 0000000..3d1330d
--- /dev/null
+++ b/display/Client.py
@@ -0,0 +1,170 @@
+#! /usr/bin/env python
+
+# __________
+import os, sys
+if __name__ == '__main__':
+ LOCALDIR = sys.argv[0]
+else:
+ LOCALDIR = __file__
+try:
+ LOCALDIR = os.readlink(LOCALDIR)
+except:
+ pass
+LOCALDIR = os.path.dirname(os.path.abspath(LOCALDIR))
+# ----------
+
+sys.path.insert(0, os.path.dirname(LOCALDIR))
+sys.path.insert(0, LOCALDIR)
+import common
+import pclient
+import modes
+
+
+UdpLookForServer = [
+ '127.0.0.1',
+ '<broadcast>',
+ ]
+
+def parse_cmdline(argv):
+ # parse command-line
+ def usage():
+ print >> sys.stderr, 'usage:'
+ print >> sys.stderr, ' python Client.py [-d#] [-s#] [extra options] [host[:port]]'
+ print >> sys.stderr
+ print >> sys.stderr, 'options:'
+ print >> sys.stderr, ' host search for a game on the given machine'
+ print >> sys.stderr, ' host:port connect to the given game server'
+ print >> sys.stderr, ' (default search for any local server)'
+ print >> sys.stderr, ' -d# --display=# graphic driver (see below)'
+ print >> sys.stderr, ' -s# --sound=# sound driver (see below)'
+ print >> sys.stderr, ' --music=no disable background music'
+ print >> sys.stderr, ' -h --help display this text'
+ print >> sys.stderr, ' -m --metaserver connect with the help of the metaserver'
+ print >> sys.stderr, ' (list servers with Client.py -m)'
+ print >> sys.stderr, ' -t --tcp for slow or proxy connections'
+ print >> sys.stderr, ' -u --udp for fast direct connections'
+ print >> sys.stderr, ' (default is to autodetect tcp or udp)'
+ print >> sys.stderr, ' --port UDP=# or #:# fixed inbound udp port or host:port'
+ print >> sys.stderr, ' --port TCP=# fixed inbound tcp port (-m only)'
+ print >> sys.stderr
+ print >> sys.stderr, 'graphic drivers:'
+ for info in modes.graphicmodeslist():
+ info.printline(sys.stderr)
+ print >> sys.stderr
+ print >> sys.stderr, 'sound drivers:'
+ for info in modes.soundmodeslist():
+ info.printline(sys.stderr)
+ print >> sys.stderr
+ sys.exit(2)
+
+ shortopts = 'd:s:htum'
+ longopts = ['display=', 'sound=', 'music=', 'help', 'tcp', 'udp',
+ 'cfg=', 'metaserver', 'port=']
+ for info in modes.graphicmodeslist() + modes.soundmodeslist():
+ short, long = info.getformaloptions()
+ shortopts += short
+ longopts += long
+ try:
+ from getopt import gnu_getopt as getopt
+ except ImportError:
+ from getopt import getopt
+ from getopt import error
+ try:
+ opts, args = getopt(argv, shortopts, longopts)
+ except error, e:
+ print >> sys.stderr, 'Client.py: %s' % str(e)
+ print >> sys.stderr
+ usage()
+
+ metaserver = 0
+ driver = sound = None
+ extraopts = {}
+ for key, value in opts:
+ if key in ('-d', '--display'):
+ driver = value
+ elif key in ('-s', '--sound'):
+ sound = value
+ elif key in ('-t', '--tcp'):
+ extraopts['udp_over_tcp'] = 1
+ elif key in ('-u', '--udp'):
+ extraopts['udp_over_tcp'] = 0
+ elif key in ('-m', '--metaserver'):
+ metaserver = 1
+ elif key == '--port':
+ import common.msgstruct
+ try:
+ portname, value = value.split('=')
+ if portname == 'UDP':
+ portname = 'CLIENT'
+ elif portname == 'TCP':
+ portname = 'BACK'
+ except ValueError:
+ portname = 'CLIENT'
+ if portname == 'CLIENT' and ':' in value:
+ udphostname, value = value.split(':')
+ common.msgstruct.PORTS['sendudpto'] = udphostname
+ common.msgstruct.PORTS[portname] = int(value)
+ elif key == '--cfg':
+ extraopts['cfgfile'] = value
+ elif key in ('-h', '--help'):
+ usage()
+ else:
+ extraopts[key] = value
+ mode = driver, sound, extraopts
+
+ if metaserver:
+ if len(args) == 0:
+ metalist()
+ sys.exit(0)
+ elif len(args) != 1 or ':' not in args[0]:
+ usage()
+ return metaconnect(args[0]), mode
+
+ if args:
+ if len(args) > 1:
+ usage()
+ hosts = args[0].split(':')
+ if len(hosts) == 1:
+ host, = hosts
+ from common import hostchooser
+ server = hostchooser.pick([host] * 5)
+ elif len(hosts) == 2:
+ host, port = hosts
+ try:
+ port = int(port)
+ except ValueError:
+ usage()
+ server = host, port
+ else:
+ usage()
+ return directconnect(server), mode
+
+ from common import hostchooser
+ server = hostchooser.pick(UdpLookForServer * 3)
+ return directconnect(server), mode
+
+def directconnect(sockaddr):
+ print "connecting to %s:%d..." % sockaddr
+ from socket import socket, AF_INET, SOCK_STREAM
+ s = socket(AF_INET, SOCK_STREAM)
+ s.connect(sockaddr)
+ return s, sockaddr
+
+def metaconnect(metaaddr):
+ from metaserver import metaclient
+ import common.msgstruct
+ port = common.msgstruct.PORTS.get('BACK')
+ s = metaclient.meta_connect(metaaddr, port)
+ sockaddr = s.getpeername()
+ return s, sockaddr
+
+def metalist():
+ from metaserver import metaclient
+ metaclient.print_server_list()
+
+def main():
+ (s, sockaddr), mode = parse_cmdline(sys.argv[1:])
+ pclient.run(s, sockaddr, mode)
+
+if __name__ == '__main__':
+ main()
diff --git a/display/Makefile b/display/Makefile
new file mode 100644
index 0000000..214347a
--- /dev/null
+++ b/display/Makefile
@@ -0,0 +1,2 @@
+xshm.so: xshm.c
+ python setup.py build_ext -i
diff --git a/display/__init__.py b/display/__init__.py
new file mode 100644
index 0000000..ea30561
--- /dev/null
+++ b/display/__init__.py
@@ -0,0 +1 @@
+#empty
diff --git a/display/caching.py b/display/caching.py
new file mode 100644
index 0000000..27d772c
--- /dev/null
+++ b/display/caching.py
@@ -0,0 +1,261 @@
+from __future__ import generators
+import os, md5, sys
+#import common.debug
+
+
+class FileCache:
+ MAX_FILES = 8
+
+ def __init__(self):
+ self.cache = {}
+ self.time = 0
+
+ def access(self, filename, position, writing=0):
+ if filename in self.cache:
+ time, mode, f = self.cache[filename]
+ if writing > mode:
+ f.close()
+ del self.cache[filename]
+ if filename not in self.cache:
+ if len(self.cache) >= FileCache.MAX_FILES:
+ (time, mode, f), k = min([(v,k) for (k,v) in self.cache.items()])
+ f.close()
+ del self.cache[k]
+ try:
+ f = open(filename, ('rb', 'r+b')[writing])
+ except (IOError, OSError):
+ if not writing:
+ raise
+ if not os.path.isdir(os.path.dirname(filename)):
+ os.mkdir(os.path.dirname(filename))
+ f = open(filename, 'w+b')
+ mode = writing
+ self.time += 1
+ self.cache[filename] = self.time, mode, f
+ f.seek(position)
+ return f
+
+
+class MemoryBlock:
+ def __init__(self, data):
+ self.data = data
+ def overwrite(self, newdata):
+ self.data = newdata
+ def read(self):
+ return self.data
+
+class FileBlock:
+ def __init__(self, filename, position, length, readonly=1, complete=1):
+ self.filename = filename
+ self.position = position
+ self.length = length
+ self.readonly = readonly
+ self.complete = complete
+ def overwrite(self, newdata):
+ self.memorydata = newdata
+ if self.readonly:
+ print >> sys.stderr, "cannot overwrite file", self.filename
+ return
+ try:
+ f = Data.Cache.access(self.filename, self.position, writing=1)
+ f.write(newdata)
+ except (IOError, OSError):
+ print >> sys.stderr, "cache write error:", self.filename
+ return
+ self.complete = 1
+ del self.memorydata
+ def read(self):
+ if self.complete:
+ f = Data.Cache.access(self.filename, self.position)
+ return f.read(self.length)
+ else:
+ return self.memorydata
+
+
+class Data:
+ SafeChars = {}
+ for c in ".abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789":
+ SafeChars[c] = c
+ Translate = ''.join([SafeChars.get(chr(c), '_') for c in range(256)])
+ del c, SafeChars
+ Cache = FileCache()
+
+ def __init__(self):
+ self.content = {}
+ self.backupfile = None
+ self.readonly = 0
+
+ clear = __init__
+
+ ### Public interface ###
+
+ def store(self, position, data, filename=None, readonly=1):
+ """This class assumes that all accesses to block within the data
+ are done for disjoint intervals: no overlapping writes !"""
+ if self.content is not None:
+ try:
+ self.content[position].overwrite(data)
+ except KeyError:
+ if filename is None:
+ self.content[position] = MemoryBlock(data)
+ else:
+ self.content[position] = FileBlock(filename, position,
+ len(data), readonly)
+ if self.backupfile and not self.readonly:
+ try:
+ f = Data.Cache.access(self.backupfile, position, writing=1)
+ f.write(data)
+ f.flush()
+ except (IOError, OSError):
+ print >> sys.stderr, "cache write error:", self.backupfile
+
+ def loadfrom(self, filename, position, length, checksum):
+ """Try to load data from the given filename, with the given
+ expected MD5 checksum. The filename must be Unix-style, and is
+ looked up both in the directory SOURCEDIR and with a mangled name
+ in the cache directory CACHEDIR."""
+ directname = os.path.join(self.SOURCEDIR, *filename.split('/'))
+ mangledname = filename.translate(Data.Translate)
+ cachename = os.path.join(self.CACHEDIR, mangledname)
+ for name, readonly in ((directname, 1), (cachename, 0)):
+ try:
+ f = Data.Cache.access(name, position)
+ data = f.read(length)
+ except (IOError, OSError):
+ pass
+ else:
+ if len(data) == length and md5.new(data).digest() == checksum:
+ # correct data
+ self.store(position, data, name, readonly)
+ return 1
+ if self.content is not None and not self.content.has_key(position):
+ self.content[position] = FileBlock(cachename, position, length,
+ readonly=0, complete=0)
+ elif self.readonly:
+ print >> sys.stderr, "Note: the music data has changed. You can get"
+ print >> sys.stderr, "the server's version by deleting", directname
+ return 1 # incorrect data, but ignored
+ return 0
+
+ def read(self):
+ """Return the data as built so far."""
+ if self.content is not None:
+ items = self.content.items()
+ items.sort()
+ result = ''
+ for position, block in items:
+ if len(result) < position:
+ result += '\x00' * (position-len(result))
+ data = block.read()
+ result = result[:position] + data + result[position+len(data):]
+ return result
+ else:
+ f = Data.Cache.access(self.backupfile, 0)
+ return f.read()
+
+ def fopen(self):
+ if self.content is not None:
+ from cStringIO import StringIO
+ return StringIO(self.read())
+ else:
+ return Data.Cache.access(self.backupfile, 0)
+
+ def freezefilename(self, fileexthint='.wav'):
+ """Return the name of a file from which the data can be read. If all
+ the current data comes from the same file, it is assumed to be exactly
+ the file that we want."""
+ if not self.backupfile:
+ files = {}
+ for position, block in self.content.items():
+ if not isinstance(block, FileBlock):
+ break
+ if block.complete:
+ files[block.filename] = block
+ else:
+ if len(files) == 1:
+ self.backupfile, block = files.items()[0]
+ self.readonly = block.readonly
+ if not self.backupfile:
+ self.backupfile = mktemp(fileexthint)
+ f = Data.Cache.access(self.backupfile, 0, writing=1)
+ for position, block in self.content.items():
+ f.seek(position)
+ f.write(block.read())
+ f.flush()
+ #print 'freezefilename ->', self.backupfile
+ #print ' readonly =', self.readonly
+ self.content = None
+ return self.backupfile
+
+# ____________________________________________________________
+# Temporary files management
+# from the 'py' lib, mostly written by hpk
+
+def try_remove_dir(udir):
+ try:
+ for name in os.listdir(udir):
+ try:
+ os.unlink(os.path.join(udir, name))
+ except:
+ pass
+ os.rmdir(udir)
+ except:
+ pass
+
+def make_numbered_dir(prefix='tmp-bub-n-bros-', rootdir=None, keep=0,
+ lock_timeout = 172800): # two days
+ """ return unique directory with a number greater than the current
+ maximum one. The number is assumed to start directly after prefix.
+ Directories with a number less than (maxnum-keep) will be removed.
+ """
+ import atexit, tempfile
+ if rootdir is None:
+ rootdir = tempfile.gettempdir()
+
+ def parse_num(bn):
+ """ parse the number out of a path (if it matches the prefix) """
+ if bn.startswith(prefix):
+ try:
+ return int(bn[len(prefix):])
+ except ValueError:
+ pass
+
+ # compute the maximum number currently in use with the
+ # prefix
+ maxnum = -1
+ for basename in os.listdir(rootdir):
+ num = parse_num(basename)
+ if num is not None:
+ maxnum = max(maxnum, num)
+
+ # make the new directory
+ udir = os.path.join(rootdir, prefix + str(maxnum+1))
+ os.mkdir(udir)
+
+ # try to remove the directory at process exit
+ atexit.register(try_remove_dir, udir)
+
+ # prune old directories
+ for basename in os.listdir(rootdir):
+ num = parse_num(basename)
+ if num is not None and num <= (maxnum - keep):
+ d1 = os.path.join(rootdir, basename)
+ try:
+ t1 = os.stat(d1).st_mtime
+ t2 = os.stat(udir).st_mtime
+ if abs(t2-t1) < lock_timeout:
+ continue # skip directories still recently used
+ except:
+ pass
+ try_remove_dir(d1)
+ return udir
+
+def enumtempfiles():
+ tempdir = make_numbered_dir()
+ i = 0
+ while True:
+ yield os.path.join(tempdir, 'b%d' % i)
+ i += 1
+
+def mktemp(fileext, gen = enumtempfiles()):
+ return gen.next() + fileext
diff --git a/display/dpy_gtk.py b/display/dpy_gtk.py
new file mode 100644
index 0000000..7b73516
--- /dev/null
+++ b/display/dpy_gtk.py
@@ -0,0 +1,204 @@
+
+ ################################################
+## GTK-based implementation of xshm ##
+################################################
+
+import os, sys, math
+from modes import KeyPressed, KeyReleased
+import caching
+
+def import_trickery():
+ global gtk, gdk
+ argv = sys.argv[:]
+ del sys.argv[1:]
+ import gtk
+ from gtk import gdk
+ sys.argv[:] = argv
+import_trickery()
+
+
+class Display:
+
+ def __init__(self, width, height, title, zoom="100"):
+ if zoom.endswith('%'):
+ zoom = zoom[:-1]
+ scale = float(zoom) / 100.0
+ iscale = int(scale+0.001)
+ if abs(scale - iscale) < 0.002:
+ scale = iscale
+ self.scale = scale
+
+ self.width = int(width * scale)
+ self.height = int(height * scale)
+ self.tempppmfile = caching.mktemp('.ppm')
+
+ # create a top level window
+ w = self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+ w.connect("destroy", lambda w: sys.exit())
+ w.connect("key-press-event", self.key_press_event)
+ w.connect("key-release-event", self.key_release_event)
+ w.connect("motion-notify-event", self.motion_notify_event)
+ w.connect("button-press-event", self.button_press_event)
+ w.add_events(gdk.KEY_PRESS_MASK |
+ gdk.POINTER_MOTION_MASK |
+ gdk.BUTTON_PRESS_MASK)
+ w.resize(self.width, self.height)
+ w.set_title(title)
+ w.show()
+
+ self.offscreen = gtk.create_pixmap(w.window, self.width, self.height)
+ self.gc = gdk.gc_new(w.window)
+ self.gc.set_rgb_fg_color(gdk.color_parse('#000000'))
+
+ self.events_key = []
+ self.events_mouse = []
+ self.event_motion = None
+
+ pixel = "\x00\x00\x80"
+ hole = "\x01\x01\x01"
+ pb = self.pixmap(32, 32, ((pixel+hole)*16 + (hole+pixel)*16) * 16, 0x010101)
+ self.taskbkgnd = self.renderpixbuf(pb)
+
+ def taskbar(self, (x, y, w, h)):
+ scale = self.scale
+ x2 = x+w
+ y2 = y+h
+ x, y, x2, y2 = int(x*scale), int(y*scale), int(x2*scale), int(y2*scale)
+ pixmap, gc, ignored = self.taskbkgnd
+ for j in range(y, y2, 32):
+ for i in range(x, x2, 32):
+ gc.set_clip_origin(i, j)
+ self.offscreen.draw_drawable(gc, pixmap, 0, 0,
+ i, j, x2-i, y2-j)
+
+ def pixmap(self, w, h, data, colorkey=-1):
+ filename = self.tempppmfile
+ f = open(filename, 'wb')
+ print >> f, 'P6'
+ print >> f, w, h
+ print >> f, 255
+ f.write(data)
+ f.close()
+ pb = gdk.pixbuf_new_from_file(filename)
+ if colorkey >= 0:
+ pb = pb.add_alpha(1, chr(colorkey >> 16),
+ chr((colorkey >> 8) & 0xFF),
+ chr(colorkey & 0xFF))
+ if self.scale == 1:
+ return self.renderpixbuf((pb,))
+ else:
+ return (pb,)
+
+ def renderpixbuf(self, input):
+ if len(input) == 3:
+ return input
+ pb, = input
+ pixmap, mask = pb.render_pixmap_and_mask()
+ if mask is not None:
+ gc = gdk.gc_new(self.window.window)
+ gc.set_clip_mask(mask)
+ return (pixmap, gc, mask)
+ else:
+ return (pixmap, self.gc, None)
+
+ def getopticon(self, input, (x, y, w, h), ignored_alpha=255):
+ if len(input) == 3:
+ return None
+ pb, = input
+ scale = self.scale
+ newpb = gdk.Pixbuf("rgb", 1, 8, w, h)
+ newpb.fill(0)
+ pb.copy_area(x, y, w, h, newpb, 0, 0)
+ newpb = newpb.scale_simple(int(w*scale), int(h*scale),
+ gdk.INTERP_HYPER)
+ if newpb is None:
+ return None, None, None
+ else:
+ return self.renderpixbuf((newpb,))
+
+ def getppm(self, (x, y, w, h), int=int, ceil=math.ceil):
+ scale = self.scale
+ if isinstance(scale, int):
+ x *= scale
+ y *= scale
+ w *= scale
+ h *= scale
+ else:
+ w = int(ceil((x+w)*scale))
+ h = int(ceil((y+h)*scale))
+ x = int(x*scale)
+ y = int(y*scale)
+ w -= x
+ h -= y
+ bkgnd = gtk.create_pixmap(self.window.window, w, h)
+ bkgnd.draw_drawable(self.gc, self.offscreen, x, y, 0, 0, w, h)
+ return bkgnd, self.gc, None
+
+ def putppm(self, x, y, (pixmap, gc, ignored), rect=None, int=int):
+ if pixmap is None:
+ return
+ scale = self.scale
+ if rect is None:
+ srcx = srcy = 0
+ w = h = 4095
+ else:
+ srcx, srcy, w, h = rect
+ x = int(x*scale)
+ y = int(y*scale)
+ if gc is not self.gc:
+ gc.set_clip_origin(x-srcx, y-srcy)
+ self.offscreen.draw_drawable(gc, pixmap, srcx, srcy, x, y, w, h)
+
+ def flip(self):
+ self.window.window.draw_drawable(self.gc, self.offscreen,
+ 0, 0, 0, 0, self.width, self.height)
+ gdk.flush()
+ self.events_poll()
+
+ def close(self):
+ self.window.destroy()
+
+ def clear(self):
+ self.offscreen.draw_rectangle(self.gc, 1,
+ 0, 0, self.width, self.height)
+
+ def events_poll(self):
+ while gtk.events_pending():
+ gtk.main_iteration()
+
+ def key_press_event(self, window, event):
+ self.events_key.append((event.keyval, KeyPressed))
+
+ def key_release_event(self, window, event):
+ self.events_key.append((event.keyval, KeyReleased))
+
+ def motion_notify_event(self, window, event):
+ self.event_motion = (int(event.x/self.scale), int(event.y/self.scale))
+
+ def button_press_event(self, window, event):
+ self.events_mouse.append((int(event.x/self.scale), int(event.y/self.scale)))
+
+ def keyevents(self):
+ self.events_poll()
+ result = self.events_key
+ self.events_key = []
+ return result
+
+ def pointermotion(self):
+ result = self.event_motion
+ self.event_motion = None
+ return result
+
+ def mouseevents(self):
+ self.events_poll()
+ result = self.events_mouse
+ self.events_mouse = []
+ return result
+
+ def selectlist(self):
+ return []
+
+
+def htmloptionstext(nameval):
+ return 'Scale image by <%s size=5>%%' % (
+ nameval('text', 'zoom', default='100'))
diff --git a/display/dpy_pygame.py b/display/dpy_pygame.py
new file mode 100644
index 0000000..acfd200
--- /dev/null
+++ b/display/dpy_pygame.py
@@ -0,0 +1,245 @@
+
+ ################################################
+## pygame-based implementation of xshm ##
+################################################
+
+import os
+import pygame
+from pygame.locals import *
+from modes import KeyPressed, KeyReleased
+
+
+class Display:
+ musthidemouse = 0
+ mousevisible = 1
+
+ def __init__(self, width, height, title, transparency='yes',
+ fullscreen='no', zoom='100%', smooth='yes',
+ smoothfast='yes'):
+ self.use_transparency = not transparency.startswith('n')
+ self.use_fullscreen = fullscreen.startswith('y')
+ if zoom.endswith('%'):
+ zoom = zoom[:-1]
+ scale = float(zoom) / 100.0
+ iscale = int(scale+0.001)
+ if abs(scale - iscale) < 0.002:
+ scale = iscale
+ self.scale = scale
+ self.smooth = smooth.startswith('y')
+ self.smoothfast = smoothfast.startswith('y')
+
+ # Initialize pygame
+ pygame.init()
+
+ # Set the display mode
+ winstyle = HWSURFACE
+ if self.use_fullscreen:
+ winstyle |= FULLSCREEN
+ bestdepth = pygame.display.mode_ok((int(width * self.scale),
+ int(height * self.scale)),
+ winstyle, 32)
+ self.screen = pygame.display.set_mode((int(width * self.scale),
+ int(height * self.scale)),
+ winstyle, bestdepth)
+ self.offscreen = pygame.Surface((width, height))
+ #decorate the game window
+ pygame.display.set_caption(title)
+ #pygame.mouse.set_visible(0)
+ self.tbcache = None, None
+ self.events_key = []
+ self.events_mouse = []
+ self.prevposition = None
+ EVENT_HANDLERS[KEYDOWN] = self.keydown_handler
+ EVENT_HANDLERS[KEYUP] = self.keyup_handler
+ EVENT_HANDLERS[MOUSEBUTTONDOWN] = self.mousebuttondown_handler
+
+ def keydown_handler(self, e):
+ if e.key == K_ESCAPE and self.use_fullscreen:
+ raise SystemExit # ESC to exit the game if full-screen
+ self.showmouse(not self.musthidemouse)
+ self.events_key.append((e.key, KeyPressed))
+ del self.events_key[:-16]
+
+ def keyup_handler(self, e):
+ self.events_key.append((e.key, KeyReleased))
+ del self.events_key[:-16]
+
+ def mousebuttondown_handler(self, e):
+ self.showmouse(1)
+ self.events_mouse.append(self.fixpos(e.pos))
+ del self.events_mouse[:-8]
+
+ def pixmap(self, w, h, data, colorkey=-1):
+ img = pygame.image.fromstring(data, (w, h), "RGB")
+ if colorkey >= 0:
+ r = colorkey & 0xFF
+ g = (colorkey >> 8) & 0xFF
+ b = (colorkey >> 16) & 0xFF
+ img.set_colorkey([r, g, b])
+ return img # not optimized -- must use getopticon()
+
+ def getopticon(self, pixmap, rect, alpha=255):
+ if not self.use_transparency:
+ alpha = 255
+ img = pixmap.subsurface(rect)
+ colorkey = pixmap.get_colorkey()
+ if alpha == 255 and not colorkey:
+ return img.convert(self.offscreen)
+ else:
+ if colorkey:
+ img.set_colorkey(colorkey, RLEACCEL)
+ if alpha < 255:
+ img.set_alpha(alpha, RLEACCEL)
+ img = img.convert_alpha(self.offscreen)
+ img.set_alpha(255, RLEACCEL)
+ return img
+
+## def vflipppm(self, img):
+## w, h = img.get_size()
+## colorkey = img.get_colorkey()
+## data = pygame.image.tostring(img, "RGB", 1)
+## flipimg = pygame.image.fromstring(data, (w, h), "RGB")
+## flipimg.set_colorkey(colorkey, RLEACCEL)
+## return flipimg, h
+
+ def getppm(self, rect):
+ bkgnd = pygame.Surface(rect[2:])
+ bkgnd.blit(self.offscreen, (0, 0), rect)
+ return bkgnd
+
+ def putppm(self, x, y, bitmap, rect=None):
+ if rect:
+ self.offscreen.blit(bitmap, (x, y), rect)
+ else:
+ self.offscreen.blit(bitmap, (x, y))
+
+ def flip(self):
+ offscreen = self.offscreen
+ if self.scale != 1:
+ w, h = offscreen.get_size()
+ w = int(w * self.scale)
+ h = int(h * self.scale)
+ if self.scale == 2 and self.smoothfast:
+ offscreen = pygame.transform.scale2x(offscreen)
+ elif self.smooth:
+ offscreen = pygame.transform.smoothscale(offscreen, (w, h))
+ else:
+ offscreen = pygame.transform.scale(offscreen, (w, h))
+ self.screen.blit(offscreen, (0, 0))
+ pygame.display.flip()
+ events_dispatch()
+
+ def close(self):
+ self.showmouse(1)
+ pygame.display.quit()
+
+ def clear(self):
+ self.offscreen.fill([0,0,0,])
+
+ def fixpos(self, (x, y)):
+ if self.scale != 1:
+ x = int(x / self.scale)
+ y = int(y / self.scale)
+ return (x, y)
+
+ def events_poll(self):
+ while 1:
+ e = pygame.event.poll()
+ if e.type == NOEVENT:
+ break
+ elif e.type == KEYDOWN:
+ self.events_key.append((e.key, KeyPressed))
+ del self.events_key[:-16]
+ elif e.type == KEYUP:
+ self.events_key.append((e.key, KeyReleased))
+ del self.events_key[:-16]
+ elif e.type == MOUSEBUTTONDOWN:
+ self.events_mouse.append(self.fixpos(e.pos))
+ del self.events_mouse[:-8]
+ elif e.type == ENDMUSICEVENT:
+ self.next_music()
+ elif e.type == QUIT:
+ raise SystemExit
+
+ def keyevents(self):
+ events_dispatch()
+ events = self.events_key
+ self.events_key = []
+ return events
+
+ def pointermotion(self):
+ position = pygame.mouse.get_pos()
+ if position != self.prevposition:
+ self.showmouse(1)
+ self.prevposition = position
+ return self.fixpos(position)
+ else:
+ return None
+
+ def mouseevents(self):
+ events_dispatch()
+ events = self.events_mouse
+ self.events_mouse = []
+ return events
+
+ def selectlist(self):
+ return []
+
+ def taskbar(self, (x, y, w, h)):
+ tbs, tbh = self.tbcache
+ if tbh != h:
+ tbs = pygame.Surface((32, h)).convert_alpha(self.offscreen)
+ alpha_f = 256.0 / h
+ for j in range(h):
+ tbs.fill((128, 128, 255, int(j*alpha_f)),
+ (0, j, 32, 1))
+ self.tbcache = tbs, h
+ for i in range(x, x+w, 32):
+ dw = x+w-i
+ if dw < 32:
+ self.offscreen.blit(tbs, (i, y), (0, 0, dw, h))
+ else:
+ self.offscreen.blit(tbs, (i, y))
+
+ def settaskbar(self, tb_visible):
+ self.showmouse(1)
+ self.musthidemouse = not tb_visible # and self.use_fullscreen
+
+ def showmouse(self, v):
+ if v != self.mousevisible:
+ self.mousevisible = v
+ pygame.mouse.set_visible(v)
+
+
+def quit_handler(e):
+ raise SystemExit
+
+EVENT_HANDLERS = {
+ QUIT: quit_handler,
+ }
+
+def events_dispatch(handlers = EVENT_HANDLERS):
+ while 1:
+ e = pygame.event.poll()
+ if e.type == NOEVENT:
+ break
+ elif handlers.has_key(e.type):
+ handlers[e.type](e)
+
+
+def htmloptionstext(nameval):
+ return '''
+<%s> Full Screen (Esc key to exit)</input><%s><br>
+<%s> Draw slightly transparent bubbles</input><%s><br>
+Scale image by <%s size=5>%%<br>
+<%s> Smoothed scaled image</input><%s><br>
+<%s> Semi-smoothed scaled image (for 200%% only)</input><%s><br>
+''' % (nameval("checkbox", "fullscreen", "yes", default="no"),
+ nameval("hidden", "fullscreen", "no"),
+ nameval("checkbox", "transparency", "yes", default="yes"),
+ nameval("hidden", "transparency", "no"),
+ nameval("text", "zoom", default="100"),
+ nameval("checkbox", "smooth", "yes", default="yes"),
+ nameval("hidden", "smooth", "no"),
+ nameval("checkbox", "smoothfast", "yes", default="no"),
+ nameval("hidden", "smoothfast", "no"))
diff --git a/display/dpy_windows.py b/display/dpy_windows.py
new file mode 100755
index 0000000..b01dfcf
--- /dev/null
+++ b/display/dpy_windows.py
@@ -0,0 +1,23 @@
+import sys
+import wingame
+from modes import BaseDisplay
+from cStringIO import StringIO
+
+
+class Display(BaseDisplay):
+
+ def __init__(self, width, height, title):
+ self.xdpy = xdpy = wingame.Display(width, height)
+ xdpy.settitle(title)
+ self.pixmap = xdpy.pixmap
+ self.getppm = xdpy.getppm
+ self.putppm = xdpy.putppm
+ self.close = xdpy.close
+ self.clear = xdpy.clear
+ self.flip = xdpy.flip
+ self.keyevents = xdpy.keyevents
+ self.mouseevents = xdpy.mouseevents
+ self.pointermotion = xdpy.pointermotion
+
+ def selectlist(self):
+ return []
diff --git a/display/dpy_x.py b/display/dpy_x.py
new file mode 100644
index 0000000..ffca90f
--- /dev/null
+++ b/display/dpy_x.py
@@ -0,0 +1,40 @@
+import sys
+import xshm
+from modes import BaseDisplay
+from cStringIO import StringIO
+
+
+class Display(BaseDisplay):
+
+ def __init__(self, width, height, title, shm='yes'):
+ use_shm = not shm.startswith('n')
+ self.xdpy = xdpy = xshm.Display(width, height, use_shm)
+ self.pixmap = xdpy.pixmap
+ self.getppm = xdpy.getppm
+ self.putppm = xdpy.putppm
+ self.overlayppm = xdpy.overlayppm
+ self.close = xdpy.close
+ self.clear = xdpy.clear
+ self.flip = xdpy.flip
+ self.keyevents = xdpy.keyevents
+ self.mouseevents = xdpy.mouseevents
+ self.pointermotion = xdpy.pointermotion
+ if use_shm and not xdpy.shmmode():
+ print >> sys.stderr, \
+ "Note: cannot use SHM extension (%dx%d), display will be slow." % \
+ (width, height)
+
+ def selectlist(self):
+ if hasattr(self.xdpy, 'fd'):
+ from socket import fromfd, AF_INET, SOCK_STREAM
+ return [fromfd(self.xdpy.fd(), AF_INET, SOCK_STREAM)]
+ else:
+ return []
+
+
+def htmloptionstext(nameval):
+ return '''
+<%s> Use the shared memory extension</input><%s><br>
+<font size=-1>Note: Disable it for remote connections or old X servers</font>
+''' % (nameval("checkbox", "shm", "yes", default="yes"),
+ nameval("hidden", "shm", "no"))
diff --git a/display/modes.py b/display/modes.py
new file mode 100644
index 0000000..ad0c1c0
--- /dev/null
+++ b/display/modes.py
@@ -0,0 +1,196 @@
+import sys
+
+KeyPressed = 2
+KeyReleased = 3
+
+
+class BaseDisplay:
+ __taskbkgnd = None
+
+ def taskbar(self, (x, y, w, h)):
+ if self.__taskbkgnd is None:
+ pixel = "\x00\x00\x80"
+ hole = "\x01\x01\x01"
+ self.__taskbkgnd = self.pixmap(32, 32,
+ ((pixel+hole)*16 + (hole+pixel)*16) * 16, 0x010101)
+ for j in range(y, y+h, 32):
+ for i in range(x, x+w, 32):
+ self.putppm(i, j, self.__taskbkgnd,
+ (0, 0, x+w-i, y+h-j))
+
+
+class Mode:
+ low_priority = 0
+
+ def __init__(self, name, descr, extraoptsdescr,
+ options={}, url=None):
+ self.name = name
+ self.descr = descr
+ self.extraoptsdescr = extraoptsdescr
+ self.options = options.copy()
+ self.url = url
+
+ def getmodule(self):
+ return __import__(self.prefix + self.name.lower(), globals(),
+ locals(), ['available'])
+
+ def imperror(self):
+ try:
+ return self.__imperror
+ except AttributeError:
+ try:
+ module = self.getmodule()
+ except ImportError:
+ result = 'not installed'
+ else:
+ result = hasattr(module, 'imperror') and module.imperror()
+ self.__imperror = result
+ return result
+
+ def unique_id(self):
+ return self.prefix + self.name
+
+ def printline(self, f):
+ err = self.imperror()
+ if err:
+ state = ' [%s]' % err
+ else:
+ state = ''
+ print >> f, ' %-8s %s%s' % (self.name, self.descr, state)
+ if self.url:
+ print >> f, ' %s' % self.url
+ for line in self.extraoptsdescr:
+ print >> f, ' %s' % line
+
+ def getformaloptions(self):
+ return '', [c+'=' for c in self.options.keys()]
+
+ def setoptions(self, options):
+ for key in self.options.keys():
+ if options.has_key('--'+key):
+ self.options[key] = options['--'+key]
+
+ def currentdriver(self):
+ lst = self.options.items()
+ lst.sort()
+ lst = ['--%s=%s' % keyvalue for keyvalue in lst]
+ return ' '.join([self.name] + lst)
+
+ def htmloptionstext(self, *args):
+ if self.imperror():
+ return None
+ module = self.getmodule()
+ return (hasattr(module, 'htmloptionstext') and
+ module.htmloptionstext(*args))
+
+
+class GraphicMode(Mode):
+ prefix = 'dpy_'
+
+
+class SoundMode(Mode):
+ prefix = 'snd_'
+
+
+def graphicmodeslist():
+ return [
+ GraphicMode('X', 'XWindow (Linux/Unix)',
+ ['--shm=yes use the Shared Memory extension (default)',
+ '--shm=no disable it (for remote connections or old X servers)',
+ ],
+ {'shm': 'yes'}),
+ GraphicMode('windows', 'MS Windows', []),
+ GraphicMode('pygame', 'PyGame library (all platforms)',
+ ['--fullscreen=yes go full screen (Esc key to exit)',
+ '--transparency=yes slightly transparent bubbles (default)',
+ '--transparency=no disable it (a bit faster)',
+ '--zoom=xxx% scale image by xxx %',
+ '--smooth smoothed scaled image',
+ '--smoothfast semi-smoothed, for 200% scale only'],
+ {'transparency': 'yes', 'fullscreen': 'no',
+ 'zoom': '100', 'smooth': 'yes', 'smoothfast': 'no'},
+ url='http://www.pygame.org'),
+ GraphicMode('gtk', 'PyGTK (Gnome)',
+ ['--zoom=xxx% scale image by xxx %'],
+ {'zoom': '100'},
+ url='http://www.pygtk.org/'),
+ ]
+
+def soundmodeslist():
+ return [
+ SoundMode('pygame', 'PyGame library mixer (all platforms)',
+ [], url='http://www.pygame.org'),
+ SoundMode('linux', 'audio mixer for Linux',
+ ['--freq=# mixer frequency (default 44100)',
+ '--fmt=# data format (default S16_NE, --fmt=list for a list)'],
+ {'freq': '44100', 'fmt': 'S16_NE'}),
+ SoundMode('windows', 'audio mixer for Windows',
+ ['--freq=# mixer frequency (default 44100)',
+ '--bits=# bits per sample (8 or default 16)'],
+ {'freq': '44100', 'bits': '16'}),
+ SoundMode('off', 'no sounds', []),
+ ]
+
+def findmode(name, lst):
+ if name is None:
+ # find the first installed mode
+ last_chance = None
+ for info in lst:
+ err = info.imperror()
+ if err:
+ continue
+ if info.low_priority:
+ if last_chance is None:
+ last_chance = info
+ else:
+ return info
+ if last_chance is not None:
+ return last_chance
+ raise KeyError, 'no driver available!'
+ else:
+ # find mode by name
+ for info in lst:
+ if info.name.upper() == name.upper():
+ err = info.imperror()
+ if err:
+ raise KeyError, '%s: %s' % (info.name, err)
+ return info
+ raise KeyError, '%s: no such driver' % name
+
+def findmode_err(*args):
+ try:
+ return findmode(*args)
+ except KeyError, e:
+ print >> sys.stderr, str(e)
+ sys.exit(1)
+
+def open_dpy(mode, width, height, title):
+ driver, sound, extraopts = mode
+ ginfo = findmode_err(driver, graphicmodeslist())
+ ginfo.setoptions(extraopts)
+ dpy = ginfo.getmodule().Display(width, height, title, **ginfo.options)
+ print 'graphics driver:', ginfo.currentdriver()
+ return dpy
+
+def open_snd(mode):
+ driver, sound, extraopts = mode
+ sinfo = findmode_err(sound, soundmodeslist())
+ sinfo.setoptions(extraopts)
+ snd = sinfo.getmodule().Sound(**sinfo.options)
+ if snd.has_sound:
+ sinfo.options['music'] = 'yes'
+ sinfo.setoptions(extraopts)
+ if (sinfo.options['music'].startswith('n') or
+ sinfo.options['music'] == 'off'):
+ snd.has_music = 0
+ print 'sound driver:', sinfo.currentdriver()
+ return snd
+ else:
+ return None
+
+
+def musichtmloptiontext(nameval):
+ return '''<font size=-1>
+<%s> Background music</input><%s>
+</font>''' % (nameval("checkbox", "music", "yes", default="yes", mangling=0),
+ nameval("hidden", "music", "no", mangling=0))
diff --git a/display/music1.py b/display/music1.py
new file mode 100644
index 0000000..b5c0c6d
--- /dev/null
+++ b/display/music1.py
@@ -0,0 +1,33 @@
+class Music:
+ def __init__(self, filename):
+ self.filename = filename
+ self.w = None
+ self.sampledata = ''
+ def openchannel(self):
+ if self.w is not None:
+ self.w.close()
+ import wave
+ self.w = w = wave.open(open(self.filename, 'rb'), 'r')
+ self.w_params = (w.getnchannels(),
+ w.getsampwidth(),
+ w.getframerate())
+ chan, width, freq = self.w_params
+ self.dataleft = w.getnframes() * (chan*width)
+ self.sampledata = ''
+ def decode(self, mixer, bytecount):
+ result = self.sampledata
+ if not result and self.dataleft > 0:
+ # decode and convert some more data
+ chan, width, freq = self.w_params
+ #framecount = bytecount / (chan*width)
+ inputdata = self.w.readframes(bytecount) #(framecount)
+ self.dataleft -= len(inputdata)
+ result = mixer.resample(inputdata,
+ freq = freq,
+ bits = width * 8,
+ signed = width > 1,
+ channels = chan,
+ byteorder = 'little')
+ #print len(result)
+ self.sampledata = result[bytecount:]
+ return result[:bytecount]
diff --git a/display/pclient.py b/display/pclient.py
new file mode 100644
index 0000000..c6546ce
--- /dev/null
+++ b/display/pclient.py
@@ -0,0 +1,860 @@
+#! /usr/bin/env python
+
+import sys, os
+from socket import *
+from select import select
+import struct, zlib
+import time
+from common.msgstruct import *
+from common.pixmap import decodepixmap
+from common import hostchooser
+import modes
+from modes import KeyPressed, KeyReleased
+import caching
+
+#import psyco; psyco.full()
+
+# switch to udp_over_tcp if the udp socket didn't receive at least 60% of
+# the packets sent by the server
+UDP_EXPECTED_RATIO = 0.60
+
+
+def loadpixmap(dpy, data, colorkey=None):
+ w, h, data = decodepixmap(data)
+ if colorkey is None:
+ colorkey = -1
+ elif colorkey < 0:
+ r, g, b = struct.unpack("BBB", self.data[:3])
+ colorkey = b | (g<<8) | (r<<16)
+ return dpy.pixmap(w, h, data, colorkey)
+
+class Icon:
+ alpha = 255
+ def __init__(self, playfield):
+ self.playfield = playfield
+ self.size = 0, 0
+ def __getattr__(self, attr):
+ if attr == 'pixmap':
+ self.pixmap = self.playfield.getpixmap(self.bmpcode)
+ if hasattr(self.playfield.dpy, 'getopticon'):
+ ico = self.playfield.dpy.getopticon(
+ self.pixmap, self.originalrect, self.alpha)
+ if ico is not None:
+ self.pixmap = ico
+ self.rect = None
+ return self.pixmap
+ elif attr in ('bmpcode', 'rect'):
+ raise KeyError, attr
+ elif attr == 'originalrect':
+ self.originalrect = self.rect
+ return self.originalrect
+ raise AttributeError, attr
+ def clear(self):
+ if self.__dict__.has_key('pixmap'):
+ del self.pixmap
+
+class DataChunk(caching.Data):
+ SOURCEDIR = os.path.abspath(os.path.join(os.path.dirname(caching.__file__),
+ os.pardir))
+ CACHEDIR = os.path.join(SOURCEDIR, 'cache')
+ TOTAL = 0
+
+ def __init__(self, fileid):
+ caching.Data.__init__(self)
+ self.fileid = fileid
+ self.pending = []
+ self.progresshook = None
+
+ def server_md5(self, playfield, filename, position, length, checksum):
+ if not self.loadfrom(filename, position, length, checksum):
+ self.pending.append((0, position))
+ playfield.s.sendall(message(CMSG_DATA_REQUEST, self.fileid,
+ position, length))
+
+ def server_patch(self, position, data, lendata):
+ #print 'server_patch', self.fileid, position, len(data)
+ prev = DataChunk.TOTAL >> 10
+ DataChunk.TOTAL += lendata
+ total = DataChunk.TOTAL >> 10
+ if total != prev:
+ print "downloaded %dkb of data from server" % total
+ self.store(position, data)
+ try:
+ self.pending.remove((0, position))
+ except ValueError:
+ pass
+ else:
+ while self.pending and self.pending[0][0]:
+ callback = self.pending[0][1]
+ del self.pending[0]
+ callback(self)
+
+ def when_ready(self, callback):
+ if self.pending:
+ self.pending.append((1, callback))
+ else:
+ callback(self)
+
+
+class Playfield:
+ TASKBAR_HEIGHT = 48
+
+ def __init__(self, s, sockaddr):
+ self.s = s
+ self.sockaddr = sockaddr
+ try:
+ self.s.setsockopt(SOL_IP, IP_TOS, 0x10) # IPTOS_LOWDELAY
+ except error, e:
+ print >> sys.stderr, "Cannot set IPTOS_LOWDELAY:", str(e)
+ try:
+ self.s.setsockopt(SOL_TCP, TCP_NODELAY, 1)
+ except error, e:
+ print >> sys.stderr, "Cannot set TCP_NODELAY:", str(e)
+
+ initialbuf = ""
+ while 1:
+ t = self.s.recv(200)
+ if not t and not hasattr(self.s, 'RECV_CAN_RETURN_EMPTY'):
+ raise error, "connexion closed"
+ initialbuf += t
+ if len(initialbuf) >= len(MSG_WELCOME):
+ head = initialbuf[:len(MSG_WELCOME)]
+ tail = initialbuf[len(MSG_WELCOME):]
+ if head != MSG_WELCOME:
+ raise error, "connected to something not a game server"
+ if '\n' in tail:
+ break
+ n = tail.index('\n')
+ line2 = tail[:n]
+ self.initialbuf = tail[n+1:]
+
+ self.gameident = line2.strip()
+## self.datapath = None
+## if self.gameident.endswith(']'):
+## i = self.gameident.rfind('[')
+## if i >= 0:
+## self.gameident, self.datapath = (self.gameident[:i].strip(),
+## self.gameident[i+1:-1])
+ print "connected to %r." % self.gameident
+ self.s.sendall(message(CMSG_PROTO_VERSION, 3))
+
+ def setup(self, mode, udp_over_tcp):
+ self.playing = {} # 0, 1, or 'l' for local
+ self.keys = {}
+ self.keycodes = {}
+ self.last_key_event = (None, None)
+ self.dpy = None
+ self.snd = None
+ self.pixmaps = {} # {bmpcode: dpy_pixmap}
+ self.bitmaps = {} # {bmpcode: (fileid_or_data, colorkey)}
+ self.icons = {}
+ self.sounds = {}
+ self.currentmusic = None
+ self.fileids = {}
+ self.sprites = []
+ self.playingsounds = {}
+ self.playericons = {}
+ self.screenmode = mode
+ self.initlevel = 0
+ if mode[-1].has_key('udp_over_tcp'):
+ udp_over_tcp = mode[-1]['udp_over_tcp']
+ self.trackcfgmtime = None
+ if mode[-1].has_key('cfgfile'):
+ self.trackcfgfile = mode[-1]['cfgfile']
+ else:
+ self.trackcfgfile = os.path.join(DataChunk.SOURCEDIR,
+ 'http2', 'config.txt')
+ self.udpsock = None
+ self.udpsock_low = None
+ self.udpsock2 = None
+ self.accepted_broadcast = 0
+ self.tcpbytecounter = 0
+ self.udpbytecounter = 0
+ if udp_over_tcp == 1:
+ self.start_udp_over_tcp()
+ else:
+ self.pending_udp_data = None
+ if udp_over_tcp == 'auto':
+ self.udpsock_low = 0
+ self.dyndecompress = [[None, None, None, None] for i in range(8)]
+ self.dynnorepeat = None
+
+ def run(self, mode, udp_over_tcp='auto'):
+ self.setup(mode, udp_over_tcp)
+ try:
+ self.mainloop()
+ finally:
+ if self.dpy:
+ self.dpy.close()
+ try:
+ self.s.close()
+ except:
+ pass
+
+ def mainloop(self):
+ pss = hostchooser.serverside_ping()
+ self.initial_iwtd = [self.s, pss]
+ self.iwtd = self.initial_iwtd[:]
+ self.animdelay = 0.0
+ inbuf = self.process_inbuf(self.initialbuf)
+ self.initialbuf = ""
+ errors = 0
+ while 1:
+ if self.dpy:
+ self.processkeys()
+ iwtd, owtd, ewtd = select(self.iwtd, [], [], self.animdelay)
+ self.animdelay = 0.5
+ if self.dpy:
+ self.processkeys()
+ if self.s in iwtd:
+ inputdata = self.s.recv(0x6000)
+ self.tcpbytecounter += len(inputdata)
+ inbuf += inputdata
+ inbuf = self.process_inbuf(inbuf)
+ if self.dpy:
+ if self.udpsock in iwtd:
+ udpdata1 = None
+ while self.udpsock in iwtd:
+ try:
+ udpdata = self.udpsock.recv(65535)
+ except error, e:
+ print >> sys.stderr, e
+ errors += 1
+ if errors > 10:
+ raise
+ break
+ self.udpbytecounter += len(udpdata)
+ if len(udpdata) > 3 and '\x80' <= udpdata[0] < '\x90':
+ udpdata = self.dynamic_decompress(udpdata)
+ if udpdata is not None:
+ udpdata1 = udpdata
+ iwtd, owtd, ewtd = select(self.iwtd, [], [], 0)
+ if udpdata1 is not None:
+ self.update_sprites(udpdata1)
+ if self.udpsock2 in iwtd:
+ while self.udpsock2 in iwtd:
+ udpdata = self.udpsock2.recv(65535)
+ self.udpbytecounter += len(udpdata)
+ if udpdata == BROADCAST_MESSAGE:
+ if not self.accepted_broadcast:
+ self.s.sendall(message(CMSG_UDP_PORT, '*'))
+ self.accepted_broadcast = 1
+ #self.udpsock_low = None
+ udpdata = ''
+ iwtd, owtd, ewtd = select(self.iwtd, [], [], 0)
+ if udpdata and self.accepted_broadcast:
+ self.update_sprites(udpdata)
+ if self.pending_udp_data:
+ self.update_sprites(self.pending_udp_data)
+ self.pending_udp_data = ''
+ erasetb = self.taskbarmode and self.draw_taskbar()
+ d = self.dpy.flip()
+ if d:
+ self.animdelay = min(self.animdelay, d)
+ if self.snd:
+ d = self.snd.flop()
+ if d:
+ self.animdelay = min(self.animdelay, d)
+ if erasetb:
+ self.erase_taskbar(erasetb)
+ if pss in iwtd:
+ hostchooser.answer_ping(pss, self.gameident, self.sockaddr)
+
+ def process_inbuf(self, inbuf):
+ while inbuf:
+ values, inbuf = decodemessage(inbuf)
+ if not values:
+ break # incomplete message
+ fn = Playfield.MESSAGES.get(values[0], self.msg_unknown)
+ fn(self, *values[1:])
+ return inbuf
+
+ def dynamic_decompress(self, udpdata):
+ # Format of a UDP version 3 packet:
+ # header byte: 0x80 - 0x87 packet from thread 0 - 7
+ # or 0x88 - 0x8F reset packet from thread 0 - 7
+ # previous frame in same thread (1 byte)
+ # frame number (1 byte)
+ thread = self.dyndecompress[ord(udpdata[0]) & 7]
+ # thread==[decompress, lastframenumber, recompressed, lastframedata]
+ prevframe = udpdata[1]
+ thisframe = udpdata[2]
+ #print '---'
+ #for t in self.dyndecompress:
+ # print repr(t)[:120]
+ #print
+ #print `udpdata[:3]`
+
+ if udpdata[0] >= '\x88':
+ # reset
+ d = zlib.decompressobj().decompress
+ if prevframe != thisframe: # if not global sync point
+ # sync point from a previous frame
+ # find all threads with the same prevframe
+ threads = [t for t in self.dyndecompress if prevframe == t[1]]
+ if not threads:
+ return None # lost
+ # find a thread with already-recompressed data
+ for t in threads:
+ if t[2]:
+ data = t[3]
+ break
+ else:
+ # recompress and cache the prevframe data
+ t = threads[0]
+ data = t[3]
+ co = zlib.compressobj(6)
+ data = co.compress(data) + co.flush(zlib.Z_SYNC_FLUSH)
+ t[2] = 1
+ t[3] = data
+ d(data) # use it to initialize the state of the decompressobj
+ #print d
+ thread[0] = d
+ elif prevframe != thread[1]:
+ #print 'lost'
+ return None # lost
+ else:
+ d = thread[0]
+ # go forward in thread
+ try:
+ framedata = d(udpdata[3:])
+ #print d
+ thread[1] = thisframe
+ thread[2] = 0
+ thread[3] = framedata
+ if thisframe == self.dynnorepeat:
+ return None
+ self.dynnorepeat = thisframe
+ return framedata
+ except zlib.error:
+ #print 'crash'
+ return None
+
+ def geticon(self, icocode):
+ try:
+ return self.icons[icocode]
+ except KeyError:
+ ico = self.icons[icocode] = Icon(self)
+ return ico
+
+ def getpixmap(self, bmpcode):
+ try:
+ return self.pixmaps[bmpcode]
+ except KeyError:
+ data, colorkey = self.bitmaps[bmpcode]
+ if type(data) is type(''):
+ data = zlib.decompress(data)
+ else:
+ if data.pending:
+ raise KeyError
+ data = data.read()
+ pixmap = loadpixmap(self.dpy, data, colorkey)
+ self.pixmaps[bmpcode] = pixmap
+ return pixmap
+
+ def update_sprites(self, udpdata):
+ sprites = self.sprites
+ unpack = struct.unpack
+
+ currentsounds = {}
+ base = 0
+ while udpdata[base+4:base+6] == '\xFF\xFF':
+ key, lvol, rvol = struct.unpack("!hBB", udpdata[base:base+4])
+ try:
+ snd = self.sounds[key]
+ except KeyError:
+ pass # ignore sounds with bad code (probably not defined yet)
+ else:
+ n = self.playingsounds.get(key)
+ if n:
+ currentsounds[key] = n-1
+ elif self.snd:
+ self.snd.play(snd,
+ lvol / 255.0,
+ rvol / 255.0)
+ currentsounds[key] = 4
+ base += 6
+ self.playingsounds = currentsounds
+
+ for j in range(len(sprites)):
+ if sprites[j][0] != udpdata[base:base+6]:
+ removes = sprites[j:]
+ del sprites[j:]
+ removes.reverse()
+ eraser = self.dpy.putppm
+ for reserved, eraseargs in removes:
+ eraser(*eraseargs)
+ break
+ base += 6
+ #print "%d sprites redrawn" % (len(udpdata)/6-j)
+ try:
+ overlayer = self.dpy.overlayppm
+ except AttributeError:
+ getter = self.dpy.getppm
+ setter = self.dpy.putppm
+ for j in range(base, len(udpdata)-5, 6):
+ info = udpdata[j:j+6]
+ x, y, icocode = unpack("!hhh", info[:6])
+ try:
+ ico = self.icons[icocode]
+ sprites.append((info, (x, y, getter((x, y) + ico.size))))
+ setter(x, y, ico.pixmap, ico.rect)
+ except KeyError:
+ #print "bad ico code", icocode
+ pass # ignore sprites with bad ico (probably not defined yet)
+ else:
+ for j in range(base, len(udpdata)-5, 6):
+ info = udpdata[j:j+6]
+ x, y, icocode = unpack("!hhh", info[:6])
+ try:
+ ico = self.icons[icocode]
+ overlay = overlayer(x, y, ico.pixmap, ico.rect, ico.alpha)
+ sprites.append((info, overlay))
+ except KeyError:
+ #print "bad ico code", icocode
+ pass # ignore sprites with bad ico (probably not defined yet)
+
+ t0, n = self.painttimes
+ n = n + 1
+ if n == 50:
+ t = time.time()
+ t, t0 = t-t0, t
+ if t:
+ print "%.2f images per second, %.1f kbytes per second" % (
+ float(n)/t,
+ float(self.tcpbytecounter+self.udpbytecounter)/1024/t)
+ self.tcpbytecounter = -self.udpbytecounter
+ n = 0
+ self.painttimes = t0, n
+
+ def get_taskbar(self):
+ y0 = self.height - self.TASKBAR_HEIGHT
+ iconlist = []
+ f = 1.5 * time.time()
+ f = f-int(f)
+ pi = self.playericons.items()
+ pi.sort()
+ xpos = 0
+ for id, ico in pi:
+ if self.playing.get(id) != 'l':
+ w, h = ico.size
+ xpos += int(w * 5 / 3)
+ if not self.playing.get(id):
+ y = self.height - h
+ if self.keydefinition and id == self.keydefinition[0]:
+ num, icons = self.keys[self.nextkeyname()]
+ ico = icons[int(f*len(icons))-1]
+ y = y0 + int((self.TASKBAR_HEIGHT-ico.size[1])/2)
+ self.animdelay = 0.04
+ iconlist.append((xpos-w, y, ico, id))
+ pi.reverse()
+ f = f * (1.0-f) * 4.0
+ xpos = self.width
+ for id, ico in pi:
+ if self.playing.get(id) == 'l':
+ w, h = ico.size
+ xpos -= int(w * 5 / 3)
+ dy = self.TASKBAR_HEIGHT - h - 1
+ y = self.height - h - int(dy*f)
+ iconlist.append((xpos, y, ico, id))
+ self.animdelay = 0.04
+ return y0, iconlist
+
+ def clic_taskbar(self, (cx,cy)):
+ y0, icons = self.get_taskbar()
+ if cy >= y0:
+ for x, y, ico, id in icons:
+ if x <= cx < x+ico.size[0]:
+ return id
+ return None
+
+ def draw_taskbar(self):
+ y0, icons = self.get_taskbar()
+ rect = (0, y0, self.width, self.TASKBAR_HEIGHT)
+ bkgnd = self.dpy.getppm(rect)
+ self.dpy.taskbar(rect)
+ for x, y, ico, id in icons:
+ try:
+ self.dpy.putppm(x, y, ico.pixmap, ico.rect)
+ except KeyError:
+ pass
+ return y0, bkgnd
+
+ def erase_taskbar(self, (y0, bkgnd)):
+ self.dpy.putppm(0, y0, bkgnd)
+
+ def nextkeyname(self):
+ pid, df = self.keydefinition
+ undef = [(num, keyname) for keyname, (num, icons) in self.keys.items()
+ if not df.has_key(keyname) and icons]
+ if undef:
+ num, keyname = min(undef)
+ return keyname
+ else:
+ return None
+
+ def startplaying(self):
+ args = ()
+ if hasattr(self.s, 'udp_over_udp_mixer'):
+ # for SocketOverUdp: reuse the UDP address
+ port = self.s.getsockname()[1]
+ self.udpsock_low = None
+ self.s.udp_over_udp_decoder = self.udp_over_udp_decoder
+ self.start_udp_over_tcp()
+ elif self.pending_udp_data is not None:
+ port = MSG_INLINE_FRAME
+ else:
+ if '*udpsock*' in PORTS:
+ self.udpsock, (host, port) = PORTS['*udpsock*']
+ args = (host,)
+ else:
+ self.udpsock = socket(AF_INET, SOCK_DGRAM)
+ self.udpsock.bind(('', PORTS.get('CLIENT', INADDR_ANY)))
+ host, port = self.udpsock.getsockname()
+ # Send a dummy UDP message to the server. Some NATs will
+ # then let through the UDP messages from the server.
+ self.udpsock.sendto('.', self.s.getpeername())
+ self.iwtd.append(self.udpsock)
+ self.initial_iwtd.append(self.udpsock)
+ if 'sendudpto' in PORTS:
+ args = (PORTS['sendudpto'],)
+ outbound = []
+ outbound.append(message(CMSG_UDP_PORT, port, *args))
+ if self.snd and self.snd.has_music:
+ outbound.append(message(CMSG_ENABLE_MUSIC, 1))
+ outbound.append(message(CMSG_PING))
+ self.s.sendall(''.join(outbound))
+
+ def start_udp_over_tcp(self):
+ self.pending_udp_data = ''
+ self.udp_over_tcp_decompress = zlib.decompressobj().decompress
+ self.udpsock_low = None
+ for name in ('udpsock', 'udpsock2'):
+ sock = getattr(self, name)
+ if sock is not None:
+ try:
+ self.iwtd.remove(sock)
+ except ValueError:
+ pass
+ try:
+ self.initial_iwtd.remove(sock)
+ except ValueError:
+ pass
+ sock.close()
+ setattr(self, name, None)
+
+ def udp_over_udp_decoder(self, udpdata):
+ if len(udpdata) > 3 and '\x80' <= udpdata[0] < '\x90':
+ data = self.dynamic_decompress(udpdata)
+ if data:
+ self.pending_udp_data = data
+
+ def processkeys(self):
+ keyevents = self.dpy.keyevents()
+ if keyevents:
+ now = time.time()
+ pending = {}
+ for keysym, event in keyevents:
+ pending[keysym] = event
+ for keysym, event in pending.items():
+ code = self.keycodes.get((keysym, event))
+ if code and self.playing.get(code[0]) == 'l':
+ if (code == self.last_key_event[0] and
+ now - self.last_key_event[1] < 0.77):
+ continue # don't send too much events for auto-repeat
+ self.last_key_event = code, now
+ self.s.sendall(code[1])
+ elif self.keydefinition:
+ self.define_key(keysym)
+ pointermotion = self.dpy.pointermotion()
+ if pointermotion:
+ x, y = pointermotion
+ self.settaskbar(y >= self.height - 2*self.TASKBAR_HEIGHT)
+ mouseevents = self.dpy.mouseevents()
+ if mouseevents:
+ self.settaskbar(1)
+ self.keydefinition = None
+ for clic in mouseevents:
+ clic_id = self.clic_taskbar(clic)
+ if clic_id is not None:
+ if self.playing.get(clic_id) == 'l':
+ self.s.sendall(message(CMSG_REMOVE_PLAYER, clic_id))
+ else:
+ self.keydefinition = clic_id, {}
+ if self.taskbartimeout is not None and time.time() > self.taskbartimeout:
+ self.settaskbar(0)
+
+ def settaskbar(self, nmode):
+ self.taskbartimeout = None
+ if self.taskbarfree:
+ self.taskbarmode = (nmode or
+ 'l' not in self.playing.values() or
+ (self.keydefinition is not None))
+ if nmode:
+ self.taskbartimeout = time.time() + 5.0
+ if hasattr(self.dpy, 'settaskbar'):
+ self.dpy.settaskbar(self.taskbarmode)
+
+ def define_key(self, keysym):
+ clic_id, df = self.keydefinition
+ if keysym in df.values():
+ return
+ df[self.nextkeyname()] = keysym
+ if self.nextkeyname() is not None:
+ return
+ self.keydefinition = None
+ self.s.sendall(message(CMSG_ADD_PLAYER, clic_id))
+ for keyname, (num, icons) in self.keys.items():
+ if keyname[:1] == '-':
+ event = KeyReleased
+ keyname = keyname[1:]
+ else:
+ event = KeyPressed
+ if df.has_key(keyname):
+ keysym = df[keyname]
+ self.keycodes[keysym, event] = \
+ clic_id, message(CMSG_KEY, clic_id, num)
+
+ def msg_unknown(self, *rest):
+ print >> sys.stderr, "?"
+
+ def msg_player_join(self, id, local, *rest):
+ if local:
+ self.playing[id] = 'l'
+ self.settaskbar(0)
+ self.checkcfgfile(1)
+ else:
+ self.playing[id] = 1
+
+ def msg_player_kill(self, id, *rest):
+ self.playing[id] = 0
+ for key, (pid, msg) in self.keycodes.items():
+ if pid == id:
+ del self.keycodes[key]
+
+ def msg_broadcast_port(self, port):
+ if self.pending_udp_data is not None:
+ return
+ if self.udpsock2 is not None:
+ try:
+ self.iwtd.remove(self.udpsock2)
+ except ValueError:
+ pass
+ try:
+ self.initial_iwtd.remove(self.udpsock2)
+ except ValueError:
+ pass
+ self.udpsock2.close()
+ self.udpsock2 = None
+ self.accepted_broadcast = 0
+ try:
+ self.udpsock2 = socket(AF_INET, SOCK_DGRAM)
+ self.udpsock2.bind(('', port))
+ self.udpsock2.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
+ except error, e:
+ print "Cannot listen on the broadcast port %d" % port, str(e)
+ self.udpsock2 = None
+ else:
+ self.iwtd.append(self.udpsock2)
+ self.initial_iwtd.append(self.udpsock2)
+
+ def msg_def_playfield(self, width, height, backcolor=None,
+ gameident=None, *rest):
+ #if self.snd is not None:
+ # self.snd.close()
+ if self.dpy is not None:
+ # clear all pixmaps
+ for ico in self.icons.values():
+ ico.clear()
+ self.pixmaps.clear()
+ self.dpy.close()
+ del self.sprites[:]
+ self.width = width
+ self.height = height
+ if gameident:
+ self.gameident = gameident
+ self.dpy = modes.open_dpy(self.screenmode, width, height, self.gameident)
+ self.snd = self.snd or modes.open_snd(self.screenmode)
+ if self.snd:
+ self.s.sendall(message(CMSG_ENABLE_SOUND))
+ self.iwtd = self.dpy.selectlist() + self.initial_iwtd
+ self.dpy.clear() # backcolor is ignored
+ self.painttimes = (time.time(), 0)
+ self.s.sendall(message(CMSG_PING))
+ self.taskbarmode = 0
+ self.taskbarfree = 0
+ self.taskbartimeout = None
+ self.keydefinition = None
+
+ def msg_def_key(self, name, num, *icons):
+ self.keys[name] = num, [self.geticon(ico) for ico in icons]
+
+ def msg_def_icon(self, bmpcode, icocode, x, y, w, h, alpha=255, *rest):
+## if h<0:
+## try:
+## bitmap, height = self.flippedbitmaps[bmpcode]
+## except KeyError:
+## bitmap, height = self.dpy.vflipppm(self.bitmaps[bmpcode])
+## self.flippedbitmaps[bmpcode] = bitmap, height
+## y = height - y
+## h = - h
+## else:
+ ico = self.geticon(icocode)
+ ico.bmpcode = bmpcode
+ ico.rect = x, y, w, h
+ ico.size = w, h
+ if alpha < 255:
+ ico.alpha = alpha
+
+ def msg_def_bitmap(self, bmpcode, data, colorkey=None, *rest):
+ if type(data) is not type(''):
+ data = self.fileids[data]
+ self.bitmaps[bmpcode] = data, colorkey
+
+ def msg_def_sample(self, smpcode, data, *rest):
+ def ready(f, self=self, smpcode=smpcode):
+ if self.snd:
+ self.sounds[smpcode] = self.snd.sound(f)
+ f.clear()
+ if type(data) is type(''):
+ data = zlib.decompress(data)
+ f = DataChunk(None)
+ f.store(0, data)
+ ready(f)
+ else:
+ f = self.fileids[data]
+ f.when_ready(ready)
+
+ def msg_patch_file(self, fileid, position, data, lendata=None, *rest):
+ if self.fileids.has_key(fileid):
+ f = self.fileids[fileid]
+ else:
+ f = self.fileids[fileid] = DataChunk(fileid)
+ f.server_patch(position, data, lendata or len(data))
+
+ def msg_zpatch_file(self, fileid, position, data, *rest):
+ data1 = zlib.decompress(data)
+ self.msg_patch_file(fileid, position, data1, len(data), *rest)
+
+ def msg_md5_file(self, fileid, filename, position, length, checksum, *rest):
+ if self.fileids.has_key(fileid):
+ f = self.fileids[fileid]
+ else:
+ f = self.fileids[fileid] = DataChunk(fileid)
+ f.server_md5(self, filename, position, length, checksum)
+
+ def msg_play_music(self, loop_from, *codes):
+ codes = [self.fileids[c] for c in codes]
+ self.currentmusic = loop_from, codes, list(codes)
+ self.activate_music()
+
+ def activate_music(self, f=None):
+ loop_from, codes, checkcodes = self.currentmusic
+ if checkcodes:
+ checkcodes.pop().when_ready(self.activate_music)
+ elif self.snd:
+ self.snd.play_musics(codes, loop_from)
+
+ def msg_fadeout(self, time, *rest):
+ if self.snd:
+ self.snd.fadeout(time)
+
+ def msg_player_icon(self, pid, icocode, *rest):
+ self.playericons[pid] = self.geticon(icocode)
+
+ def checkcfgfile(self, force=0):
+ if self.trackcfgfile:
+ try:
+ st = os.stat(self.trackcfgfile)
+ except OSError:
+ pass
+ else:
+ if force or (st.st_mtime != self.trackcfgmtime):
+ self.trackcfgmtime = st.st_mtime
+ try:
+ f = open(self.trackcfgfile, 'r')
+ data = f.read().strip()
+ f.close()
+ d = eval(data or '{}', {}, {})
+ except:
+ print >> sys.stderr, 'Invalid config file format'
+ else:
+ d = d.get(gethostname(), {})
+ namemsg = ''
+ for id, local in self.playing.items():
+ keyid = 'player%d' % id
+ if local == 'l' and d.has_key(keyid):
+ namemsg = namemsg + message(
+ CMSG_PLAYER_NAME, id, d[keyid])
+ if namemsg:
+ self.s.sendall(namemsg)
+
+ def msg_ping(self, *rest):
+ self.s.sendall(message(CMSG_PONG, *rest))
+ self.checkcfgfile()
+ if rest and self.udpsock_low is not None:
+ udpkbytes = rest[0]
+ if not udpkbytes:
+ return
+ #inp = self.udpbytecounter / (udpkbytes*1024.0)
+ #print "(%d%% packet loss)" % int(100*(1.0-inp))
+ if (udpkbytes<<10) * UDP_EXPECTED_RATIO > self.udpbytecounter:
+ # too many packets were dropped (including, maybe, all of them)
+ self.udpsock_low += 1
+ if self.udpsock_low >= 3 and self.initlevel >= 1:
+ # third time now -- that's too much
+ print "Note: routing UDP traffic over TCP",
+ inp = self.udpbytecounter / (udpkbytes*1024.0)
+ print "(%d%% packet loss)" % int(100*(1.0-inp))
+ self.start_udp_over_tcp()
+ self.s.sendall(message(CMSG_UDP_PORT, MSG_INLINE_FRAME))
+ else:
+ # enough packets received
+ self.udpsock_low = 0
+
+ def msg_pong(self, *rest):
+ if self.initlevel == 0:
+ self.startplaying()
+ self.initlevel = 1
+ elif self.initlevel == 1:
+ if self.snd and self.snd.has_music:
+ self.s.sendall(message(CMSG_ENABLE_MUSIC, 2))
+ self.initlevel = 2
+ if not self.taskbarfree and not self.taskbarmode:
+ self.taskbarfree = 1
+ self.settaskbar(1)
+
+ def msg_inline_frame(self, data, *rest):
+ if self.pending_udp_data is not None:
+ self.pending_udp_data = self.udp_over_tcp_decompress(data)
+
+ MESSAGES = {
+ MSG_BROADCAST_PORT:msg_broadcast_port,
+ MSG_DEF_PLAYFIELD: msg_def_playfield,
+ MSG_DEF_KEY : msg_def_key,
+ MSG_DEF_ICON : msg_def_icon,
+ MSG_DEF_BITMAP : msg_def_bitmap,
+ MSG_DEF_SAMPLE : msg_def_sample,
+ MSG_PLAY_MUSIC : msg_play_music,
+ MSG_FADEOUT : msg_fadeout,
+ MSG_PLAYER_JOIN : msg_player_join,
+ MSG_PLAYER_KILL : msg_player_kill,
+ MSG_PLAYER_ICON : msg_player_icon,
+ MSG_PING : msg_ping,
+ MSG_PONG : msg_pong,
+ MSG_INLINE_FRAME : msg_inline_frame,
+ MSG_PATCH_FILE : msg_patch_file,
+ MSG_ZPATCH_FILE : msg_zpatch_file,
+ MSG_MD5_FILE : msg_md5_file,
+## MSG_LOAD_PREFIX : msg_load_prefix,
+ }
+
+
+def run(s, sockaddr, *args, **kw):
+ try:
+ import psyco
+ except ImportError:
+ pass
+ else:
+ psyco.bind(Playfield.update_sprites)
+ Playfield(s, sockaddr).run(*args, **kw)
diff --git a/display/playback.py b/display/playback.py
new file mode 100644
index 0000000..f0e698b
--- /dev/null
+++ b/display/playback.py
@@ -0,0 +1,203 @@
+#! /usr/bin/env python
+
+import sys, os, gzip
+from socket import *
+from select import select
+import cStringIO, struct, zlib
+import time
+sys.path.insert(0, os.pardir)
+from common.msgstruct import *
+from common import hostchooser
+import modes
+from modes import KeyPressed, KeyReleased
+
+#import psyco; psyco.full()
+
+SOURCEDIR = os.pardir
+
+
+def loadpixmap(dpy, data, colorkey=None):
+ f = cStringIO.StringIO(data)
+ sig = f.readline().strip()
+ assert sig == "P6"
+ while 1:
+ line = f.readline().strip()
+ if not line.startswith('#'):
+ break
+ wh = line.split()
+ w, h = map(int, wh)
+ sig = f.readline().strip()
+ assert sig == "255"
+ data = f.read()
+ f.close()
+ if colorkey is None:
+ colorkey = -1
+ elif colorkey < 0:
+ r, g, b = struct.unpack("BBB", self.data[:3])
+ colorkey = b | (g<<8) | (r<<16)
+ return dpy.pixmap(w, h, data, colorkey)
+
+class Icon:
+ def __init__(self, bitmap, (x, y, w, h), alpha):
+ self.rect = x, y, w, h
+ self.size = w, h
+ self.bitmap = bitmap
+ self.alpha = alpha
+
+
+class Playback:
+ gameident = 'Playback'
+
+ def __init__(self, filename, mode=('x', 'off', {})):
+ f = gzip.open(filename, 'rb')
+ self.screenmode = mode
+ self.width = None
+ self.deffiles = {}
+ self.defbitmaps = {}
+ self.deficons = {}
+ self.icons = {}
+ self.frames = []
+ inbuf = ''
+ while 1:
+ values, inbuf = decodemessage(inbuf)
+ if not values:
+ # incomplete message
+ data = f.read(8192)
+ if not data:
+ break
+ inbuf += data
+ else:
+ #print values[0],
+ fn = Playback.MESSAGES.get(values[0], self.msg_unknown)
+ fn(self, *values[1:])
+ print '%d frames in file.' % len(self.frames)
+ f.close()
+ assert self.width, "no playfield definition found in file"
+
+ self.dpy = modes.open_dpy(self.screenmode,
+ self.width, self.height, self.gameident)
+ self.dpy.clear() # backcolor is ignored
+ self.sprites = []
+ self.buildicons()
+ self.go(0)
+
+ def buildicons(self):
+ bitmaps = {}
+ for bmpcode, (data, colorkey) in self.defbitmaps.items():
+ if isinstance(data, str):
+ data = zlib.decompress(data)
+ else:
+ data = self.deffiles[data]
+ bitmaps[bmpcode] = loadpixmap(self.dpy, data, colorkey)
+ for icocode, (bmpcode, rect, alpha) in self.deficons.items():
+ self.icons[icocode] = Icon(bitmaps[bmpcode], rect, alpha)
+
+ def go(self, n):
+ self.n = n
+ self.update_sprites(self.frames[n])
+ self.dpy.flip()
+
+ def save(self, filename=None):
+ "shm only!"
+ w, h, data, reserved = self.dpy.getppm((0, 0, self.width, self.height))
+ f = open(filename or ('frame%d.ppm' % self.n), 'wb')
+ print >> f, 'P6'
+ print >> f, w, h
+ print >> f, 255
+ for i in range(0, len(data), 4):
+ f.write(data[i+2]+data[i+1]+data[i])
+ f.close()
+
+ def update_sprites(self, udpdata):
+ sprites = self.sprites
+ unpack = struct.unpack
+ base = 0
+ for j in range(len(sprites)):
+ if sprites[j][0] != udpdata[base:base+6]:
+ removes = sprites[j:]
+ del sprites[j:]
+ removes.reverse()
+ eraser = self.dpy.putppm
+ for reserved, eraseargs in removes:
+ eraser(*eraseargs)
+ break
+ base += 6
+ try:
+ overlayer = self.dpy.overlayppm
+ except AttributeError:
+ getter = self.dpy.getppm
+ setter = self.dpy.putppm
+ #print "%d sprites redrawn" % (len(udpdata)/6-j)
+ for j in range(base, len(udpdata)-5, 6):
+ info = udpdata[j:j+6]
+ x, y, icocode = unpack("!hhh", info[:6])
+ try:
+ ico = self.icons[icocode]
+ sprites.append((info, (x, y, getter((x, y) + ico.size))))
+ setter(x, y, ico.bitmap, ico.rect)
+ except KeyError:
+ #print "bad ico code", icocode
+ pass # ignore sprites with bad ico (probably not defined yet)
+ else:
+ for j in range(base, len(udpdata)-5, 6):
+ info = udpdata[j:j+6]
+ x, y, icocode = unpack("!hhh", info[:6])
+ try:
+ ico = self.icons[icocode]
+ overlay = overlayer(x, y, ico.bitmap, ico.rect, ico.alpha)
+ sprites.append((info, overlay))
+ except KeyError:
+ #print "bad ico code", icocode
+ pass # ignore sprites with bad ico (probably not defined yet)
+
+ def msg_unknown(self, *rest):
+ pass
+
+ def msg_patch_file(self, fileid, position, data, lendata=None, *rest):
+ try:
+ s = self.deffiles[fileid]
+ except KeyError:
+ s = ''
+ if len(s) < position:
+ s += '\x00' * (position-len(s))
+ s = s[:position] + data + s[position+len(s):]
+ self.deffiles[fileid] = s
+
+ def msg_zpatch_file(self, fileid, position, data, *rest):
+ data1 = zlib.decompress(data)
+ self.msg_patch_file(fileid, position, data1, len(data), *rest)
+
+ def msg_md5_file(self, fileid, filename, position, length, checksum, *rest):
+ fn = os.path.join(SOURCEDIR, filename)
+ f = open(fn, 'rb')
+ f.seek(position)
+ data = f.read(length)
+ f.close()
+ assert len(data) == length
+ self.msg_patch_file(fileid, position, data)
+
+ def msg_def_playfield(self, width, height, *rest):
+ self.width, self.height = width, height
+
+ def msg_def_icon(self, bmpcode, icocode, x, y, w, h, alpha=255, *rest):
+ self.deficons[icocode] = bmpcode, (x, y, w, h), alpha
+
+ def msg_def_bitmap(self, bmpcode, data, colorkey=None, *rest):
+ self.defbitmaps[bmpcode] = data, colorkey
+
+ def msg_recorded(self, data):
+ self.frames.append(data)
+
+ MESSAGES = {
+ MSG_PATCH_FILE : msg_patch_file,
+ MSG_ZPATCH_FILE : msg_zpatch_file,
+ MSG_MD5_FILE : msg_md5_file,
+ MSG_DEF_PLAYFIELD: msg_def_playfield,
+ MSG_DEF_ICON : msg_def_icon,
+ MSG_DEF_BITMAP : msg_def_bitmap,
+ MSG_RECORDED : msg_recorded,
+ }
+
+
+if __name__ == '__main__' and len(sys.argv) > 1:
+ p = Playback(sys.argv[1])
diff --git a/display/puremixer.py b/display/puremixer.py
new file mode 100644
index 0000000..d0b567a
--- /dev/null
+++ b/display/puremixer.py
@@ -0,0 +1,123 @@
+import sys, audioop
+
+
+class PureMixer:
+ #
+ # An audio mixer in Python based on audioop
+ #
+ # Note that opening the audio device itself is outside the scope of
+ # this module. Anything else could also be done with the mixed data,
+ # e.g. stored on disk, for all this module knows.
+
+ def __init__(self, freq=44100, bits=8, signed=0,
+ channels=1, byteorder=None):
+ """Open the mixer and set its parameters."""
+ self.freq = freq
+ self.bytes = bits/8
+ self.signed = signed
+ self.channels = channels
+ self.byteorder = byteorder or sys.byteorder
+ self.parameters = (freq, self.bytes, signed, channels, self.byteorder)
+ self.bytespersample = channels*self.bytes
+ self.queue = '\x00' * self.bytes
+
+ def resample(self, data, freq=44100, bits=8, signed=0,
+ channels=1, byteorder=None):
+ "Convert a sample to the mixer's own format."
+ bytes = bits/8
+ byteorder = byteorder or sys.byteorder
+ if (freq, bytes, signed, channels, byteorder) == self.parameters:
+ return data
+ # convert to native endianness
+ if byteorder != sys.byteorder:
+ data = byteswap(data, bytes)
+ byteorder = sys.byteorder
+ # convert unsigned -> signed for the next operations
+ if not signed:
+ data = audioop.bias(data, bytes, -(1<<(bytes*8-1)))
+ signed = 1
+ # convert stereo -> mono
+ while channels > self.channels:
+ assert channels % 2 == 0
+ data = audioop.tomono(data, bytes, 0.5, 0.5)
+ channels /= 2
+ # resample to self.freq
+ if freq != self.freq:
+ data, ignored = audioop.ratecv(data, bytes, channels,
+ freq, self.freq, None)
+ freq = self.freq
+ # convert between 8bits and 16bits
+ if bytes != self.bytes:
+ data = audioop.lin2lin(data, bytes, self.bytes)
+ bytes = self.bytes
+ # convert mono -> stereo
+ while channels < self.channels:
+ data = audioop.tostereo(data, bytes, 1.0, 1.0)
+ channels *= 2
+ # convert signed -> unsigned
+ if not self.signed:
+ data = audioop.bias(data, bytes, 1<<(bytes*8-1))
+ signed = 0
+ # convert to mixer endianness
+ if byteorder != self.byteorder:
+ data = byteswap(data, bytes)
+ byteorder = self.byteorder
+ # done
+ if (freq, bytes, signed, channels, byteorder) != self.parameters:
+ raise ValueError, 'sound sample conversion failed'
+ return data
+
+ def wavesample(self, file):
+ "Read a sample from a .wav file (or file-like object)."
+ import wave
+ w = wave.open(file, 'r')
+ return self.resample(w.readframes(w.getnframes()),
+ freq = w.getframerate(),
+ bits = w.getsampwidth() * 8,
+ signed = w.getsampwidth() > 1,
+ channels = w.getnchannels(),
+ byteorder = 'little')
+
+ def mix(self, mixer_channels, bufsize):
+ """Mix the next batch buffer.
+ Each object in the mixer_channels list must be a file-like object
+ with a 'read(size)' method."""
+ data = ''
+ already_seen = {}
+ channels = mixer_channels[:]
+ channels.reverse()
+ for c in channels:
+ if already_seen.has_key(c):
+ data1 = ''
+ else:
+ data1 = c.read(bufsize)
+ already_seen[c] = 1
+ if data1:
+ l = min(len(data), len(data1))
+ data = (audioop.add(data[:l], data1[:l], 1) +
+ (data1[l:] or data[l:]))
+ else:
+ try:
+ mixer_channels.remove(c)
+ except ValueError:
+ pass
+ data += self.queue * ((bufsize - len(data)) / self.bytes)
+ self.queue = data[-self.bytes:]
+ return data
+
+
+def byteswap(data, byte):
+ if byte == 1:
+ return
+ if byte == 2:
+ typecode = 'h'
+ elif byte == 4:
+ typecode = 'i'
+ else:
+ raise ValueError, 'cannot convert endianness for samples of %d bytes' % byte
+ import array
+ a = array.array(typecode, data)
+ if a.itemsize != byte:
+ raise ValueError, 'endianness convertion failed'
+ a.byteswap()
+ return a.tostring()
diff --git a/display/pythonxlibintf.py b/display/pythonxlibintf.py
new file mode 100644
index 0000000..5e32715
--- /dev/null
+++ b/display/pythonxlibintf.py
@@ -0,0 +1,202 @@
+
+ ################################################
+## pygame-based implementation of xshm ##
+################################################
+
+import os, sys
+from Xlib import X, display
+
+
+# -*-*- SLOOWWW -*-*-
+import psyco; psyco.full()
+
+
+class Display:
+
+ def __init__(self, width, height, title):
+ self.dpy = display.Display()
+ self.default_scr = self.dpy.screen()
+ self.root = self.default_scr.root
+ self.width = width
+ self.height = height
+ self.depth = self.default_scr.root_depth
+
+ self.backpixmap = self.root.create_pixmap(width, height, self.depth)
+ self.win = self.root.create_window(
+ 0, 0, width, height, 0, self.depth,
+ override_redirec = 0,
+ background_pixel = self.default_scr.black_pixel,
+ backing_store = X.NotUseful,
+ )
+ self.win.map()
+
+ self.gc = self.win.create_gc()
+ self.gc_and = self.win.create_gc()
+ self.gc_or = self.win.create_gc()
+
+ self.gc.change(foreground = self.default_scr.black_pixel)
+ self.gc_and.change(function = X.GXand)
+ self.gc_or .change(function = X.GXor)
+
+ self.selectinput = 0
+ self.keyev = []
+ self.mouseev = []
+ self.motionev = None
+ self.dpy.flush()
+
+ pixel = "\x00\x00\x80"
+ hole = "\x01\x01\x01"
+ self.taskbkgnd = self.pixmap(32, 32,
+ ((pixel+hole)*16 + (hole+pixel)*16) * 16,
+ 0x010101)
+
+ def pixmap(self, w, h, data, colorkey=-1):
+ print >> sys.stderr, '.',
+ extent = w*h
+ depth = self.depth
+ if depth >= 24:
+ bitmap_pad = 32
+ else:
+ bitmap_pad = 16
+ scanline = ((w+bitmap_pad-1) & ~(bitmap_pad-1)) / 8;
+ if colorkey >= 0:
+ key = (chr(colorkey >> 16) +
+ chr((colorkey>>8) & 0xFF) +
+ chr(colorkey & 0xFF))
+ else:
+ key = None
+ if depth == 15:
+ p_size = 5, 5, 5
+ elif depth == 16:
+ p_size = 5, 6, 5
+ elif depth == 24 or depth == 32:
+ p_size = 8, 8, 8
+ else:
+ raise ValueError, 'unsupported screen depth %d' % depth
+
+ imgdata = []
+ maskdata = []
+
+ for color in range(3):
+ plane = 128
+ while plane >= (1<<(8-p_size[color])):
+ src = 0
+ for y in range(h):
+ imgline = 0L
+ maskline = 0L
+ shifter = 1L
+ for x in range(w):
+ if data[src:src+3] == key:
+ # transparent
+ maskline |= shifter
+ elif ord(data[src+color]) & plane:
+ imgline |= shifter
+ shifter <<= 1
+ src += 3
+ imgdata.append(long2string(imgline, scanline))
+ maskdata.append(long2string(maskline, scanline))
+ plane /= 2
+
+ imgdata = ''.join(imgdata)
+ if colorkey >= 0:
+ maskdata = ''.join(maskdata)
+ mask = self.win.create_pixmap(w, h, depth)
+ mask.put_image(self.gc, 0, 0, w, h, X.XYPixmap, depth, 0, maskdata)
+ else:
+ mask = None
+ imgdata = ''.join(imgdata)
+ image = self.win.create_pixmap(w, h, depth)
+ image.put_image(self.gc, 0, 0, w, h, X.XYPixmap, depth, 0, imgdata)
+ image.mask = mask
+ image.size = w, h
+ return image
+
+ def getppm(self, (x, y, w, h), bkgnd=None):
+ if bkgnd is None:
+ bkgnd = self.win.create_pixmap(w, h, self.depth)
+ bkgnd.mask = None
+ bkgnd.size = w, h
+ bkgnd.copy_area(self.gc, self.backpixmap, x, y, w, h, 0, 0)
+ return bkgnd
+
+ def putppm(self, x, y, image, rect=None):
+ if rect:
+ x1, y1, w1, h1 = rect
+ else:
+ x1 = y1 = 0
+ w1, h1 = image.size
+ if image.mask is None:
+ self.backpixmap.copy_area(self.gc, image, x1, y1, w1, h1, x, y)
+ else:
+ self.backpixmap.copy_area(self.gc_and, image.mask,
+ x1, y1, w1, h1, x, y)
+ self.backpixmap.copy_area(self.gc_or, image,
+ x1, y1, w1, h1, x, y)
+
+ def flip(self):
+ self.win.copy_area(self.gc, self.backpixmap,
+ 0, 0, self.width, self.height, 0, 0)
+ self.dpy.flush()
+ self.readXevents()
+
+ def close(self):
+ self.dpy.close()
+
+ def clear(self):
+ self.backpixmap.fill_rectangle(self.gc, 0, 0, self.width, self.height)
+
+ def readXevents(self):
+ n = self.dpy.pending_events()
+ if n:
+ for i in range(n):
+ event = self.dpy.next_event()
+ if event.type == X.KeyPress or event.type == X.KeyRelease:
+ self.keyev.append((event.detail, event.type))
+ elif event.type == X.ButtonPress:
+ self.mouseev.append((event.event_x, event.event_y))
+ elif event.type == X.MotionNotify:
+ self.motionev = event.event_x, event.event_y
+ elif event.type == X.DestroyNotify:
+ raise SystemExit
+ self.readXevents()
+
+ def enable_event(self, mask):
+ self.selectinput |= mask
+ self.win.change_attributes(event_mask=self.selectinput)
+
+ def keyevents(self):
+ if not (self.selectinput & X.KeyReleaseMask):
+ self.enable_event(X.KeyPressMask | X.KeyReleaseMask)
+ self.readXevents()
+ result = self.keyev
+ self.keyev = []
+ return result
+
+ def mouseevents(self):
+ if not (self.selectinput & X.ButtonPressMask):
+ self.enable_event(X.ButtonPressMask)
+ result = self.mouseev
+ self.mouseev = []
+ return result
+
+ def pointermotion(self):
+ result = self.motionev
+ self.motionev = None
+ return result
+
+ def has_sound(self):
+ return 0
+
+ def selectlist(self):
+ from socket import fromfd, AF_INET, SOCK_STREAM
+ return [fromfd(self.dpy.fileno(), AF_INET, SOCK_STREAM)]
+
+ def taskbar(self, (x, y, w, h)):
+ for j in range(y, y+h, 32):
+ for i in range(x, x+w, 32):
+ self.putppm(i, j, self.taskbkgnd,
+ (0, 0, x+w-i, y+h-j))
+
+
+def long2string(bits, strlen):
+ return ''.join([chr((bits>>n)&0xFF) for n in range(0, 8*strlen, 8)])
diff --git a/display/setup.py b/display/setup.py
new file mode 100755
index 0000000..4776e79
--- /dev/null
+++ b/display/setup.py
@@ -0,0 +1,16 @@
+#! /usr/bin/env python
+
+from distutils.core import setup
+from distutils.extension import Extension
+
+setup ( name="xshm",
+ version="0.2",
+ description="X window system Shared Memory extension",
+ author="Armin & Odie",
+ author_email="arigo@tunes.org",
+ ext_modules=[Extension(name = 'xshm',
+ sources = ['xshm.c'],
+ include_dirs = ['/usr/X11R6/include'],
+ library_dirs = ['/usr/X11R6/lib'],
+ libraries = ['X11', 'Xext'])]
+ )
diff --git a/display/snd_linux.py b/display/snd_linux.py
new file mode 100644
index 0000000..dae82e0
--- /dev/null
+++ b/display/snd_linux.py
@@ -0,0 +1,148 @@
+import sys
+from cStringIO import StringIO
+import puremixer
+from music1 import Music
+
+
+class Sound:
+ # Mono only
+ has_sound = has_music = 0 # until initialized
+
+ BUFFERTIME = 0.09
+ FLOPTIME = 0.07
+
+ Formats = [
+ ('U8', 8, 0, None),
+ ('S8', 8, 1, None),
+ ('S16_NE', 16, 1, None),
+ ('S16_LE', 16, 1, 'little'),
+ ('S16_BE', 16, 1, 'big'),
+ ('U16_LE', 16, 0, 'little'),
+ ('U16_BE', 16, 0, 'big'),
+ ]
+
+ def __init__(self, freq=44100, fmt='S16_NE'):
+ self.f = None
+ self.freq = int(freq)
+ self.format = fmt.upper()
+ self.params = p = {}
+ for name, p['bits'], p['signed'], p['byteorder'] in self.Formats:
+ if name == self.format:
+ break
+ else:
+ print >> sys.stderr, 'available sound formats:'
+ for name, bits, signed, byteorder in self.Formats:
+ print >> sys.stderr, ' %-8s %s' % (name, nicefmttext(
+ bits, signed, byteorder))
+ sys.exit(2)
+
+ import linuxaudiodev
+ try:
+ f = linuxaudiodev.open('w')
+ f.setparameters(self.freq, p['bits'], 1,
+ getattr(linuxaudiodev, 'AFMT_' + self.format))
+ except Exception, e:
+ print >> sys.stderr, "sound disabled: %s: %s" % (
+ e.__class__.__name__, e)
+ return
+ self.f = f
+ self.mixer = mixer = puremixer.PureMixer(**p)
+ buffertime = self.BUFFERTIME
+ self.bufsize = int(mixer.bytespersample*mixer.freq*buffertime +
+ 255.5) & ~ 255
+ if self.bufsize > f.bufsize():
+ self.bufsize = f.bufsize()
+ buffertime = self.bufsize / float(freq)
+ self.buffertime = buffertime
+ self.mixer_channels = []
+ self.mixer_accum = {}
+ self.has_sound = 1
+ self.has_music = 1
+
+ def close(self):
+ self.f.close()
+ self.f = None
+
+ def sound(self, f):
+ return self.mixer.wavesample(f.fopen())
+
+ def flop(self):
+ self.mixer_accum = {}
+ if self.f is None:
+ return
+ for i in range(3):
+ bufsize = self.bufsize - self.f.obufcount()
+ if bufsize <= 0:
+ break
+ self.f.write(self.mixer.mix(self.mixer_channels, bufsize))
+ #cnt = getattr(self, 'CNT', 0)
+ #import time
+ #print cnt, time.time()
+ #self.CNT = cnt+1
+ return self.FLOPTIME
+
+ def play(self, sound, lvolume, rvolume):
+ # volume ignored
+ if sound not in self.mixer_accum:
+ self.mixer_channels.append(StringIO(sound))
+ self.mixer_accum[sound] = 1
+
+ def play_musics(self, musics, loop_from):
+ self.cmusics = musics, loop_from, -1
+ if self.mixer_channels[:1] != [self]:
+ self.mixer_channels.insert(0, self)
+
+ def read(self, size):
+ "Provide some more data to self.mixer.poll()."
+ musics, loop_from, c = self.cmusics
+ if c < 0:
+ data = ''
+ else:
+ data = musics[c].mixed.decode(self.mixer, size)
+ if not data:
+ c += 1
+ if c >= len(musics): # end
+ c = loop_from
+ if c >= len(musics):
+ return ''
+ self.cmusics = musics, loop_from, c
+ try:
+ mixed = musics[c].mixed
+ except AttributeError:
+ mixed = musics[c].mixed = Music(musics[c].freezefilename())
+ mixed.openchannel()
+ data = mixed.decode(self.mixer, size)
+ if 0 < len(data) < size:
+ data += self.read(size - len(data))
+ return data
+
+ def fadeout(self, millisec):
+ self.cmusics = [], 0, -1
+
+
+def imperror():
+ try:
+ import linuxaudiodev
+ except ImportError:
+ if sys.platform.startswith('linux'):
+ return 'linuxaudiodev module not installed'
+ else:
+ return 'only available on Linux'
+
+def nicefmttext(bits, signed, byteorder):
+ s = '%s %d bits' % (signed and 'signed' or 'unsigned', bits)
+ if byteorder:
+ s += ' %s endian' % byteorder
+ return s
+
+def htmloptionstext(nameval):
+ import modes
+ l = ['<font size=-1>Sampling <%s>' % nameval('select', 'fmt')]
+ for name, bits, signed, byteorder in Sound.Formats:
+ l.append('<'+nameval('option', 'fmt', name, default='S16_NE')+'>'+
+ nicefmttext(bits, signed, byteorder))
+ l+= ['</select> rate ',
+ '<%s size=5>Hz</font>' % nameval('text', 'freq', default='44100'),
+ '<br>',
+ modes.musichtmloptiontext(nameval)]
+ return '\n'.join(l)
diff --git a/display/snd_off.py b/display/snd_off.py
new file mode 100644
index 0000000..9d2e640
--- /dev/null
+++ b/display/snd_off.py
@@ -0,0 +1,5 @@
+class Sound:
+ has_sound = 0 # no sound
+ has_music = 0 # no music
+ def __init__(self):
+ pass
diff --git a/display/snd_pygame.py b/display/snd_pygame.py
new file mode 100644
index 0000000..e1a1455
--- /dev/null
+++ b/display/snd_pygame.py
@@ -0,0 +1,82 @@
+import sys
+from modes import musichtmloptiontext as htmloptionstext
+from pygame.locals import *
+import pygame.mixer
+
+if pygame.mixer is None:
+ raise ImportError
+
+
+#ENDMUSICEVENT = USEREVENT
+
+
+class Sound:
+ has_sound = has_music = 0 # until initialized
+
+ def __init__(self):
+ try:
+ pygame.mixer.init()
+ except pygame.error, e:
+ print >> sys.stderr, "sound disabled: %s" % str(e)
+ else:
+ self.has_sound = 1
+ try:
+ from pygame.mixer import music
+ except ImportError:
+ pass
+ else:
+ self.has_music = music is not None
+ self.cmusics = None
+
+ def close(self):
+ try:
+ pygame.mixer.stop()
+ except pygame.error:
+ pass
+ if self.has_music:
+ try:
+ pygame.mixer.music.stop()
+ except pygame.error:
+ pass
+
+ def sound(self, f):
+ return pygame.mixer.Sound(f.freezefilename())
+
+ def flop(self):
+ # the events are not processed if pygame is not also the display,
+ # so ENDMUSICEVENT will not arrive -- poll for end of music
+ if self.cmusics and not pygame.mixer.music.get_busy():
+ self.next_music()
+
+ def play(self, sound, lvolume, rvolume):
+ channel = pygame.mixer.find_channel(1)
+ channel.stop()
+ try:
+ channel.set_volume(lvolume, rvolume)
+ except TypeError:
+ channel.set_volume(0.5 * (lvolume+rvolume))
+ channel.play(sound)
+
+ def play_musics(self, musics, loop_from):
+ #dpy_pygame.EVENT_HANDLERS[ENDMUSICEVENT] = self.next_music
+ #pygame.mixer.music.set_endevent(ENDMUSICEVENT)
+ self.cmusics = musics, loop_from, 0
+ self.next_music()
+
+ def next_music(self, e=None):
+ if self.cmusics:
+ musics, loop_from, c = self.cmusics
+ if c >= len(musics): # end
+ c = loop_from
+ if c >= len(musics):
+ pygame.mixer.music.stop()
+ self.cmusics = None
+ return
+ pygame.mixer.music.load(musics[c].freezefilename())
+ pygame.mixer.music.play()
+ self.cmusics = musics, loop_from, c+1
+
+ def fadeout(self, millisec):
+ #print "fadeout:", millisec
+ pygame.mixer.music.fadeout(millisec)
+ self.cmusics = None
diff --git a/display/snd_windows.py b/display/snd_windows.py
new file mode 100644
index 0000000..3aea3bd
--- /dev/null
+++ b/display/snd_windows.py
@@ -0,0 +1,93 @@
+import sys
+from cStringIO import StringIO
+import puremixer
+import wingame
+from music1 import Music
+
+
+class Sound:
+ # Mono only
+ has_sound = has_music = 0 # until initialized
+
+ BUFFERTIME = 0.09
+ FLOPTIME = 0.07
+
+ def __init__(self, freq=44100, bits=16):
+ self.freq = int(freq)
+ self.bits = int(bits)
+ self.bufsize = (int(self.BUFFERTIME*self.freq*self.bits/8) + 64) & ~63
+
+ try:
+ self.audio = wingame.Audio(1, self.freq, self.bits, self.bufsize)
+ except Exception, e:
+ print >> sys.stderr, "sound disabled: %s: %s" % (
+ e.__class__.__name__, e)
+ return
+ self.mixer = puremixer.PureMixer(self.freq, self.bits, self.bits==16,
+ byteorder='little')
+ self.mixer_channels = []
+ self.mixer_accum = {}
+ self.has_sound = 1
+ self.has_music = 1
+
+ def stop(self):
+ self.audio.close()
+
+ def sound(self, f):
+ return self.mixer.wavesample(f.fopen())
+
+ def flop(self):
+ self.mixer_accum = {}
+ while self.audio.ready():
+ self.audio.write(self.mixer.mix(self.mixer_channels, self.bufsize))
+ return self.FLOPTIME
+
+ def play(self, sound, lvolume, rvolume):
+ # volume ignored
+ if sound not in self.mixer_accum:
+ self.mixer_channels.append(StringIO(sound))
+ self.mixer_accum[sound] = 1
+
+ def play_musics(self, musics, loop_from):
+ self.cmusics = musics, loop_from, -1
+ self.mixer_channels.insert(0, self)
+
+ def read(self, size):
+ "Provide some more data to self.mixer.poll()."
+ musics, loop_from, c = self.cmusics
+ if c < 0:
+ data = ''
+ else:
+ data = musics[c].mixed.decode(self.mixer, size)
+ if not data:
+ c += 1
+ if c >= len(musics): # end
+ c = loop_from
+ if c >= len(musics):
+ return ''
+ self.cmusics = musics, loop_from, c
+ try:
+ mixed = musics[c].mixed
+ except AttributeError:
+ mixed = musics[c].mixed = Music(musics[c].freezefilename())
+ mixed.openchannel()
+ data = mixed.decode(self.mixer, size)
+ if 0 < len(data) < size:
+ data += self.read(size - len(data))
+ return data
+
+ def fadeout(self, millisec):
+ self.cmusics = [], 0, -1
+
+
+def htmloptionstext(nameval):
+ import modes
+ l = ['<font size=-1>Sampling <%s>' % nameval('select', 'bits')]
+ for bits in (8, 16):
+ l.append('<'+nameval('option', 'bits', str(bits), default='16')+'>'+
+ '%d bits' % bits)
+ l+= ['</select> rate ',
+ '<%s size=5>Hz</font>' % nameval('text', 'freq', default='44100'),
+ '<br>',
+ modes.musichtmloptiontext(nameval)]
+ return '\n'.join(l)
diff --git a/display/windows/.cvsignore b/display/windows/.cvsignore
new file mode 100644
index 0000000..0e01180
--- /dev/null
+++ b/display/windows/.cvsignore
@@ -0,0 +1,3 @@
+*.ncb
+*.opt
+*.plg
diff --git a/display/windows/wingame.c b/display/windows/wingame.c
new file mode 100755
index 0000000..644e496
--- /dev/null
+++ b/display/windows/wingame.c
@@ -0,0 +1,867 @@
+#include <Python.h>
+#include <windows.h>
+#include <mmsystem.h>
+
+
+ /************************** DISPLAY PART ***************************/
+
+typedef struct {
+ BITMAPINFOHEADER bmiHeader;
+ union {
+ DWORD bmiMask[3];
+ short bmiIndices[255];
+ };
+} screenbmp_t;
+
+typedef struct {
+ PyObject_HEAD
+ HWND win;
+ int width, height, bpp;
+ HDC dc;
+ HPALETTE hpal, hprevpal;
+ screenbmp_t screenbmpinfo;
+ unsigned char* screenbmpdata;
+ unsigned char* screenfirstline;
+ int screenscanline; /* < 0 ! */
+ PyObject* keyevents;
+ PyObject* mouseevents;
+ PyObject* motionevent;
+} DisplayObject;
+
+#define DisplayObject_Check(v) ((v)->ob_type == &Display_Type)
+staticforward PyTypeObject Display_Type;
+
+
+static void flush(DisplayObject* self)
+{
+ /*GdiFlush();*/
+}
+
+static void release_window_data(DisplayObject* self)
+{
+ if (self->hprevpal)
+ {
+ SelectPalette(self->dc, self->hprevpal, FALSE);
+ self->hprevpal = (HPALETTE) NULL;
+ }
+ if (self->hpal)
+ {
+ DeleteObject(self->hpal);
+ self->hpal = (HPALETTE) NULL;
+ }
+ if (self->dc && self->win)
+ {
+ ReleaseDC(self->win, self->dc);
+ self->dc = (HDC) NULL;
+ }
+}
+
+static void display_close(DisplayObject* self)
+{
+ release_window_data(self);
+ if (self->win)
+ {
+ SetWindowLong(self->win, 0, 0);
+ DestroyWindow(self->win);
+ self->win = (HWND) NULL;
+ }
+}
+
+static LRESULT CALLBACK display_proc(HWND hwnd, UINT uMsg,
+ WPARAM wParam, LPARAM lParam)
+{
+ DisplayObject* self;
+ switch (uMsg) {
+
+ case WM_KEYDOWN:
+ case WM_KEYUP:
+ self = (DisplayObject*) GetWindowLong(hwnd, 0);
+ if (self)
+ {
+ PyObject* v;
+ int etype;
+ if (self->keyevents == NULL)
+ {
+ self->keyevents = PyList_New(0);
+ if (self->keyevents == NULL)
+ break;
+ }
+ etype = (uMsg == WM_KEYDOWN) ? 2 : 3;
+ v = Py_BuildValue("ii", (int) wParam, etype);
+ if (v == NULL)
+ break;
+ PyList_Append(self->keyevents, v);
+ Py_DECREF(v);
+ }
+ break;
+
+ case WM_LBUTTONDOWN:
+ self = (DisplayObject*) GetWindowLong(hwnd, 0);
+ if (self)
+ {
+ PyObject* v;
+ if (self->mouseevents == NULL)
+ {
+ self->mouseevents = PyList_New(0);
+ if (self->mouseevents == NULL)
+ break;
+ }
+ v = Py_BuildValue("ii", LOWORD(lParam), HIWORD(lParam));
+ if (v == NULL)
+ break;
+ PyList_Append(self->mouseevents, v);
+ Py_DECREF(v);
+ }
+ break;
+
+ case WM_MOUSEMOVE:
+ self = (DisplayObject*) GetWindowLong(hwnd, 0);
+ if (self)
+ {
+ Py_XDECREF(self->motionevent);
+ self->motionevent = Py_BuildValue("ii", LOWORD(lParam), HIWORD(lParam));
+ }
+ break;
+
+ case WM_DESTROY:
+ self = (DisplayObject*) GetWindowLong(hwnd, 0);
+ if (self)
+ {
+ release_window_data(self);
+ self->win = (HWND) NULL;
+ }
+ break;
+
+ default:
+ return DefWindowProc(hwnd, uMsg, wParam, lParam);
+ }
+
+ return 0;
+}
+
+static PyObject* new_display(PyObject* dummy, PyObject* args)
+{
+ char* CLASSNAME = "winxshm";
+ WNDCLASS wcls;
+ DisplayObject* self;
+ int width, height, bytes, bpp, use_shm=0;
+ if (!PyArg_ParseTuple(args, "ii|i", &width, &height, &use_shm))
+ return NULL;
+
+ self = PyObject_New(DisplayObject, &Display_Type);
+ if (self == NULL)
+ return NULL;
+
+ memset(&self->win, 0, ((char*)(self+1)) - ((char*)(&self->win)));
+ self->width = width;
+ self->height = height;
+
+ /* set window class */
+ memset(&wcls, 0, sizeof(wcls));
+ wcls.style = CS_BYTEALIGNCLIENT;
+ wcls.lpfnWndProc = &display_proc;
+ wcls.cbWndExtra = sizeof(DisplayObject*);
+ //wcls.hInstance = HINSTANCE;
+ wcls.hCursor = LoadCursor(0, IDC_ARROW);
+ wcls.lpszClassName = CLASSNAME;
+ RegisterClass(&wcls);
+
+ /* Create the window */
+ self->win = CreateWindowEx(0, CLASSNAME, NULL,
+ WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU |
+ WS_MINIMIZEBOX | WS_VISIBLE,
+ CW_USEDEFAULT, CW_USEDEFAULT,
+ width + 2*GetSystemMetrics(SM_CXFIXEDFRAME),
+ height + 2*GetSystemMetrics(SM_CYFIXEDFRAME) + GetSystemMetrics(SM_CYCAPTION),
+ (HWND) NULL, (HMENU) NULL,
+ /*HINSTANCE*/ 0, (LPVOID) NULL);
+ if (self->win == (HWND) NULL) goto err2;
+ SetWindowLong(self->win, 0, (long) self);
+
+ /* Create DC */
+ self->dc = GetDC(self->win);
+ if (!self->dc) goto err2;
+ self->bpp = bpp = GetDeviceCaps(self->dc, BITSPIXEL);
+ if (bpp == 8)
+ {
+ struct {
+ WORD palVersion;
+ WORD palNumEntries;
+ PALETTEENTRY palPalEntry[255];
+ } pal;
+ pal.palNumEntries = GetSystemPaletteEntries(self->dc, 0, 255, pal.palPalEntry);
+ if (pal.palNumEntries != 0)
+ {
+ int i;
+ pal.palVersion = 0x300;
+ self->hpal = CreatePalette((LOGPALETTE*)(&pal));
+ self->screenbmpinfo.bmiHeader.biClrUsed = pal.palNumEntries;
+ self->hprevpal = SelectPalette(self->dc, self->hpal, FALSE);
+ for (i=0; i<pal.palNumEntries; i++) {
+ self->screenbmpinfo.bmiIndices[i] = i;
+ }
+ }
+ }
+ if (bpp != 15 && bpp != 16 && bpp != 24 && bpp != 32 && !self->hpal)
+ {
+ bpp = 24; /* default */
+ fprintf(stderr, "WARNING: a hi/true color screen mode of 15, 16, 24 or 32 bits per pixels\n");
+ fprintf(stderr, " is highly recommended !\n");
+ }
+
+ /* Allocate screen bitmaps */
+ bytes = (bpp+7)/8; /* per pixel */
+ bytes = (bytes*width+3)&~3; /* per scan line */
+ self->screenscanline = -bytes;
+ bytes = bytes*height; /* for the whole screen */
+ self->screenbmpdata = PyMem_Malloc(bytes);
+ self->screenfirstline = self->screenbmpdata + bytes + self->screenscanline;
+ if (self->screenbmpdata == NULL)
+ {
+ PyErr_NoMemory();
+ goto err2;
+ }
+ self->screenbmpinfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
+ self->screenbmpinfo.bmiHeader.biWidth = self->width;
+ self->screenbmpinfo.bmiHeader.biHeight = self->height;
+ self->screenbmpinfo.bmiHeader.biPlanes = 1;
+ self->screenbmpinfo.bmiHeader.biBitCount = (bpp+7)&~7;
+ if (bpp == 16)
+ {
+ self->screenbmpinfo.bmiHeader.biCompression = BI_BITFIELDS;
+ self->screenbmpinfo.bmiMask[0] = 0xF800;
+ self->screenbmpinfo.bmiMask[1] = 0x07E0;
+ self->screenbmpinfo.bmiMask[2] = 0x001F;
+ }
+
+ flush(self);
+ return (PyObject*) self;
+
+ err2:
+ display_close(self);
+ Py_DECREF(self);
+ if (!PyErr_Occurred())
+ PyErr_SetString(PyExc_IOError, "cannot open window");
+ return NULL;
+}
+
+static void display_dealloc(DisplayObject* self)
+{
+ display_close(self);
+ Py_XDECREF(self->keyevents);
+ Py_XDECREF(self->mouseevents);
+ Py_XDECREF(self->motionevent);
+ PyMem_Free(self->screenbmpdata);
+ PyObject_Del(self);
+}
+
+static PyObject* display_close1(DisplayObject* self, PyObject* args)
+{
+ display_close(self);
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
+static int checkopen(DisplayObject* self)
+{
+ if (self->win)
+ return 1;
+ //PyErr_SetString(PyExc_IOError, "the window was closed");
+ PyErr_SetString(PyExc_SystemExit, "window closed.");
+ return 0;
+}
+
+static PyObject* display_clear1(DisplayObject* self, PyObject* args)
+{
+ if (!checkopen(self))
+ return NULL;
+ memset(self->screenbmpdata, 0, (-self->screenscanline) * self->height);
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
+static void pack_pixel(DisplayObject* self, unsigned char *data, int r, int g, int b,
+ int depth)
+{
+ unsigned short pixel;
+ switch( depth )
+ {
+ case 8:
+ data[0] = GetNearestPaletteIndex(self->hpal, (b<<16) | (g<<8) | r);
+ break;
+ case 15:
+ pixel = ((r<<7) & 0x7c00) | ((g<<2) & 0x03e0) | ((b>>3) & 0x001f);
+ data[0] = (pixel) & 0xff;
+ data[1] = (pixel>>8) & 0xff;
+ break;
+ case 16:
+ /* assumes 5,6,5 model. */
+ pixel = ((r<<8) & 0xf800) | ((g<<3) & 0x07e0) | ((b>>3) & 0x001f);
+ data[0] = (pixel) & 0xff;
+ data[1] = (pixel>>8) & 0xff;
+ break;
+ case 24:
+ if( 1 )
+ {
+ data[0] = b;
+ data[1] = g;
+ data[2] = r;
+ break;
+ }
+ case 32:
+ *((long *)data) = (r<<16) | (g<<8) | b;
+ break;
+ }
+}
+
+static PyObject* display_pixmap1(DisplayObject* self, PyObject* args)
+{
+ int w,h;
+ int length;
+ unsigned char* input = NULL;
+ long keycol = -1;
+
+ if (!checkopen(self))
+ return NULL;
+ if (!PyArg_ParseTuple(args, "ii|s#l", &w, &h, &input, &length, &keycol))
+ return NULL;
+
+ if (1) /* SHM */
+ {
+ int x, y;
+ unsigned char *dst;
+ int size, bytes_per_pixel;
+ long packed_keycol = keycol;
+ PyObject* result;
+ PyObject* str;
+
+ if (input == NULL )
+ {
+ Py_INCREF(Py_None);
+ return Py_None;
+ }
+
+ bytes_per_pixel = self->screenbmpinfo.bmiHeader.biBitCount/8;
+ size = bytes_per_pixel*w*h;
+
+ if( 3*w*h != length )
+ {
+ PyErr_SetString(PyExc_TypeError, "bad string length");
+ return NULL;
+ }
+ /* Create a new string and fill it with the correctly packed image */
+ str = PyString_FromStringAndSize(NULL, size);
+ if (!str)
+ return NULL;
+ if (keycol >= 0)
+ switch( self->bpp )
+ {
+ case 8:
+ packed_keycol = 0xFF;
+ break;
+ case 15:
+ packed_keycol = (1 << 10) | (1 << 5) | 1;
+ break;
+ case 16:
+ packed_keycol = (1 << 11) | (1 << 5) | 1;
+ break;
+ default:
+ packed_keycol = keycol;
+ break;
+ }
+ result = Py_BuildValue("iiOl", w, h, str, packed_keycol);
+ Py_DECREF(str); /* one ref left in 'result' */
+ if (!result)
+ return NULL;
+ dst = (unsigned char*) PyString_AS_STRING(str);
+ memset(dst,0,size);
+
+ for( y=0; y<h; y++ )
+ for( x=0; x<w; x++, input+=3, dst += bytes_per_pixel )
+ {
+ int r = input[0];
+ int g = input[1];
+ int b = input[2];
+ if( ((r<<16)|(g<<8)|b) == keycol )
+ for( b=0; b<bytes_per_pixel; b++ )
+ dst[b] = ((unsigned char *)&packed_keycol)[b];
+ else
+ pack_pixel(self, dst, r, g, b, self->bpp);
+ }
+ return result;
+ }
+}
+
+static PyObject* display_putppm1(DisplayObject* self, PyObject* args)
+{
+ if (!checkopen(self))
+ return NULL;
+
+ if (1) /* SHM */
+ {
+ int x,y,w,h,scanline;
+ int clipx=0, clipy=0, clipw=65536, cliph=65536;
+ unsigned char* src;
+ int length;
+ long keycol;
+ int bytes_per_pixel = self->screenbmpinfo.bmiHeader.biBitCount/8;
+ unsigned char* data = self->screenfirstline;
+ if (!PyArg_ParseTuple(args, "ii(iis#l)|(iiii)",
+ &x, &y, &w, &h, &src, &length, &keycol,
+ &clipx, &clipy, &clipw, &cliph) || !data)
+ return NULL;
+
+ scanline = bytes_per_pixel*w;
+ if (scanline*h != length)
+ {
+ PyErr_SetString(PyExc_TypeError, "bad string length");
+ return NULL;
+ }
+ x -= clipx;
+ y -= clipy;
+ clipx += x;
+ clipy += y;
+ clipw += clipx;
+ cliph += clipy;
+ if (clipx<0) clipx=0;
+ if (clipy<0) clipy=0;
+ if (clipw>self->width) clipw=self->width;
+ if (cliph>self->height) cliph=self->height;
+ if (x<clipx) { src+=(clipx-x)*bytes_per_pixel; w+=x-clipx; x=clipx; }
+ if (y<clipy) { src+=(clipy-y)*scanline; h+=y-clipy; y=clipy; }
+ if (x+w > clipw) w = clipw-x;
+ if (y+h > cliph) h = cliph-y;
+ data += bytes_per_pixel*x+y*self->screenscanline;
+ while (h>0)
+ {
+ int i;
+ int b;
+ unsigned char* src0 = src;
+ unsigned char* data0 = data;
+ if (keycol < 0)
+ for (i=0; i<w; i++)
+ for (b=0; b<bytes_per_pixel; b++)
+ *data++ = *src++;
+ else
+ {
+ unsigned char *keycol_bytes = (unsigned char *)&keycol;
+ for (i=0; i<w; i++)
+ {
+ int transparent = 1;
+ for( b=0; b<bytes_per_pixel; b++ )
+ transparent = transparent && (keycol_bytes[b] == src[b]);
+
+ if (!transparent)
+ for( b=0; b<bytes_per_pixel; b++ )
+ *data++ = *src++;
+ else
+ {
+ data += bytes_per_pixel;
+ src += bytes_per_pixel;
+ }
+ }
+ }
+ src = src0 + scanline;
+ data = data0 + self->screenscanline;
+ h--;
+ }
+ }
+
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
+static PyObject* display_getppm1(DisplayObject* self, PyObject* args)
+{
+ if (!checkopen(self))
+ return NULL;
+
+ if (1) /* SHM */
+ {
+ int x,y,w,h,scanline;
+ int clipx=0, clipy=0, clipw=self->width, cliph=self->height;
+ unsigned char* dst;
+ int length;
+ PyObject* ignored;
+ PyObject* result;
+ PyObject* str;
+ int bytes_per_pixel = self->screenbmpinfo.bmiHeader.biBitCount/8;
+ unsigned char* data = self->screenfirstline;
+ if (!PyArg_ParseTuple(args, "(iiii)|O", &x, &y, &w, &h,
+ &ignored) || !data)
+ return NULL;
+
+ scanline = bytes_per_pixel*w;
+ length = scanline*h;
+ str = PyString_FromStringAndSize(NULL, length);
+ if (!str)
+ return NULL;
+ result = Py_BuildValue("iiOl", w, h, str, -1);
+ Py_DECREF(str); /* one ref left in 'result' */
+ if (!result)
+ return NULL;
+ dst = (unsigned char*) PyString_AS_STRING(str);
+
+ if (x<clipx) { dst+=(clipx-x)*bytes_per_pixel; w+=x-clipx; x=clipx; }
+ if (y<clipy) { dst+=(clipy-y)*scanline; h+=y-clipy; y=clipy; }
+ if (x+w > clipw) w = clipw-x;
+ if (y+h > cliph) h = cliph-y;
+ data += bytes_per_pixel*x+y*self->screenscanline;
+ while (h>0)
+ {
+ int i;
+ int b;
+ unsigned char* dst0 = dst;
+ unsigned char* data0 = data;
+ for (i=0; i<w; i++)
+ {
+ for( b=0; b<bytes_per_pixel; b++ )
+ *dst++ = *data++;
+ }
+ dst = dst0 + scanline;
+ data = data0 + self->screenscanline;
+ h--;
+ }
+ return result;
+ }
+}
+
+static int readXevents(DisplayObject* self)
+{
+ MSG Msg;
+ while (PeekMessage(&Msg, (HWND) NULL, 0, 0, PM_REMOVE))
+ {
+ DispatchMessage(&Msg);
+ if (PyErr_Occurred())
+ return 0;
+ }
+ return checkopen(self);
+}
+
+#define ENABLE_EVENTS(mask) do { } while (0) /* nothing */
+
+static PyObject* display_keyevents1(DisplayObject* self, PyObject* args)
+{
+ PyObject* result;
+ ENABLE_EVENTS(KeyPressMask|KeyReleaseMask);
+ if (!readXevents(self))
+ return NULL;
+ result = self->keyevents;
+ if (result == NULL)
+ result = PyList_New(0);
+ else
+ self->keyevents = NULL;
+ return result;
+}
+
+static PyObject* display_mouseevents1(DisplayObject* self, PyObject* args)
+{
+ PyObject* result;
+ ENABLE_EVENTS(ButtonPressMask);
+ result = self->mouseevents;
+ if (result == NULL)
+ result = PyList_New(0);
+ else
+ self->mouseevents = NULL;
+ return result;
+}
+
+static PyObject* display_pointermotion1(DisplayObject* self, PyObject* args)
+{
+ PyObject* result;
+ ENABLE_EVENTS(PointerMotionMask);
+ result = self->motionevent;
+ if (result == NULL)
+ {
+ Py_INCREF(Py_None);
+ result = Py_None;
+ }
+ else
+ self->motionevent = NULL;
+ return result;
+}
+
+static PyObject* display_flip1(DisplayObject* self, PyObject* args)
+{
+ int r;
+ if (!checkopen(self))
+ return NULL;
+
+ if (self->hpal)
+ RealizePalette(self->dc);
+
+ r = SetDIBitsToDevice(self->dc, 0, 0, self->width, self->height, 0, 0,
+ 0, self->height, self->screenbmpdata, (BITMAPINFO*)(&self->screenbmpinfo),
+ DIB_PAL_COLORS);
+ if (!r)
+ {
+ PyErr_SetString(PyExc_IOError, "SetDIBitsToDevice failed");
+ return NULL;
+ }
+
+ flush(self);
+ if (!readXevents(self))
+ return NULL;
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
+static PyObject* display_shmmode(DisplayObject* self, PyObject *args)
+{
+ return PyInt_FromLong(0);
+}
+
+static PyObject* display_settitle(DisplayObject* self, PyObject* args)
+{
+ char* title;
+ if (!checkopen(self))
+ return NULL;
+ if (!PyArg_ParseTuple(args, "s", &title))
+ return NULL;
+ SetWindowText(self->win, title);
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
+static PyMethodDef display_methods[] = {
+ {"close", (PyCFunction)display_close1, METH_VARARGS, NULL},
+ {"flip", (PyCFunction)display_flip1, METH_VARARGS, NULL},
+ {"clear", (PyCFunction)display_clear1, METH_VARARGS, NULL},
+ {"pixmap", (PyCFunction)display_pixmap1, METH_VARARGS, NULL},
+ {"putppm", (PyCFunction)display_putppm1, METH_VARARGS, NULL},
+ {"getppm", (PyCFunction)display_getppm1, METH_VARARGS, NULL},
+ {"keyevents",(PyCFunction)display_keyevents1,METH_VARARGS, NULL},
+ {"mouseevents",(PyCFunction)display_mouseevents1,METH_VARARGS,NULL},
+ {"pointermotion",(PyCFunction)display_pointermotion1,METH_VARARGS,NULL},
+ {"shmmode", (PyCFunction)display_shmmode, METH_VARARGS, NULL},
+ {"settitle", (PyCFunction)display_settitle, METH_VARARGS, NULL},
+ {NULL, NULL} /* sentinel */
+};
+
+static PyObject* display_getattr(DisplayObject* self, char* name)
+{
+ return Py_FindMethod(display_methods, (PyObject*)self, name);
+}
+
+
+statichere PyTypeObject Display_Type = {
+ PyObject_HEAD_INIT(NULL)
+ 0, /*ob_size*/
+ "Display", /*tp_name*/
+ sizeof(DisplayObject), /*tp_basicsize*/
+ 0, /*tp_itemsize*/
+ /* methods */
+ (destructor)display_dealloc, /*tp_dealloc*/
+ 0, /*tp_print*/
+ (getattrfunc)display_getattr, /*tp_getattr*/
+ 0, /*tp_setattr*/
+ 0, /*tp_compare*/
+ 0, /*tp_repr*/
+ 0, /*tp_as_number*/
+ 0, /*tp_as_sequence*/
+ 0, /*tp_as_mapping*/
+ 0, /*tp_hash*/
+ 0, /*tp_call*/
+};
+
+
+ /************************** AUDIO PART ***************************/
+
+#define NUM_WAVE_HDR 2
+
+typedef struct {
+ PyObject_HEAD
+ HWAVEOUT waveOut;
+ HANDLE doneEvent;
+ WAVEHDR waveHdr[NUM_WAVE_HDR];
+ int waveHdrCount, waveHdrNext;
+} AudioObject;
+
+#define AudioObject_Check(v) ((v)->ob_type == &Audio_Type)
+staticforward PyTypeObject Audio_Type;
+
+
+static PyObject* new_audio(PyObject* dummy, PyObject* args)
+{
+ WAVEFORMATEX wf;
+ int channels, freq, bits, err, bufsize;
+ AudioObject* self;
+ if (!PyArg_ParseTuple(args, "iiii", &channels, &freq, &bits, &bufsize))
+ return NULL;
+
+ self = PyObject_New(AudioObject, &Audio_Type);
+ if (self == NULL)
+ return NULL;
+
+ self->waveHdrCount = 0;
+ self->waveHdrNext = 0;
+ self->waveOut = 0;
+ self->doneEvent = CreateEvent(NULL, FALSE, TRUE, NULL);
+
+ memset(&wf, 0, sizeof(wf));
+ wf.wFormatTag = WAVE_FORMAT_PCM;
+ wf.nChannels = channels;
+ wf.nSamplesPerSec = freq;
+ wf.wBitsPerSample = bits;
+ wf.nBlockAlign = wf.nChannels * wf.wBitsPerSample / 8;
+ wf.nAvgBytesPerSec = wf.nSamplesPerSec * wf.nBlockAlign;
+ err = waveOutOpen(&self->waveOut, WAVE_MAPPER, &wf, (DWORD) self->doneEvent, 0, CALLBACK_EVENT);
+ if (err != MMSYSERR_NOERROR || self->doneEvent == NULL) {
+ Py_DECREF(self);
+ PyErr_SetString(PyExc_IOError, "cannot open audio device");
+ return NULL;
+ }
+
+ while (self->waveHdrCount < NUM_WAVE_HDR) {
+ WAVEHDR* wh = &self->waveHdr[self->waveHdrCount];
+ wh->lpData = PyMem_Malloc(bufsize);
+ wh->dwBufferLength = bufsize;
+ wh->dwFlags = 0;
+ if (wh->lpData == NULL ||
+ waveOutPrepareHeader(self->waveOut, wh, sizeof(WAVEHDR)) != MMSYSERR_NOERROR) {
+ Py_DECREF(self);
+ return NULL;
+ }
+ wh->dwFlags |= WHDR_DONE;
+ self->waveHdrCount++;
+ }
+ return (PyObject*) self;
+}
+
+static void audio_close(AudioObject* self)
+{
+ if (self->waveOut != 0) {
+ waveOutReset(self->waveOut);
+ while (self->waveHdrCount > 0) {
+ WAVEHDR* wh = &self->waveHdr[--self->waveHdrCount];
+ waveOutUnprepareHeader(self->waveOut, wh, sizeof(WAVEHDR));
+ PyMem_Free(wh->lpData);
+ }
+ waveOutClose(self->waveOut);
+ self->waveOut = 0;
+ }
+}
+
+static void audio_dealloc(AudioObject* self)
+{
+ audio_close(self);
+ PyObject_Del(self);
+}
+
+static PyObject* audio_wait1(AudioObject* self, PyObject* args)
+{
+ float delay = -1.0;
+ if (!PyArg_ParseTuple(args, "|f", &delay))
+ return NULL;
+ if (self->waveHdrNext >= self->waveHdrCount) {
+ PyErr_SetString(PyExc_IOError, "audio device not ready");
+ return NULL;
+ }
+ Py_BEGIN_ALLOW_THREADS
+ while (!(self->waveHdr[self->waveHdrNext].dwFlags & WHDR_DONE)) {
+ WaitForSingleObject(self->doneEvent, delay<0.0?INFINITE:(DWORD)(delay*1000.0));
+ }
+ Py_END_ALLOW_THREADS
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
+static PyObject* audio_ready1(AudioObject* self, PyObject* args)
+{
+ if (!PyArg_ParseTuple(args, ""))
+ return NULL;
+ return PyInt_FromLong(self->waveHdrNext < self->waveHdrCount &&
+ (self->waveHdr[self->waveHdrNext].dwFlags & WHDR_DONE));
+}
+
+static PyObject* audio_write1(AudioObject* self, PyObject* args)
+{
+ WAVEHDR* wh;
+ char* buffer;
+ int bufsize;
+ if (!PyArg_ParseTuple(args, "s#", &buffer, &bufsize))
+ return NULL;
+ if (self->waveHdrNext >= self->waveHdrCount) {
+ PyErr_SetString(PyExc_IOError, "audio device not ready");
+ return NULL;
+ }
+ wh = &self->waveHdr[self->waveHdrNext];
+ if (!(wh->dwFlags & WHDR_DONE)) {
+ PyErr_SetString(PyExc_IOError, "audio device would block");
+ return NULL;
+ }
+ if ((DWORD) bufsize != wh->dwBufferLength) {
+ PyErr_SetString(PyExc_ValueError, "bufsize mismatch");
+ return NULL;
+ }
+ wh->dwFlags &= ~WHDR_DONE;
+ memcpy(wh->lpData, buffer, bufsize);
+ if (waveOutWrite(self->waveOut, wh, sizeof(WAVEHDR)) != MMSYSERR_NOERROR) {
+ PyErr_SetString(PyExc_IOError, "audio device write error");
+ return NULL;
+ }
+ self->waveHdrNext++;
+ if (self->waveHdrNext >= self->waveHdrCount)
+ self->waveHdrNext = 0;
+
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
+static PyObject* audio_close1(AudioObject* self, PyObject* args)
+{
+ if (!PyArg_ParseTuple(args, ""))
+ return NULL;
+ audio_close(self);
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
+
+static PyMethodDef audio_methods[] = {
+ {"ready", (PyCFunction)audio_ready1, METH_VARARGS, NULL},
+ {"wait", (PyCFunction)audio_wait1, METH_VARARGS, NULL},
+ {"write", (PyCFunction)audio_write1, METH_VARARGS, NULL},
+ {"close", (PyCFunction)audio_close1, METH_VARARGS, NULL},
+ {NULL, NULL} /* sentinel */
+};
+
+static PyObject* audio_getattr(AudioObject* self, char* name)
+{
+ return Py_FindMethod(audio_methods, (PyObject*)self, name);
+}
+
+
+statichere PyTypeObject Audio_Type = {
+ PyObject_HEAD_INIT(NULL)
+ 0, /*ob_size*/
+ "Audio", /*tp_name*/
+ sizeof(AudioObject), /*tp_basicsize*/
+ 0, /*tp_itemsize*/
+ /* methods */
+ (destructor)audio_dealloc, /*tp_dealloc*/
+ 0, /*tp_print*/
+ (getattrfunc)audio_getattr, /*tp_getattr*/
+ 0, /*tp_setattr*/
+ 0, /*tp_compare*/
+ 0, /*tp_repr*/
+ 0, /*tp_as_number*/
+ 0, /*tp_as_sequence*/
+ 0, /*tp_as_mapping*/
+ 0, /*tp_hash*/
+ 0, /*tp_call*/
+};
+
+
+static PyMethodDef WinMethods[] = {
+ {"Display", new_display, METH_VARARGS},
+ {"Audio", new_audio, METH_VARARGS},
+ {NULL, NULL} /* Sentinel */
+ };
+
+void initwingame(void)
+{
+ Display_Type.ob_type = &PyType_Type;
+ Audio_Type.ob_type = &PyType_Type;
+ Py_InitModule("wingame", WinMethods);
+}
diff --git a/display/windows/wingame.def b/display/windows/wingame.def
new file mode 100755
index 0000000..de59dda
--- /dev/null
+++ b/display/windows/wingame.def
@@ -0,0 +1,2 @@
+EXPORTS
+ initwingame
diff --git a/display/windows/wingame.dsp b/display/windows/wingame.dsp
new file mode 100755
index 0000000..ce1db26
--- /dev/null
+++ b/display/windows/wingame.dsp
@@ -0,0 +1,111 @@
+# Microsoft Developer Studio Project File - Name="wingame" - Package Owner=<4>
+# Microsoft Developer Studio Generated Build File, Format Version 6.00
+# ** NICHT BEARBEITEN **
+
+# TARGTYPE "Win32 (x86) Dynamic-Link Library" 0x0102
+
+CFG=wingame - Win32 Debug
+!MESSAGE Dies ist kein gültiges Makefile. Zum Erstellen dieses Projekts mit NMAKE
+!MESSAGE verwenden Sie den Befehl "Makefile exportieren" und führen Sie den Befehl
+!MESSAGE
+!MESSAGE NMAKE /f "wingame.mak".
+!MESSAGE
+!MESSAGE Sie können beim Ausführen von NMAKE eine Konfiguration angeben
+!MESSAGE durch Definieren des Makros CFG in der Befehlszeile. Zum Beispiel:
+!MESSAGE
+!MESSAGE NMAKE /f "wingame.mak" CFG="wingame - Win32 Debug"
+!MESSAGE
+!MESSAGE Für die Konfiguration stehen zur Auswahl:
+!MESSAGE
+!MESSAGE "wingame - Win32 Release" (basierend auf "Win32 (x86) Dynamic-Link Library")
+!MESSAGE "wingame - Win32 Debug" (basierend auf "Win32 (x86) Dynamic-Link Library")
+!MESSAGE
+
+# Begin Project
+# PROP AllowPerConfigDependencies 0
+# PROP Scc_ProjName ""
+# PROP Scc_LocalPath ""
+CPP=cl.exe
+MTL=midl.exe
+RSC=rc.exe
+
+!IF "$(CFG)" == "wingame - Win32 Release"
+
+# PROP BASE Use_MFC 0
+# PROP BASE Use_Debug_Libraries 0
+# PROP BASE Output_Dir "Release"
+# PROP BASE Intermediate_Dir "Release"
+# PROP BASE Target_Dir ""
+# PROP Use_MFC 0
+# PROP Use_Debug_Libraries 0
+# PROP Output_Dir "Release"
+# PROP Intermediate_Dir "Release"
+# PROP Ignore_Export_Lib 0
+# PROP Target_Dir ""
+# ADD BASE CPP /nologo /MT /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "WINGAME_EXPORTS" /YX /FD /c
+# ADD CPP /nologo /MT /W3 /GX /O2 /I "\home\python222\include" /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "WINGAME_EXPORTS" /U "_DEBUG" /U "DEBUG" /YX /FD /c
+# ADD BASE MTL /nologo /D "NDEBUG" /mktyplib203 /win32
+# ADD MTL /nologo /D "NDEBUG" /mktyplib203 /win32
+# ADD BASE RSC /l 0x100c /d "NDEBUG"
+# ADD RSC /l 0x100c /d "NDEBUG"
+BSC32=bscmake.exe
+# ADD BASE BSC32 /nologo
+# ADD BSC32 /nologo
+LINK32=link.exe
+# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /machine:I386
+# ADD LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib winmm.lib /nologo /dll /machine:I386 /out:"../wingame.pyd" /libpath:"\home\python23\libs" /libpath:"\home\python222\libs"
+
+!ELSEIF "$(CFG)" == "wingame - Win32 Debug"
+
+# PROP BASE Use_MFC 0
+# PROP BASE Use_Debug_Libraries 1
+# PROP BASE Output_Dir "Debug"
+# PROP BASE Intermediate_Dir "Debug"
+# PROP BASE Target_Dir ""
+# PROP Use_MFC 0
+# PROP Use_Debug_Libraries 1
+# PROP Output_Dir "Debug"
+# PROP Intermediate_Dir "Debug"
+# PROP Ignore_Export_Lib 0
+# PROP Target_Dir ""
+# ADD BASE CPP /nologo /MTd /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "WINGAME_EXPORTS" /YX /FD /GZ /c
+# ADD CPP /nologo /MTd /W3 /Gm /GX /ZI /Od /I "\home\python222\include" /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "WINGAME_EXPORTS" /U "_DEBUG" /U "DEBUG" /YX /FD /GZ /c
+# ADD BASE MTL /nologo /D "_DEBUG" /mktyplib203 /win32
+# ADD MTL /nologo /D "_DEBUG" /mktyplib203 /win32
+# ADD BASE RSC /l 0x100c /d "_DEBUG"
+# ADD RSC /l 0x100c /d "_DEBUG"
+BSC32=bscmake.exe
+# ADD BASE BSC32 /nologo
+# ADD BSC32 /nologo
+LINK32=link.exe
+# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /debug /machine:I386 /pdbtype:sept
+# ADD LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib winmm.lib /nologo /dll /debug /machine:I386 /out:"../wingame.pyd" /pdbtype:sept /libpath:"\home\python222\libs"
+
+!ENDIF
+
+# Begin Target
+
+# Name "wingame - Win32 Release"
+# Name "wingame - Win32 Debug"
+# Begin Group "Quellcodedateien"
+
+# PROP Default_Filter "cpp;c;cxx;rc;def;r;odl;idl;hpj;bat"
+# Begin Source File
+
+SOURCE=.\wingame.c
+# End Source File
+# Begin Source File
+
+SOURCE=.\wingame.def
+# End Source File
+# End Group
+# Begin Group "Header-Dateien"
+
+# PROP Default_Filter "h;hpp;hxx;hm;inl"
+# End Group
+# Begin Group "Ressourcendateien"
+
+# PROP Default_Filter "ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe"
+# End Group
+# End Target
+# End Project
diff --git a/display/windows/wingame.dsw b/display/windows/wingame.dsw
new file mode 100755
index 0000000..06f07c9
--- /dev/null
+++ b/display/windows/wingame.dsw
@@ -0,0 +1,29 @@
+Microsoft Developer Studio Workspace File, Format Version 6.00
+# WARNUNG: DIESE ARBEITSBEREICHSDATEI DARF NICHT BEARBEITET ODER GELÖSCHT WERDEN!
+
+###############################################################################
+
+Project: "wingame"=.\wingame.dsp - Package Owner=<4>
+
+Package=<5>
+{{{
+}}}
+
+Package=<4>
+{{{
+}}}
+
+###############################################################################
+
+Global:
+
+Package=<5>
+{{{
+}}}
+
+Package=<3>
+{{{
+}}}
+
+###############################################################################
+
diff --git a/display/xshm.c b/display/xshm.c
new file mode 100644
index 0000000..31ea861
--- /dev/null
+++ b/display/xshm.c
@@ -0,0 +1,1202 @@
+#include <Python.h>
+#include <sys/ipc.h>
+#include <sys/shm.h>
+#include <X11/X.h>
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <X11/extensions/XShm.h>
+
+typedef struct {
+ XImage* m_shm_image;
+ XShmSegmentInfo m_shminfo;
+ int m_width, m_height;
+} XImage_Shm;
+
+typedef struct {
+ PyObject_HEAD
+ Display* dpy;
+ int default_scr;
+ Window root, win;
+ int width, height;
+ XVisualInfo visual_info;
+ GC gc, gc_and, gc_or;
+ XImage_Shm plane;
+ Pixmap backpixmap;
+ int shmmode;
+ int selectinput;
+ PyObject* keyevents;
+ PyObject* mouseevents;
+ PyObject* motionevent;
+} DisplayObject;
+
+typedef struct {
+ PyObject_HEAD
+ DisplayObject* dpy;
+ int width, height;
+ Pixmap mask;
+ Pixmap handle;
+} XPixmapObject;
+
+
+#define DisplayObject_Check(v) ((v)->ob_type == &Display_Type)
+staticforward PyTypeObject Display_Type;
+staticforward PyTypeObject XPixmap_Type;
+
+
+static void pixmap_dealloc(XPixmapObject* pm)
+{
+ if (pm->dpy->dpy)
+ {
+ if (pm->mask != (Pixmap) -1)
+ XFreePixmap(pm->dpy->dpy, pm->mask);
+ XFreePixmap(pm->dpy->dpy, pm->handle);
+ }
+ Py_DECREF(pm->dpy);
+ PyObject_Del(pm);
+}
+
+static XPixmapObject* new_pixmap(DisplayObject* self, int w, int h, int withmask)
+{
+ XPixmapObject* pm = PyObject_New(XPixmapObject, &XPixmap_Type);
+ if (pm != NULL)
+ {
+ Py_INCREF(self);
+ pm->dpy = self;
+ pm->width = w;
+ pm->height = h;
+ pm->handle = XCreatePixmap(self->dpy, self->win, w, h,
+ self->visual_info.depth);
+ if (withmask)
+ pm->mask = XCreatePixmap(self->dpy, self->win, w, h,
+ self->visual_info.depth);
+ else
+ pm->mask = (Pixmap) -1;
+ }
+ return pm;
+}
+
+
+static void flush(DisplayObject* self)
+{
+ XSync(self->dpy, False);
+}
+
+static int create_shm_image(DisplayObject* self, XImage_Shm* img,
+ int width, int height)
+{
+ int image_size = 4*width*height;
+
+ if (XShmQueryExtension(self->dpy) == False)
+ /* does we have the extension at all? */
+ return 0;
+
+ img->m_shm_image = XShmCreateImage(
+ self->dpy,
+ self->visual_info.visual,
+ self->visual_info.depth,
+ ZPixmap,
+ NULL,
+ &img->m_shminfo,
+ width,
+ height);
+ if (img->m_shm_image == NULL)
+ return 0;
+ img->m_width = width;
+ img->m_height = height;
+
+ /* Create shared memory segment: */
+ img->m_shminfo.shmid = shmget(IPC_PRIVATE, image_size, IPC_CREAT|0777);
+ if (img->m_shminfo.shmid < 0)
+ return 0;
+
+ /* Get memory address to segment: */
+ img->m_shminfo.shmaddr = (char *) shmat(img->m_shminfo.shmid, 0, 0);
+
+ /* Mark the segment as destroyable (it will be destroyed when this
+ process terminates) */
+ shmctl(img->m_shminfo.shmid, IPC_RMID, NULL);
+
+ /* Tell XServer that it may only read from it and attach to display: */
+ img->m_shminfo.readOnly = True;
+ XShmAttach (self->dpy, &img->m_shminfo);
+
+ /* Fill the XImage struct: */
+ img->m_shm_image->data = img->m_shminfo.shmaddr;
+ return 1;
+}
+
+static PyObject* new_display(PyObject* dummy, PyObject* args)
+{
+ DisplayObject* self;
+ XSetWindowAttributes attr;
+ int width, height, use_shm=1;
+ if (!PyArg_ParseTuple(args, "ii|i", &width, &height, &use_shm))
+ return NULL;
+
+ self = PyObject_New(DisplayObject, &Display_Type);
+ if (self == NULL)
+ return NULL;
+
+ self->dpy = XOpenDisplay(NULL);
+ if (self->dpy == NULL) goto err;
+ self->default_scr = DefaultScreen(self->dpy);
+ self->root = RootWindow(self->dpy, self->default_scr);
+ self->width = width;
+ self->height = height;
+
+ if (!XMatchVisualInfo(self->dpy, self->default_scr,
+ DefaultDepth(self->dpy,self->default_scr), TrueColor,
+ &self->visual_info)) goto err2;
+
+ /* set window attributes */
+ memset(&attr, 0, sizeof(attr));
+ attr.override_redirect = False;
+ attr.background_pixel = BlackPixel(self->dpy, self->default_scr);
+ attr.backing_store = NotUseful;
+
+ /* Create the window */
+ self->win = XCreateWindow(
+ self->dpy,
+ self->root,
+ 0,
+ 0,
+ width,
+ height,
+ 0,
+ CopyFromParent,
+ CopyFromParent,
+ self->visual_info.visual,
+ CWOverrideRedirect | CWBackPixel | CWBackingStore,
+ &attr);
+ if (self->win == (Window) -1) goto err2;
+
+ XMapRaised(self->dpy, self->win);
+
+ self->shmmode = use_shm &&
+ create_shm_image(self, &self->plane, width, height);
+
+ self->gc = XCreateGC(self->dpy, self->win, 0, 0);
+ if (!self->shmmode)
+ {
+ self->backpixmap = XCreatePixmap(self->dpy, self->root,
+ width, height, self->visual_info.depth);
+ if (self->backpixmap == (Pixmap) -1) goto err2;
+
+ self->gc_and = XCreateGC(self->dpy, self->win, 0, 0);
+ self->gc_or = XCreateGC(self->dpy, self->win, 0, 0);
+ XSetForeground(self->dpy, self->gc, attr.background_pixel);
+ XSetFunction(self->dpy, self->gc_and, GXand);
+ XSetFunction(self->dpy, self->gc_or, GXor);
+ }
+
+ self->selectinput = 0;
+ self->keyevents = NULL;
+ self->mouseevents = NULL;
+ self->motionevent = NULL;
+
+ flush(self);
+ return (PyObject*) self;
+
+ err2:
+ XCloseDisplay(self->dpy);
+ err:
+ Py_DECREF(self);
+ PyErr_SetString(PyExc_IOError, "cannot open X11 display");
+ return NULL;
+}
+
+static void display_close(DisplayObject* self)
+{
+ if (self->dpy)
+ {
+ XCloseDisplay(self->dpy);
+ self->dpy = NULL;
+ }
+}
+
+static void display_dealloc(DisplayObject* self)
+{
+ display_close(self);
+ Py_XDECREF(self->keyevents);
+ Py_XDECREF(self->mouseevents);
+ Py_XDECREF(self->motionevent);
+ PyObject_Del(self);
+}
+
+static PyObject* display_close1(DisplayObject* self, PyObject* args)
+{
+ display_close(self);
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
+static int checkopen(DisplayObject* self)
+{
+ if (self->dpy)
+ return 1;
+ PyErr_SetString(PyExc_IOError, "X11 connexion already closed");
+ return 0;
+}
+
+static unsigned char* get_dpy_data(DisplayObject* self)
+{
+ unsigned char* result;
+ if (!checkopen(self))
+ return NULL;
+ result = (unsigned char*)(self->plane.m_shminfo.shmaddr);
+ if (!result)
+ PyErr_SetString(PyExc_IOError, "X11 SHM failed");
+ return result;
+}
+
+static PyObject* display_clear1(DisplayObject* self, PyObject* args)
+{
+ if (self->shmmode)
+ {
+ unsigned char* data = get_dpy_data(self);
+ if (data == NULL)
+ return NULL;
+ memset(data, 0,
+ ( self->plane.m_shm_image->bits_per_pixel/8
+ *self->width*self->height ) );
+ }
+ else
+ {
+ if (!checkopen(self))
+ return NULL;
+ XFillRectangle(self->dpy, self->backpixmap, self->gc,
+ 0, 0, self->width, self->height);
+ }
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
+inline void pack_pixel(unsigned char *data, int r, int g, int b,
+ int depth, int bytes_per_pixel)
+{
+ unsigned short pixel = 0;
+ switch( depth )
+ {
+ /* No True color below 15 bits per pixel */
+ case 15:
+ pixel = ((r<<7) & 0x7c00) | ((g<<2) & 0x03e0) | ((b>>3) & 0x001f);
+ data[0] = (pixel) & 0xff;
+ data[1] = (pixel>>8) & 0xff;
+ break;
+ case 16:
+ /* assumes 5,6,5 model. */
+ pixel = ((r<<8) & 0xf800) | ((g<<3) & 0x07e0) | ((b>>3) & 0x001f);
+ data[0] = (pixel) & 0xff;
+ data[1] = (pixel>>8) & 0xff;
+ break;
+ case 24:
+ if( bytes_per_pixel == 3 )
+ {
+ data[0] = b;
+ data[1] = g;
+ data[2] = r;
+ break;
+ }
+ /* else it's on 32 bits. Drop into depth of 32. */
+ case 32:
+ *((long *)data) = (r<<16) | (g<<8) | b;
+ break;
+ }
+}
+
+static PyObject* display_pixmap1(DisplayObject* self, PyObject* args)
+{
+ int w,h;
+ unsigned char* input = NULL;
+ int length;
+ long keycol = -1;
+
+ if (!checkopen(self))
+ return NULL;
+ if (!PyArg_ParseTuple(args, "ii|s#l", &w, &h, &input, &length, &keycol))
+ return NULL;
+
+ if (self->shmmode)
+ {
+ int x, y;
+ int bytes_per_pixel = self->plane.m_shm_image->bits_per_pixel/8;
+ int countblocks, countpixels;
+ PyObject* result;
+ PyObject* strblocks;
+ PyObject* strpixels;
+ unsigned int* pblocks;
+ unsigned char* ppixels;
+ unsigned char* input1;
+
+ if (input == NULL)
+ {
+ Py_INCREF(Py_None);
+ return Py_None;
+ }
+ if (3*w*h != length)
+ {
+ PyErr_SetString(PyExc_ValueError, "bad string length");
+ return NULL;
+ }
+
+ /* Convert the image to our internal format.
+ See display_putppm1() for a description of the format.
+ */
+
+ countblocks = 0;
+ countpixels = 0;
+ input1 = input;
+ for (y=0; y<h; y++)
+ {
+ int opaque = 0;
+ for (x=0; x<w; x++)
+ {
+ unsigned int r = input1[0];
+ unsigned int g = input1[1];
+ unsigned int b = input1[2];
+ input1 += 3;
+ if (((r<<16)|(g<<8)|b) == keycol)
+ opaque = 0;
+ else
+ {
+ if (!opaque)
+ {
+ countblocks++; /* start a new block */
+ opaque = 1;
+ }
+ countpixels++;
+ }
+ }
+ countblocks++; /* end-of-line marker block */
+ }
+
+ /* allocate memory */
+ strblocks = PyString_FromStringAndSize(NULL,
+ countblocks*sizeof(int));
+ if (strblocks == NULL)
+ return NULL;
+ strpixels = PyString_FromStringAndSize(NULL,
+ countpixels*bytes_per_pixel);
+ if (strpixels == NULL)
+ {
+ Py_DECREF(strblocks);
+ return NULL;
+ }
+
+ /* write data */
+ pblocks = (unsigned int*) PyString_AS_STRING(strblocks);
+ ppixels = (unsigned char*) PyString_AS_STRING(strpixels);
+ for (y=0; y<h; y++)
+ {
+ int opaque = 0;
+ for (x=0; x<w; x++)
+ {
+ unsigned int r = input[0];
+ unsigned int g = input[1];
+ unsigned int b = input[2];
+ input += 3;
+ if (((r<<16)|(g<<8)|b) == keycol)
+ opaque = 0;
+ else
+ {
+ if (!opaque)
+ {
+ *pblocks++ = x*bytes_per_pixel; /* start a new block */
+ opaque = 1;
+ }
+ pblocks[-1] += bytes_per_pixel<<16; /* add pixel to block */
+ pack_pixel(ppixels, r, g, b,
+ self->visual_info.depth, bytes_per_pixel);
+ ppixels += bytes_per_pixel;
+ }
+ }
+ *pblocks++ = 0; /* end-of-line marker block */
+ }
+
+ result = Py_BuildValue("iiOO", w, h, strblocks, strpixels);
+ Py_DECREF(strblocks);
+ Py_DECREF(strpixels);
+ return result;
+ }
+ else
+ {
+ XImage* image;
+ long extent;
+ unsigned char* data = NULL;
+ unsigned char* maskdata = NULL;
+ int scanline, bitmap_pad;
+ XPixmapObject* pm;
+
+ pm = new_pixmap(self, w, h, keycol>=0);
+ if (pm == NULL)
+ return NULL;
+
+ if (input == NULL)
+ return (PyObject*) pm; /* uninitialized pixmap */
+
+ extent = w*h;
+ if (3*extent != length)
+ {
+ PyErr_SetString(PyExc_ValueError, "bad string length");
+ goto err;
+ }
+
+ bitmap_pad = self->visual_info.depth >= 24 ? 32 : 16;
+ scanline = ((w+bitmap_pad-1) & ~(bitmap_pad-1)) / 8;
+ /*while (scanline&3) scanline++;*/
+ data = malloc(self->visual_info.depth*scanline*h);
+ if (data == NULL)
+ {
+ PyErr_NoMemory();
+ goto err;
+ }
+ memset(data, 0, self->visual_info.depth*scanline*h);
+ maskdata = malloc(self->visual_info.depth*scanline*h);
+ if (maskdata == NULL)
+ {
+ PyErr_NoMemory();
+ goto err;
+ }
+ memset(maskdata, 0, self->visual_info.depth*scanline*h);
+
+ {
+ int key_r = keycol>>16;
+ unsigned char key_g = keycol>>8;
+ unsigned char key_b = keycol>>0;
+ unsigned char* target = data;
+ unsigned char* masktarget = maskdata;
+ int plane, color;
+
+ unsigned int p_size[3];
+ switch( self->visual_info.depth )
+ {
+ case 15:
+ p_size[0] = p_size[1] = p_size[2] = 5;
+ break;
+ case 16:
+ p_size[0] = p_size[2] = 5;
+ p_size[1] = 6;
+ break;
+ case 24:
+ case 32:
+ p_size[0] = p_size[1] = p_size[2] = 8;
+ break;
+ }
+
+ for (color=0; color<3; color++)
+ for (plane=128; plane>=(1<<(8-p_size[color])); plane/=2)
+ {
+ unsigned char* src = input;
+ int x, y;
+ for (y=0; y<h; y++, target+=scanline, masktarget+=scanline)
+ for (x=0; x<w; x++, src+=3)
+ {
+ if (src[0] == key_r && src[1] == key_g && src[2] == key_b)
+ {
+ /* transparent */
+ masktarget[x/8] |= (1<<(x&7));
+ }
+ else
+ if (src[color] & plane)
+ target[x/8] |= (1<<(x&7));
+ }
+ }
+ }
+
+ if (keycol < 0)
+ free(maskdata);
+ else
+ {
+ image = XCreateImage(self->dpy, self->visual_info.visual,
+ self->visual_info.depth, XYPixmap, 0,
+ maskdata, w, h,
+ bitmap_pad, scanline);
+ if (image == NULL || image == (XImage*) -1)
+ {
+ PyErr_SetString(PyExc_IOError, "XCreateImage failed (2)");
+ goto err;
+ }
+ image->byte_order = LSBFirst;
+ image->bitmap_bit_order = LSBFirst;
+ maskdata = NULL;
+ XPutImage(self->dpy, pm->mask, self->gc, image, 0, 0, 0, 0, w, h);
+ XDestroyImage(image);
+ }
+
+ image = XCreateImage(self->dpy, self->visual_info.visual,
+ self->visual_info.depth, XYPixmap, 0,
+ data, w, h,
+ bitmap_pad, scanline);
+ if (image == NULL || image == (XImage*) -1)
+ {
+ PyErr_SetString(PyExc_IOError, "XCreateImage failed");
+ goto err;
+ }
+ image->byte_order = LSBFirst;
+ image->bitmap_bit_order = LSBFirst;
+ data = NULL;
+ XPutImage(self->dpy, pm->handle, self->gc, image, 0, 0, 0, 0, w, h);
+ XDestroyImage(image);
+
+ return (PyObject*) pm;
+
+ err:
+ free(maskdata);
+ free(data);
+ Py_DECREF(pm);
+ return NULL;
+ }
+}
+
+static PyObject* display_get(DisplayObject* self, int x, int y, int w, int h)
+{
+ if (self->shmmode)
+ {
+ int clipx=0, clipy=0, clipw=self->width, cliph=self->height;
+ int original_w, original_h;
+ int firstline=0, firstcol=0;
+ unsigned int bytes_per_pixel = self->plane.m_shm_image->bits_per_pixel/8;
+ unsigned char* data = get_dpy_data(self);
+ if (!data)
+ return NULL;
+
+ original_w = w;
+ original_h = h;
+ if (x<clipx) { firstcol=clipx-x; w+=x-clipx; x=clipx; }
+ if (y<clipy) { firstline=clipy-y; h+=y-clipy; y=clipy; }
+ if (x+w > clipw) w = clipw-x;
+ if (y+h > cliph) h = cliph-y;
+
+ {
+ int countblocks = original_h + ((w>0 && h>0) ? h : 0);
+ /* end blocks + real blocks */
+ int countpixels = (w>0 && h>0) ? w * h : 0;
+ PyObject* result;
+ PyObject* strblocks;
+ PyObject* strpixels;
+ unsigned int* pblocks;
+ unsigned char* ppixels;
+ int wbytes = w * bytes_per_pixel;
+ int block = (firstcol * bytes_per_pixel) | (wbytes << 16);
+ int data_scanline = bytes_per_pixel*self->width;
+
+ /* allocate memory */
+ strblocks = PyString_FromStringAndSize(NULL,
+ countblocks*sizeof(int));
+ if (strblocks == NULL)
+ return NULL;
+ strpixels = PyString_FromStringAndSize(NULL,
+ countpixels*bytes_per_pixel);
+ if (strpixels == NULL)
+ {
+ Py_DECREF(strblocks);
+ return NULL;
+ }
+
+ /* write data */
+ pblocks = (unsigned int*) PyString_AS_STRING(strblocks);
+ ppixels = (unsigned char*) PyString_AS_STRING(strpixels);
+ data += bytes_per_pixel*(x+y*self->width);
+ for (y=0; y<original_h; y++)
+ {
+ if (y >= firstline && y < firstline+h && w > 0)
+ {
+ *pblocks++ = block;
+ memcpy(ppixels, data, wbytes);
+ ppixels += wbytes;
+ data += data_scanline;
+ }
+ *pblocks++ = 0;
+ }
+
+ result = Py_BuildValue("iiOO", original_w, original_h,
+ strblocks, strpixels);
+ Py_DECREF(strblocks);
+ Py_DECREF(strpixels);
+ return result;
+ }
+ }
+ else
+ {
+ XPixmapObject* pm = new_pixmap(self, w, h, 0);
+ if (pm != NULL)
+ XCopyArea(self->dpy, self->backpixmap, pm->handle, self->gc,
+ x, y, w, h, 0, 0);
+ return (PyObject*) pm;
+ }
+}
+
+static PyObject* save_background(DisplayObject* self, int x, int y,
+ int w, int h, int save_bkgnd)
+{
+ if (save_bkgnd)
+ {
+ PyObject* pm = display_get(self, x, y, w, h);
+ PyObject* result;
+ if (pm == NULL)
+ return NULL;
+ result = Py_BuildValue("iiO", x, y, pm);
+ Py_DECREF(pm);
+ return result;
+ }
+ else
+ {
+ Py_INCREF(Py_None);
+ return Py_None;
+ }
+}
+
+#define ALPHAFACTOR 2
+#define ALPHABLEND(maximum, x, y) ((maximum-y)*x/(maximum*ALPHAFACTOR) + y)
+
+static void memcpy_alpha_32(unsigned int* dst, unsigned int* src, int count)
+{
+ int i;
+ for (i=0; i<count/4; i++)
+ {
+ int x = dst[i];
+ int y = src[i];
+
+ int xr = x >> 16;
+ int xg = x & 0xff00;
+ int xb = x & 0xff;
+
+ int yr = y >> 16;
+ int yg = y & 0xff00;
+ int yb = y & 0xff;
+
+ int zr = ALPHABLEND(0xff, xr, yr);
+ int zg = ALPHABLEND(0xff00, xg, yg);
+ int zb = ALPHABLEND(0xff, xb, yb);
+
+ dst[i] = (zr << 16) | (zg & 0xff00) | zb;
+ }
+}
+
+static void memcpy_alpha_24(unsigned char* dst, unsigned char* src, int count)
+{
+ int i;
+ for (i=0; i<count; i++)
+ {
+ int x = dst[i];
+ int y = src[i];
+ dst[i] = ALPHABLEND(255, x, y);
+ }
+}
+
+static void memcpy_alpha_15(unsigned short* dst, unsigned short* src, int count)
+{
+ int i;
+ for (i=0; i<count/2; i++)
+ {
+ unsigned short x = dst[i];
+ unsigned short y = src[i];
+
+ int xr = x >> 10;
+ int xg = x & 0x03e0;
+ int xb = x & 0x001f;
+
+ int yr = y >> 10;
+ int yg = y & 0x03e0;
+ int yb = y & 0x001f;
+
+ int zr = ALPHABLEND(31, xr, yr);
+ int zg = ALPHABLEND(0x3e0, xg, yg);
+ int zb = ALPHABLEND(31, xb, yb);
+
+ dst[i] = (zr << 10) | (zg & 0x03e0) | zb;
+ }
+}
+
+static void memcpy_alpha_16(unsigned short* dst, unsigned short* src, int count)
+{
+ int i;
+ for (i=0; i<count/2; i++)
+ {
+ unsigned short x = dst[i];
+ unsigned short y = src[i];
+
+ int xr = x >> 11;
+ int xg = x & 0x07e0;
+ int xb = x & 0x001f;
+
+ int yr = y >> 11;
+ int yg = y & 0x07e0;
+ int yb = y & 0x001f;
+
+ int zr = ALPHABLEND(31, xr, yr);
+ int zg = ALPHABLEND(0x7e0, xg, yg);
+ int zb = ALPHABLEND(31, xb, yb);
+
+ dst[i] = (zr << 11) | (zg & 0x07e0) | zb;
+ }
+}
+
+typedef void (*memcpy_alpha_fn) (unsigned char*, unsigned char*, int);
+
+static PyObject* display_overlay(DisplayObject* self, PyObject* args,
+ int save_bkgnd)
+{
+ PyObject* result;
+
+ if (self->shmmode)
+ {
+ int x,y,w,h, original_x, original_y, original_w, original_h;
+ int data_scanline;
+ int clipx=0, clipy=0, clipw=65536, cliph=65536, alpha=255;
+ unsigned int* src;
+ unsigned char* srcdata;
+ unsigned char* original_srcdata;
+ int length1, length2, firstline=0, firstcol=0;
+ unsigned int bytes_per_pixel = self->plane.m_shm_image->bits_per_pixel/8;
+ memcpy_alpha_fn memcpy_alpha;
+ unsigned char* data = get_dpy_data(self);
+ if (!PyArg_ParseTuple(args, "ii(iis#s#)|(iiii)i",
+ &x, &y, &w, &h, &src, &length1, &srcdata, &length2,
+ &clipx, &clipy, &clipw, &cliph, &alpha) || !data)
+ return NULL;
+
+ original_x = x;
+ original_y = y;
+ original_w = w;
+ original_h = h;
+ original_srcdata = srcdata;
+ x -= clipx;
+ y -= clipy;
+ clipx += x;
+ clipy += y;
+ clipw += clipx;
+ cliph += clipy;
+ if (clipx<0) clipx=0;
+ if (clipy<0) clipy=0;
+ if (clipw>self->width) clipw=self->width;
+ if (cliph>self->height) cliph=self->height;
+ if (x<clipx) { firstcol = clipx-x; w+=x-clipx; x=clipx; }
+ if (y<clipy) { firstline = clipy-y; h+=y-clipy; y=clipy; }
+ if (x+w > clipw) w = clipw-x;
+ if (y+h > cliph) h = cliph-y;
+ if (w > 0 && h > 0)
+ {
+ int dstoffset, blocksize;
+ unsigned int block;
+ data += bytes_per_pixel*(x+y*self->width);
+ data_scanline = bytes_per_pixel*self->width;
+
+ memcpy_alpha = (memcpy_alpha_fn) memcpy;
+ if (alpha < 255)
+ switch (self->visual_info.depth) {
+ case 15: memcpy_alpha = (memcpy_alpha_fn) memcpy_alpha_15; break;
+ case 16: memcpy_alpha = (memcpy_alpha_fn) memcpy_alpha_16; break;
+ case 24: memcpy_alpha = (memcpy_alpha_fn) memcpy_alpha_24; break;
+ case 32: memcpy_alpha = (memcpy_alpha_fn) memcpy_alpha_32; break;
+ }
+
+ /* 'structure' points to a sequence of int-sized blocks with the
+ following meaning:
+
+ n & 0xFFFF -- byte offset within the line
+ n >> 16 -- number of opaque bytes to copy there
+
+ n == 0 means end of line.
+ */
+
+ /* read and ignore 'firstline' complete lines */
+ while (firstline--)
+ {
+ while ((block = *src++) != 0)
+ {
+ blocksize = block >> 16;
+ srcdata += blocksize;
+ }
+ }
+
+ if (w == original_w)
+ {
+ if (!save_bkgnd)
+ {
+ /* common fast case: copy the whole width of the image */
+ do
+ {
+ while ((block = *src++) != 0)
+ {
+ dstoffset = block & 0xFFFF;
+ blocksize = block >> 16;
+ memcpy(data + dstoffset, srcdata, blocksize);
+ srcdata += blocksize;
+ }
+ data += data_scanline;
+ }
+ while (--h);
+ result = Py_None;
+ Py_INCREF(result);
+ }
+ else
+ {
+ /* copy and save the background */
+ PyObject* cliprect;
+ PyObject* strblocks;
+ PyObject* strpixels;
+ unsigned char* ppixels;
+
+ strpixels = PyString_FromStringAndSize(NULL, length2);
+ if (strpixels == NULL)
+ return NULL;
+ ppixels = (unsigned char*) PyString_AS_STRING(strpixels);
+ ppixels += srcdata - original_srcdata;
+
+ do
+ {
+ while ((block = *src++) != 0)
+ {
+ dstoffset = block & 0xFFFF;
+ blocksize = block >> 16;
+ memcpy(ppixels, data + dstoffset, blocksize);
+ ppixels += blocksize;
+ memcpy_alpha(data + dstoffset, srcdata, blocksize);
+ srcdata += blocksize;
+ }
+ data += data_scanline;
+ }
+ while (--h);
+
+ strblocks = PyTuple_GET_ITEM(PyTuple_GET_ITEM(args, 2), 2);
+ if (PyTuple_GET_SIZE(args) > 3)
+ {
+ cliprect = PyTuple_GET_ITEM(args, 3);
+ result = Py_BuildValue("ii(iiOO)O",
+ original_x,
+ original_y,
+ original_w,
+ original_h,
+ strblocks,
+ strpixels,
+ cliprect);
+ }
+ else
+ {
+ result = Py_BuildValue("ii(iiOO)",
+ original_x,
+ original_y,
+ original_w,
+ original_h,
+ strblocks,
+ strpixels);
+ }
+ Py_DECREF(strpixels);
+ }
+ }
+ else
+ {
+ /* byte offsets within a line */
+ unsigned char* blocksrc;
+ int skip, lastcol;
+
+ result = save_background(self, x, y, w, h, save_bkgnd);
+
+ lastcol = (firstcol + w) * bytes_per_pixel;
+ firstcol *= bytes_per_pixel;
+
+ /* slow case: only copy a portion of the width of the image */
+ data -= firstcol;
+ do
+ {
+ while ((block = *src++) != 0)
+ {
+ dstoffset = block & 0xFFFF;
+ blocksize = block >> 16;
+ blocksrc = srcdata;
+ srcdata += blocksize;
+ skip = firstcol - dstoffset;
+ if (skip < 0)
+ skip = 0;
+ if (blocksize > lastcol - dstoffset)
+ blocksize = lastcol - dstoffset;
+ if (blocksize > skip)
+ memcpy_alpha(data + dstoffset + skip, blocksrc + skip,
+ blocksize - skip);
+ }
+ data += data_scanline;
+ }
+ while (--h);
+ }
+ }
+ else
+ {
+ result = args;
+ Py_INCREF(result);
+ }
+ }
+ else
+ {
+ int x,y, x1=0,y1=0,w1=-1,h1=-1,alpha;
+ XPixmapObject* pm;
+
+ if (!checkopen(self))
+ return NULL;
+ if (!PyArg_ParseTuple(args, "iiO!|(iiii)i", &x, &y, &XPixmap_Type, &pm,
+ &x1, &y1, &w1, &h1, &alpha))
+ return NULL;
+
+ if (w1 < 0)
+ w1 = pm->width;
+ if (h1 < 0)
+ h1 = pm->height;
+
+ result = save_background(self, x, y, w1, h1, save_bkgnd);
+
+ if (pm->mask == (Pixmap) -1)
+ {
+ XCopyArea(self->dpy, pm->handle, self->backpixmap, self->gc,
+ x1, y1, w1, h1, x, y);
+ }
+ else
+ {
+ XCopyArea(self->dpy, pm->mask, self->backpixmap, self->gc_and,
+ x1, y1, w1, h1, x, y);
+ XCopyArea(self->dpy, pm->handle, self->backpixmap, self->gc_or,
+ x1, y1, w1, h1, x, y);
+ }
+ }
+ return result;
+}
+
+static PyObject* display_putppm1(DisplayObject* self, PyObject* args)
+{
+ return display_overlay(self, args, 0);
+}
+
+static PyObject* display_overlayppm1(DisplayObject* self, PyObject* args)
+{
+ return display_overlay(self, args, 1);
+}
+
+static PyObject* display_getppm1(DisplayObject* self, PyObject* args)
+{
+ int x, y, w, h;
+ if (!checkopen(self))
+ return NULL;
+ if (!PyArg_ParseTuple(args, "(iiii)", &x, &y, &w, &h))
+ return NULL;
+ return display_get(self, x, y, w, h);
+}
+
+static int readXevents(DisplayObject* self)
+{
+ while (XEventsQueued(self->dpy, QueuedAfterReading) > 0)
+ {
+ XEvent e;
+ XNextEvent(self->dpy, &e);
+ switch (e.type) {
+ case KeyPress:
+ case KeyRelease:
+ {
+ KeySym sym;
+ PyObject* v;
+ int err;
+ if (self->keyevents == NULL)
+ {
+ self->keyevents = PyList_New(0);
+ if (self->keyevents == NULL)
+ return 0;
+ }
+ sym = XLookupKeysym(&e.xkey,0);
+ v = Py_BuildValue("ii", sym, e.type);
+ if (v == NULL)
+ return 0;
+ err = PyList_Append(self->keyevents, v);
+ Py_DECREF(v);
+ if (err)
+ return 0;
+ break;
+ }
+ case ButtonPress:
+ {
+ PyObject* v;
+ int err;
+ if (self->mouseevents == NULL)
+ {
+ self->mouseevents = PyList_New(0);
+ if (self->mouseevents == NULL)
+ return 0;
+ }
+ v = Py_BuildValue("ii", e.xbutton.x, e.xbutton.y);
+ if (v == NULL)
+ return 0;
+ err = PyList_Append(self->mouseevents, v);
+ Py_DECREF(v);
+ if (err)
+ return 0;
+ break;
+ }
+ case MotionNotify:
+ {
+ Py_XDECREF(self->motionevent);
+ self->motionevent = Py_BuildValue("ii", e.xmotion.x, e.xmotion.y);
+ if (self->motionevent == NULL)
+ return 0;
+ break;
+ }
+ }
+ }
+ return 1;
+}
+
+#define ENABLE_EVENTS(mask) do { \
+ if (!(self->selectinput & (mask))) \
+ { \
+ self->selectinput |= (mask); \
+ XSelectInput(self->dpy, self->win, self->selectinput); \
+ } \
+} while (0)
+
+static PyObject* display_keyevents1(DisplayObject* self, PyObject* args)
+{
+ PyObject* result;
+ ENABLE_EVENTS(KeyPressMask|KeyReleaseMask);
+ if (!readXevents(self))
+ return NULL;
+ result = self->keyevents;
+ if (result == NULL)
+ result = PyList_New(0);
+ else
+ self->keyevents = NULL;
+ return result;
+}
+
+static PyObject* display_mouseevents1(DisplayObject* self, PyObject* args)
+{
+ PyObject* result;
+ ENABLE_EVENTS(ButtonPressMask);
+ result = self->mouseevents;
+ if (result == NULL)
+ result = PyList_New(0);
+ else
+ self->mouseevents = NULL;
+ return result;
+}
+
+static PyObject* display_pointermotion1(DisplayObject* self, PyObject* args)
+{
+ PyObject* result;
+ ENABLE_EVENTS(PointerMotionMask);
+ result = self->motionevent;
+ if (result == NULL)
+ {
+ Py_INCREF(Py_None);
+ result = Py_None;
+ }
+ else
+ self->motionevent = NULL;
+ return result;
+}
+
+static PyObject* display_flip1(DisplayObject* self, PyObject* args)
+{
+ if (!checkopen(self))
+ return NULL;
+
+ if (self->shmmode)
+ {
+ XShmPutImage(self->dpy, self->win, self->gc,
+ self->plane.m_shm_image,
+ 0, 0, 0, 0,
+ self->plane.m_width,
+ self->plane.m_height,
+ False);
+ }
+ else
+ {
+ XCopyArea(self->dpy, self->backpixmap, self->win, self->gc,
+ 0, 0, self->width, self->height, 0, 0);
+ }
+ flush(self);
+ if (!readXevents(self))
+ return NULL;
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
+static PyObject* display_fd1(DisplayObject* self, PyObject *args)
+{
+ return PyInt_FromLong(ConnectionNumber(self->dpy));
+}
+
+static PyObject* display_shmmode(DisplayObject* self, PyObject *args)
+{
+ return PyInt_FromLong(self->shmmode);
+}
+
+static PyMethodDef display_methods[] = {
+ {"close", (PyCFunction)display_close1, METH_VARARGS, NULL},
+ {"flip", (PyCFunction)display_flip1, METH_VARARGS, NULL},
+ {"clear", (PyCFunction)display_clear1, METH_VARARGS, NULL},
+ {"pixmap", (PyCFunction)display_pixmap1, METH_VARARGS, NULL},
+ {"putppm", (PyCFunction)display_putppm1, METH_VARARGS, NULL},
+ {"getppm", (PyCFunction)display_getppm1, METH_VARARGS, NULL},
+ {"overlayppm",(PyCFunction)display_overlayppm1, METH_VARARGS, NULL},
+ {"keyevents",(PyCFunction)display_keyevents1,METH_VARARGS, NULL},
+ {"mouseevents",(PyCFunction)display_mouseevents1,METH_VARARGS,NULL},
+ {"pointermotion",(PyCFunction)display_pointermotion1,METH_VARARGS,NULL},
+ {"fd", (PyCFunction)display_fd1, METH_VARARGS, NULL},
+ {"shmmode", (PyCFunction)display_shmmode, METH_VARARGS, NULL},
+ {NULL, NULL} /* sentinel */
+};
+
+static PyObject* display_getattr(DisplayObject* self, char* name)
+{
+ return Py_FindMethod(display_methods, (PyObject*)self, name);
+}
+
+
+statichere PyTypeObject Display_Type = {
+ PyObject_HEAD_INIT(NULL)
+ 0, /*ob_size*/
+ "Display", /*tp_name*/
+ sizeof(DisplayObject), /*tp_basicsize*/
+ 0, /*tp_itemsize*/
+ /* methods */
+ (destructor)display_dealloc, /*tp_dealloc*/
+ 0, /*tp_print*/
+ (getattrfunc)display_getattr, /*tp_getattr*/
+ 0, /*tp_setattr*/
+ 0, /*tp_compare*/
+ 0, /*tp_repr*/
+ 0, /*tp_as_number*/
+ 0, /*tp_as_sequence*/
+ 0, /*tp_as_mapping*/
+ 0, /*tp_hash*/
+ 0, /*tp_call*/
+};
+
+statichere PyTypeObject XPixmap_Type = {
+ PyObject_HEAD_INIT(NULL)
+ 0, /*ob_size*/
+ "Pixmap", /*tp_name*/
+ sizeof(XPixmapObject), /*tp_basicsize*/
+ 0, /*tp_itemsize*/
+ /* methods */
+ (destructor)pixmap_dealloc, /*tp_dealloc*/
+ 0, /*tp_print*/
+ 0, /*tp_getattr*/
+ 0, /*tp_setattr*/
+ 0, /*tp_compare*/
+ 0, /*tp_repr*/
+ 0, /*tp_as_number*/
+ 0, /*tp_as_sequence*/
+ 0, /*tp_as_mapping*/
+ 0, /*tp_hash*/
+ 0, /*tp_call*/
+};
+
+
+static PyMethodDef ShmMethods[] = {
+ {"Display", new_display, METH_VARARGS},
+ {NULL, NULL} /* Sentinel */
+ };
+
+void initxshm(void)
+{
+ Display_Type.ob_type = &PyType_Type;
+ XPixmap_Type.ob_type = &PyType_Type;
+ Py_InitModule("xshm", ShmMethods);
+}
diff --git a/display/xshm.so b/display/xshm.so
new file mode 100755
index 0000000..2296a67
--- /dev/null
+++ b/display/xshm.so
Binary files differ
diff --git a/doc/BubBob.py.1 b/doc/BubBob.py.1
new file mode 100644
index 0000000..0b77548
--- /dev/null
+++ b/doc/BubBob.py.1
@@ -0,0 +1,32 @@
+.\" $Id: BubBob.py.1,v 1.2 2005/04/13 18:24:17 opqdonut Exp $
+.\"
+.\" Process this file with
+.\" groff -man -Tascii BubBob.py.1
+.\"
+
+.TH BubBob.py 1 "APRIL 2005" Linux "User Manuals"
+
+.SH NAME
+BubBob.py \- Generic startup script for bub-n-bros
+
+.SH SYNOPSIS
+.B BubBob.py
+.br
+.B python BubBob.py
+
+.SH DESCRIPTION
+.B BubBob.py
+runs
+.BR bb.py (1)
+(the bub-n-bros server) and then tries to open the url of its control
+panel in your webbrowser.
+
+.SH ENVIRONMENT
+The environment variable BROWSER affects the choosing of the browser
+on Unix systems.
+
+.SH SEE ALSO
+.BR bb.py (1)
+.BR Client.py (1)
+.BR python (1)
+
diff --git a/doc/Client.py.1 b/doc/Client.py.1
new file mode 100644
index 0000000..758a8dc
--- /dev/null
+++ b/doc/Client.py.1
@@ -0,0 +1,200 @@
+.\" $Id: Client.py.1,v 1.3 2005/05/06 21:32:38 arigo Exp $
+.\"
+.\" Process this file with
+.\" groff -man -Tascii Client.py.1
+.\"
+
+.TH Client.py 1 "APRIL 2005" Linux "User Manuals"
+
+.SH NAME
+Client.py \- the bub-n-bros client
+
+.SH SYNOPSIS
+.B python Client.py [
+.I options
+.BI "] [" host [: port ]]
+.PP
+Note that this script is in the
+.B display/
+subdirectory of the original directory layout.
+
+.SH DESCRIPTION
+This is the bub-n-bros client that connects to bub-n-bros servers
+started with
+.BR bb.py "(1) or " BubBob.py (1) .
+It supports many different audio and video drivers and is very
+multi-platform.
+
+.SH OPTIONS
+
+.TP
+.I host
+Search for a game on the given machine, and connect to one if found.
+If this is not defined, search for servers on the local network.
+
+.TP
+.IB host : port
+Connect to server running on given port on given host. If this is not
+defined, search for servers on the local network.
+
+.TP
+.BI --bits= N
+This option concerns only the
+.B windows
+audio driver. Set bits per sample. Valid values are 8 and 16 (default).
+
+.TP
+.BI "-d " DRIVER ", --display=" DRIVER
+Use video driver
+.IR DRIVER .
+Valid drivers are:
+
+.RS
+.TP
+.B X
+X Window driver for Linux and Unix systems with an X server
+running. See the
+.B --shm
+option.
+.TP
+.B windows
+MS Windows driver.
+.TP
+.B pygame
+PyGame gaming library for python (if installed) has video output. Use
+it if this is specified. See
+.BR http://www.pygame.org " and the " --transparency " option."
+.TP
+.B gtk
+Use the PyGTK library (if installed). See
+.BR http://www.pygtk.org/ " and the " --zoom " option."
+.RE
+
+.TP
+.B -h, --help
+Display help.
+
+.TP
+.BI --fmt= format
+This option concerns only the
+.B linux
+audio driver. Set the sound data format. Defaults to
+.BR S16_NE .
+You can get a list of supported formats with
+.BR --fmt=list .
+
+.TP
+.BI --freq= N
+This option concerns only the
+.BR linux " and " windows
+audio drivers. Set mixing frequency to
+.I N
+(in Hz). Defaults to 44100.
+
+.TP
+.B -m, --metaserver
+Connect with the help of the Metaserver (see
+.BR bb.py (1)
+for more info). Run
+.I Client.py -m
+to print a table of currently running servers, pick the
+the exact IP address and port of the server of your choice
+from the table, and run again
+.I Client.py -m
+.BR host:port .
+
+.TP
+.BI "--port TCP=" port
+In conjunction with the
+.I -m
+option, force a specific
+.I port
+on which to listen for an incoming TCP connexion from the
+remote server. This is only used if a direct connexion to
+the server fails. A random port number is picked by default.
+This option is useful if you are behind a firewall but can
+let TCP connexions on specific ports reach you.
+
+.TP
+.BI "--port UDP=" port
+Force a specific
+.I port
+on which to listen for incoming UDP traffic (animation
+data). Useful if you are behind a firewall but can let UDP
+traffic on a given
+.I port
+pass through. This is optional: animation data is routed
+over the TCP link if it does not appear to reach the client.
+A random port number is picked by default.
+
+.TP
+.BI "-s " DRIVER ", --sound=" DRIVER
+Use driver
+.I DRIVER
+for audio. Valid drivers are:
+
+.RS
+.TP
+.B pygame
+Use the audio capabilities of the PyGame library
+.RB ( http://www.pygame.org ).
+
+.TP
+.B linux
+Use the linux audio mixer. See the
+.BR --freq " and " --fmt " options."
+
+.TP
+.B windows
+Use the Windows audio mixer. See the
+.BR --freq " and " --bits "options."
+
+.TP
+.B off
+No sounds.
+.RE
+
+
+.TP
+.B --shm=yes, --shm=no
+This option concerns only the
+.B X
+display driver. Disable or enable the
+.B shm
+(Shared Memory) extension. It is enabled by default and should only be
+disabled for remote X connections and old X servers.
+
+.TP
+.B -t
+Use the
+.B TCP
+protocol. The default is to autodetect the protocol. See the
+.B -u
+option.
+
+.TP
+.B --transparency=yes, --transparency=no
+This option concerns only the
+.B pygame
+display driver. Disable or enable transparent bubbles. Enabled by
+default. Disabling makes game a bit faster.
+
+.TP
+.B -u
+Use the
+.B UDP
+protocol. The default is to autodetect the protocol. See the
+.B -t
+option.
+
+.TP
+.BI --zoom= N %
+This option concerns only the
+.B gtk
+display driver. Scale output by
+.IR N %.
+
+.SH SEE ALSO
+.BR bb.py (1)
+.BR Client.py (1)
+.BR python (1) \ No newline at end of file
diff --git a/doc/Makefile b/doc/Makefile
new file mode 100644
index 0000000..3c21fcb
--- /dev/null
+++ b/doc/Makefile
@@ -0,0 +1,30 @@
+# 'make compress' to build the docs as gzipped manpages.
+# 'make txt' to build the docs as plain text manpages.
+# 'make html' to build the docs as html-ized manpages.
+
+all: txt html compress
+txt: bb.py.txt Client.py.txt BubBob.py.txt
+html: bb.py.html Client.py.html BubBob.py.html
+
+compress: bb.py.1.gz Client.py.1.gz BubBob.py.1.gz
+
+%.html : %.1
+ groff -man -Thtml $< > $@
+
+%.txt : %.1
+ groff -man -Tascii $< > $@
+
+%.1.gz : %.1
+ gzip -c $< > $@
+
+clean:
+ rm -f bb.py.txt Client.py.txt BubBob.py.txt
+ rm -f bb.py.html Client.py.html BubBob.py.html
+ rm -f *.gz
+
+install:
+ $(INSTALL) -c -o $(MANOWNER) -g $(MANGROUP) -m 644 bb.py.1.gz $(MANDIR)/man1/bubnbros-server.1.gz
+ $(INSTALL) -c -o $(MANOWNER) -g $(MANGROUP) -m 644 Client.py.1.gz $(MANDIR)/man1/bubnbros.1.gz
+ $(INSTALL) -c -o $(MANOWNER) -g $(MANGROUP) -m 644 BubBob.py.1.gz $(MANDIR)/man1/BubBob.1.gz
+
+ ### TODO: install for html and txt files!
diff --git a/doc/bb.py.1 b/doc/bb.py.1
new file mode 100644
index 0000000..d7bff4e
--- /dev/null
+++ b/doc/bb.py.1
@@ -0,0 +1,129 @@
+.\" $Id: bb.py.1,v 1.3 2005/04/17 17:19:32 opqdonut Exp $
+.\"
+.\" Process this file with
+.\" groff -man -Tascii bb.py.1
+.\"
+
+.TH bb.py 1 "APRIL 2005" Linux "User Manuals"
+
+.SH NAME
+bb.py \- the bub-n-bros server.
+
+.SH SYNOPSIS
+.B bb.py [
+.I level-file.bin
+.BI "] [" options ]
+.br
+.B python bb.py [
+.I level-file.bin
+.BI "] [" options ]
+.PP
+Note that this script is in the
+.B bubbob/
+subdirectory of the original directory layout.
+
+.SH DESCRIPTION
+.B bb.py
+starts an http server that acts as a control panel for the server. The
+server listens on port
+.B 8000
+by default. The url for the control panel
+is
+.BI http:// server : port / 0xN
+where
+.I 0xN
+is a random hex number (acts as minimal protection). This url is
+printed when the server starts. You can start and view games and kill
+the server from this panel. The control panel also allows you to type
+in the address of a server to connect to, the script will then open a
+client to that server. The http server also servers java applet
+clients for those players who wish to use one.
+
+When a game is started the script opens a port for the game
+server. This port can then be connected to by a client. Clients
+autodetect servers running on the local network with UDP ping on port
+.BR 8056 .
+
+.SS Connection forming
+
+The client forms a tcp connection to the server. Or, when using the
+metaserver, the server forms a connection to the client. If this
+fails, the client and server try a simultaneous SYN connect. This
+sometimes works if the server and client are behind firewalls. The
+server then tries to transmit the data over udp. If it gets no
+response from the client it will fall back to the existing tcp
+connection.
+
+.SH OPTIONS
+
+.TP
+.BI "-b " N ", --begin " N ", --start " N
+Start at board (level) number
+.IR N .
+The default is 1. See also the
+.B -s
+option.
+
+.TP
+.B -h, --help
+Display help.
+
+.TP
+.B -i, --infinite
+Restart the server at the end of the game. Normally the server quits
+after a certain period of inactivity. This is useful when used with the
+.B -m
+option to make a public server that is available for a long time.
+
+.TP
+.BI "-l " N ", --lives " N
+Limit number of lives to
+.IR N .
+If this option is not specified the number of lives will be infinite.
+
+.TP
+.B -m, --metaserver
+Register server with the
+.I Metaserver
+(currently) at
+.BR codespeak.net:8050 .
+This makes your server visible to everybody, and also facilitates
+joining through a fascistic firewall.
+
+.TP
+.B --port
+.IR TYPE = N
+Sets default listening ports. If type is
+.B LISTEN
+, sets the game server port to
+.IR N .
+The game server port is chosen randomly by default. If the type is
+.B HTTP
+, sets the http server port to
+.IR N .
+The http server port defaults to
+.BR 8000 .
+Another port will be chosen if
+none was specified and
+.B 8000
+is already in use. The server also listens to
+.B UDP
+ping on port
+.BR 8056 .
+
+.TP
+.BI "-s " N ", --step " N
+Increase board number with
+.I N
+when a board is completed. Defaults to 1. see also the
+.B -b
+option.
+
+.SH OUTPUT
+The server outputs helpful debug information concerning the http and
+game servers.
+
+.SH SEE ALSO
+.BR BubBob.py (1)
+.BR Client.py (1)
+.BR python (1)
diff --git a/http2/.cvsignore b/http2/.cvsignore
new file mode 100644
index 0000000..5be8cea
--- /dev/null
+++ b/http2/.cvsignore
@@ -0,0 +1,2 @@
+*.py[co]
+config.txt
diff --git a/http2/config.txt b/http2/config.txt
new file mode 100644
index 0000000..0af71a3
--- /dev/null
+++ b/http2/config.txt
@@ -0,0 +1 @@
+{'queen': {}, '*': {'finalboard': '100', 'beginboard': '1', 'extralife': '50000', 'autoreset': 'n', 'lvlend': 'y', 'lifegainlimit': '1', 'metapublish': 'y', 'limit': 'n', 'file': 'levels/Levels.bin', 'time': '1567957746.48', 'lives': '3', 'stepboard': '1'}}
diff --git a/http2/data/bab.png b/http2/data/bab.png
new file mode 100644
index 0000000..b97b8fc
--- /dev/null
+++ b/http2/data/bab.png
Binary files differ
diff --git a/http2/data/baub.png b/http2/data/baub.png
new file mode 100644
index 0000000..8d85660
--- /dev/null
+++ b/http2/data/baub.png
Binary files differ
diff --git a/http2/data/beab.png b/http2/data/beab.png
new file mode 100644
index 0000000..443d20a
--- /dev/null
+++ b/http2/data/beab.png
Binary files differ
diff --git a/http2/data/beb.png b/http2/data/beb.png
new file mode 100644
index 0000000..8aa3368
--- /dev/null
+++ b/http2/data/beb.png
Binary files differ
diff --git a/http2/data/biab.png b/http2/data/biab.png
new file mode 100644
index 0000000..9de577d
--- /dev/null
+++ b/http2/data/biab.png
Binary files differ
diff --git a/http2/data/bib.png b/http2/data/bib.png
new file mode 100644
index 0000000..c2ec110
--- /dev/null
+++ b/http2/data/bib.png
Binary files differ
diff --git a/http2/data/biob.png b/http2/data/biob.png
new file mode 100644
index 0000000..211e023
--- /dev/null
+++ b/http2/data/biob.png
Binary files differ
diff --git a/http2/data/bob.png b/http2/data/bob.png
new file mode 100644
index 0000000..f01ca02
--- /dev/null
+++ b/http2/data/bob.png
Binary files differ
diff --git a/http2/data/boob.png b/http2/data/boob.png
new file mode 100644
index 0000000..eef411a
--- /dev/null
+++ b/http2/data/boob.png
Binary files differ
diff --git a/http2/data/bub.png b/http2/data/bub.png
new file mode 100644
index 0000000..22742de
--- /dev/null
+++ b/http2/data/bub.png
Binary files differ
diff --git a/http2/data/byb.png b/http2/data/byb.png
new file mode 100644
index 0000000..716988b
--- /dev/null
+++ b/http2/data/byb.png
Binary files differ
diff --git a/http2/data/checked.png b/http2/data/checked.png
new file mode 100644
index 0000000..df0201c
--- /dev/null
+++ b/http2/data/checked.png
Binary files differ
diff --git a/http2/data/close.png b/http2/data/close.png
new file mode 100644
index 0000000..6d9534b
--- /dev/null
+++ b/http2/data/close.png
Binary files differ
diff --git a/http2/data/confirm.html b/http2/data/confirm.html
new file mode 100644
index 0000000..928764d
--- /dev/null
+++ b/http2/data/confirm.html
@@ -0,0 +1,34 @@
+<html>
+<head>
+<meta http-equiv="Pragma" content="no-cache">
+<meta http-equiv="Cache-Control" content="no-cache">
+<meta http-equiv="Expires" content="0">
+<title>See you</title>
+</head>
+<body text="#000000" bgcolor="#C04040" link="#800000" vlink="#800000" alink="#800000">
+
+<h1>Confirmation</h1>
+
+<p>%(count > 1 and ('There are %d clients'%count) or 'There is a client')s
+still connected to your server.</p>
+
+<p>Are you sure you want to stop the server now?
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+<img src="lbab.png">
+</p>
+
+<br>
+
+<table width="100%%" border=0>
+<tr><td bgcolor="#FF8080">
+<table border=0>
+<tr><td>
+<a href="index.html"><strong>&lt;&lt;&lt; Cancel</strong></a>
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+<a href="stop.html?really=y"><strong>Yes, Really Quit &gt;&gt;&gt;</strong></a>
+</td></tr>
+</table>
+</td></tr>
+</table>
+
+</body></html>
diff --git a/http2/data/disabled.png b/http2/data/disabled.png
new file mode 100644
index 0000000..922c394
--- /dev/null
+++ b/http2/data/disabled.png
Binary files differ
diff --git a/http2/data/hat1.png b/http2/data/hat1.png
new file mode 100644
index 0000000..b3684d1
--- /dev/null
+++ b/http2/data/hat1.png
Binary files differ
diff --git a/http2/data/hat2.png b/http2/data/hat2.png
new file mode 100644
index 0000000..6494580
--- /dev/null
+++ b/http2/data/hat2.png
Binary files differ
diff --git a/http2/data/header.png b/http2/data/header.png
new file mode 100644
index 0000000..c9a8566
--- /dev/null
+++ b/http2/data/header.png
Binary files differ
diff --git a/http2/data/index.html b/http2/data/index.html
new file mode 100644
index 0000000..08dcc2f
--- /dev/null
+++ b/http2/data/index.html
@@ -0,0 +1,292 @@
+<html>
+<head>
+<meta http-equiv="Pragma" content="no-cache">
+<meta http-equiv="Cache-Control" content="no-cache">
+<meta http-equiv="Expires" content="0">
+<title>The Bub's Brothers</title>
+</head>
+<body text="#000000" bgcolor="#FFFFFF" link="#0000EE" vlink="#000099" alink="#FF0000">
+&nbsp;
+<center><table cellspacing=0 cellpadding=0 border=0 width="95%%">
+
+%(
+externaltarget = running and ' target="new"' or ''
+)s
+
+
+<tr>
+<td width="6%%" bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+<td width="6%%" bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF"><a href="stop.html?time=%(tim)s"><img src="close.png" width=17 height=17>&nbsp;<font color="#FF0000" size=+1>Stop this program</font></a></td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#F0E0E0">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#E0C0C0" align="center"><font size=+3><strong>The Bub's Brothers</strong></font></td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#F0E0E0">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF" align="right">
+ <a href="http://bub-n-bros.sourceforge.net"%(externaltarget)s>Web Home page</a> -
+ <a href="name.html">Player Names &amp; Teams</a> -
+ <a href="options.html?time=%(tim)s">Configuration</a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF" background="wave1.png"><font size=+3>&nbsp;</font></td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td>
+ <table width="100%%" cellpadding=7>
+ <tr>
+ <td width="20%%" bgcolor="#000080" align="center"><font size=+1 color="#FFFF00"><strong>New game</strong></font></td>
+ <td bgcolor="#C0C0FF">
+ <table border=0>
+ <tr>
+ <td width=48><img src="%(juststarted and 'sfbob.png' or 'bob.png')s"></td>
+ <td>%(
+if running:
+ print '<form name="S" action="index.html" method="get">'
+ print '<input type=hidden name="time" value="%s">' % tim
+ print '<table width="100%" border=0><tr><td><p><strong>'
+ if juststarted:
+ print 'Server started!'
+ elif justconnected:
+ print 'Playing'
+ elif count == 0:
+ print 'No client is connected to your server at the moment.'
+ elif count == 1:
+ print 'One connected client.'
+ else:
+ print '%d connected clients.' % count
+ print '</strong></p></td>'
+ print '<td align="center"><input type=submit value="Update on connected clients"></td>'
+ print '</tr></table></form>'
+ print '<p><strong><a href="join.html?host=%s&port=%s&httpport=%s&time=%s">' % (running[0][0], running[0][1], self.httpport, tim),
+ print 'Join your own game now</a></strong> at <strong>%s:%s</strong></p>' % (
+ running[0])
+##if metapublish:
+## import time
+## print '<p><a href="register.html?a=%s">' % time.time()
+## if self.globaloptions.metapublish == 'y':
+## self.has_been_published = 1
+## metaquery = metaquery or []
+## metaquery.insert(0, 'desc='+fndesc)
+## s = 'a=' + metapublish
+## if s not in metaquery:
+## metaquery.insert(0, s)
+## print 'Register again',
+## else:
+## print 'Register (after all)',
+## print 'your server to the SourceForge meta-server</a><br>'
+## print '<a href="register.html?d=%s">' % time.time()
+## print 'Unregister your server</a>',
+## print '<font size=-1>(it is unregistered automatically after some time',
+## print 'when other people cannot find it, or',
+## print 'if you stop it with the link <font color="#FF2000">Stop this program</font> at the top of the page)</font></p>'
+)s
+<form name="n" action="new.html" method="get">
+<input type=hidden name="time" value="%(tim)s">
+%(
+if self.Game:
+ print '<input type=submit',
+ if running:
+ print 'value="Start another game">'
+ else:
+ print 'value="Start a new game">'
+else:
+ print 'You need the <a href="http://bub-n-bros.sourceforge.net/download.html">complete version</a> to start a new game.<br><font size=-1>With this version you can only connect to existing servers and <font color="#FF0000">only over fast links!</font></font>'
+)s
+</form>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ </table>
+</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF" background="wave3.png"><font size=+3>&nbsp;</font></td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td>%(
+servers = self.getlocalservers()[:]
+
+found = []
+def show((addr, (info, ping)), found=found):
+ import socket
+ infolst = info.split(':') or ['?']
+ if len(infolst) >= 3:
+ httpport = infolst.pop(2)
+ else:
+ httpport = 'off'
+ print '<a href="join.html?host=%s&port=%d&httpport=%s&time=%s">' % (
+ addr[0], addr[1], httpport, tim)
+ host, port = addr
+ #host = socket.gethostbyaddr(host)[0]
+ if ping is not None:
+ infolst.append('ping: %dms' % int(ping*1000))
+ if (addr, infolst[0]) == running:
+ infolst.append('this is your own server')
+ found.append(1)
+ infolst = map(htmlquote, infolst)
+ print '<strong>%s:%s</strong></a> playing <strong>%s</strong>' % (
+ host, port, infolst[0])
+ if len(infolst) > 1:
+ print '&nbsp;(%s)' % ' &middot; &middot; '.join(infolst[1:])
+
+if servers is None:
+ rowspan = 1
+else:
+ rowspan = (len(servers) or 1)+1
+)s
+<table width="100%%" cellpadding=7><tr>
+ <td width="20%%" bgcolor="#FF0080" align="center" rowspan="%(rowspan)s">
+ <font size=+1 color="#FFFF00">
+ <strong>Local games</strong>
+ </font>
+ </td>
+ <td bgcolor="#FFC0C0">
+%(
+if servers is not None:
+ if servers:
+ show(servers[0])
+ else:
+ print "(no server found)"
+ print '</td></tr>'
+ for s in servers[1:]:
+ print '<tr><td bgcolor="#FFC0C0">'
+ show(s)
+ print '</td></tr>'
+ print '<tr><td bgcolor="#FFC0C0">'
+)s
+ <table border=0><tr>
+ <td width=48><img src="boob.png"></td>
+ <td>
+%(
+if running and not found:
+ import hostchooser, gamesrv
+ if gamesrv.displaysockport(gamesrv.openpingsocket()) != hostchooser.UDP_PORT:
+ print '<p><font size=-1>Note: your server does not appear in this list'
+ print 'because the UDP port %d is already in use (is another Bub &amp; Bob server running on this machine?).' % hostchooser.UDP_PORT
+ print 'Use the full server address <strong>%s:%s</strong></font></p>' % running[0]
+)s
+ <form name="L" action="index.html" method="get">
+ <input type=hidden name="time" value="%(tim)s">
+ <input type=submit value="Search again for local servers">
+ </form>
+ </td>
+ </tr></table>
+ <p><form name="J" action="join.html" method="get">
+ <input type=hidden name="time" value="%(tim)s">
+ Or connect to server:
+ <input type=text name="host" size=25>
+ (<code>host</code> or <code>host:port</code>)
+ <input type=submit value="Go">
+ </form></p>
+ </td>
+</tr></table>
+</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF" background="wave2.png"><font size=+3>&nbsp;</font></td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td>
+ <table width="100%%" cellpadding=7>
+ <tr>
+ <td width="20%%" bgcolor="#008000" align="center"><font size=+1 color="#FFFF00"><strong>Internet games</strong></font></td>
+ <td bgcolor="#C0FFCC">
+ <table border=0>
+ <tr>
+ <td width=48><img src="bub.png"></td>
+ <td><p><a href="%(self.metaserverpage(headers))s">Go to the Internet servers page</a></p>
+ <p><font size=-1>Don't forget to <a href="name.html">give a name</a>
+ to your dragons before you join a server!
+ </font></p></td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ </table>
+</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#000000" align="center"><img src="header.png"></td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+
+</table></center>
+
+
+</body>
+</html>
diff --git a/http2/data/lbab.png b/http2/data/lbab.png
new file mode 100644
index 0000000..24fd915
--- /dev/null
+++ b/http2/data/lbab.png
Binary files differ
diff --git a/http2/data/lbeb.png b/http2/data/lbeb.png
new file mode 100644
index 0000000..a04a9e0
--- /dev/null
+++ b/http2/data/lbeb.png
Binary files differ
diff --git a/http2/data/lbib.png b/http2/data/lbib.png
new file mode 100644
index 0000000..191142a
--- /dev/null
+++ b/http2/data/lbib.png
Binary files differ
diff --git a/http2/data/lbiob.png b/http2/data/lbiob.png
new file mode 100644
index 0000000..afc809f
--- /dev/null
+++ b/http2/data/lbiob.png
Binary files differ
diff --git a/http2/data/name.html b/http2/data/name.html
new file mode 100644
index 0000000..dab24f9
--- /dev/null
+++ b/http2/data/name.html
@@ -0,0 +1,189 @@
+<html>
+<head>
+<meta http-equiv="Pragma" content="no-cache">
+<meta http-equiv="Cache-Control" content="no-cache">
+<meta http-equiv="Expires" content="0">
+<title>Name Bub's Brothers</title>
+</head>
+<body text="#000000" bgcolor="#FFFFFF" link="#0000EE" vlink="#000099" alink="#FF0000">
+&nbsp;
+
+<form name="n" action="name.html" method="get">
+<center>
+
+<table CELLSPACING=0 CELLPADDING=0 BORDER=0 WIDTH="95%%">
+
+<tr>
+<td width="6%%" bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#000000" colspan="3">&nbsp;</td>
+<td width="6%%" bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#000000" colspan="3">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF" colspan="3">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#F0E0E0" colspan="3">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#E0C0C0" align="center" colspan="3"><font size=+3><strong>Player Names</strong></font></td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#F0E0E0" colspan="3">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF" colspan="3">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF" colspan="3" align="right">
+ <a href="index.html?time=%(time.time())s">Back to the main page</a>
+</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF" colspan="3" background="wave1.png"><font size=+3>&nbsp;</font></td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+
+%(
+buttons = {
+0: """
+""",
+1: """
+"""}
+for id, img, bgcolor in [
+ (0, 'bub.png', '#c0ffc0'),
+ (1, 'bob.png', '#c0c0ff'),
+ (2,'boob.png', '#ffc0c0'),
+ (3, 'beb.png', '#ffc000'),
+ (4,'biob.png', '#ffff00'),
+ (5, 'bab.png', '#c04040'),
+ (6, 'bib.png', '#c0c0c0'),
+ (7,'baub.png', '#fc60ff'),
+ (8,'beab.png', '#4fa4ff'),
+ (9,'biab.png', '#0ab237'),
+ ]:
+ keyid = "player%d" % id
+ playername = options.get(keyid) or ''
+ playername = playername.strip()
+ for team in [1,2]:
+ if playername.endswith('(%d)' % team):
+ playername = playername[:-3].strip()
+ break
+ else:
+ team = 'off'
+ def nameval(value, team=team, teamid="team%d" % id):
+ s = 'value="%s"' % (value,)
+ if team == value:
+ s += ' selected'
+ return s
+ print """
+<tr>
+ <td bgcolor="#000000">&nbsp;</td>
+ <td bgcolor="%s" align="right" width="35%%"><img src="%s"></td>
+ <td bgcolor="%s" align="center" width="10%%"><input type=text size=16 name="%s" value="%s"></td>
+ <td bgcolor="%s" align="left">&nbsp;&nbsp;
+ <select name="team%d">
+ <option %s>no team<option %s>Team 1<option %s>Team 2</select>
+ </td>
+ <td bgcolor="#000000">&nbsp;</td>
+</tr>
+""" % (bgcolor, img, bgcolor, keyid, playername,
+ bgcolor, id, nameval('off'), nameval(1), nameval(2))
+)s
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF" colspan="3">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td colspan=3 align="center" valign="top" bgcolor="#FFFFFF">
+<input type=submit name="s" value=" Save ">&nbsp;&nbsp;&nbsp;
+<input type=submit name="f" value=" Fill in missing names ">&nbsp;&nbsp;&nbsp;
+<input type=submit name="c" value=" Clear ">
+</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF" colspan="3">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#F0E0E0" colspan="3">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#E0C0C0" colspan="3">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#F0E0E0" colspan="3">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF" colspan="3">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#000000" colspan="3" align="center"><img src="header.png"></td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#000000" colspan="3">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+
+</table>
+
+
+</center>
+</form>
+
+</body>
+</html>
diff --git a/http2/data/new.html b/http2/data/new.html
new file mode 100644
index 0000000..1709c8d
--- /dev/null
+++ b/http2/data/new.html
@@ -0,0 +1,271 @@
+<html>
+<head>
+<meta http-equiv="Pragma" content="no-cache">
+<meta http-equiv="Cache-Control" content="no-cache">
+<meta http-equiv="Expires" content="0">
+<title>New Bub's Brothers Server</title>
+</head>
+<body text="#000000" bgcolor="#FFFFFF" link="#0000EE" vlink="#000099" alink="#FF0000">
+&nbsp;
+
+<form name="n" action="run.html" method="get">
+<input type=hidden name="time" value="%(time.time())s">
+
+%(
+def nameval(name, value, default=0, options=options):
+ s = getattr(options, name)
+ if s == value or (default and not s):
+ s = 'checked '
+ else:
+ s = ''
+ return s + 'name="%s" value="%s"' % (name, value)
+)s
+
+<center>
+<table CELLSPACING=0 CELLPADDING=0 BORDER=0 WIDTH="95%%">
+
+<tr>
+<td width="6%%" bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+<td width="6%%" bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#F0E0E0">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#E0C0C0" align="center"><font size=+3><strong>New Server</strong></font></td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#F0E0E0">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF" align="right">
+ <a href="index.html?time=%(time.time())s">Back to the main page</a>
+</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF" background="wave1.png"><font size=+3>&nbsp;</font></td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td>
+ <table width="100%%" cellpadding=7>
+ <tr>
+ <td width="20%%" bgcolor="#000080" align="center"><font size=+1 color="#FFFF00"><strong>Level file</strong></font></td>
+ <td bgcolor="#C0C0FF">
+ <table border=0>
+ <tr>
+ <td width=48><img src="bob.png"></td>
+ <td><select name="file">%(
+for displayname, filename in Game.FnListBoards():
+ print '<option',
+ if filename == str(options.file):
+ print 'selected',
+ print 'value="%s">' % htmlquote(filename), htmlquote(displayname)
+)s</select>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ </table>
+</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td>
+ <table width="100%%" cellpadding=7>
+ <tr>
+ <td width="20%%" bgcolor="#000080" align="center"><font size=+1 color="#FFFF00"><strong>Levels to play</strong></font></td>
+ <td bgcolor="#C0C0FF">
+ Start at level <input type=text name="beginboard" size=3 value="%(options.beginboard or 1)s">
+and go on ...
+ </td>
+ <td bgcolor="#C0C0FF">
+
+ <input type=radio %(nameval("lvlend", "y", 1))s>&nbsp;to the end of the level file</input><br>
+ <input type=radio %(nameval("lvlend", "n"))s>&nbsp;to</input> level <input type=text name="finalboard" size=3 value="%(options.finalboard or 100)s"><br>
+ skipping levels:
+ <select name="stepboard"> %(
+steps = [
+ (1, 'none'),
+ (2, 'by steps of 2 (skip every other level)')]
+for i in range(3, 10) + range(10, 30, 5):
+ steps.append((i, 'by steps of %d levels' % i))
+for i, text in steps:
+ print '<option',
+ if str(i) == str(options.stepboard):
+ print 'selected',
+ print 'value="%d">' % i, text
+)s</select>
+ </td>
+ </tr>
+ </table>
+</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td>
+ <table width="100%%" cellpadding=7>
+ <tr>
+ <td width="20%%" bgcolor="#000080" align="center"><font size=+1 color="#FFFF00"><strong>Limited lives</strong></font></td>
+ <td bgcolor="#C0C0FF">
+ <input type=radio %(nameval("limit", "n", 1))s>&nbsp;no limited lives --- just run for points!</input><br>
+ <input type=radio %(nameval("limit", "y"))s>&nbsp;limit</input> to <input type=text name="lives" size=3 value="%(options.lives or 3)s"> lives (with an extra life for each <input type=text name="extralife" size=7 value="%(options.extralife or 50000)s"> points)<br>
+ <input type=checkbox %(nameval("limitlifegain", "y"))s>&nbsp;limit lifegain to max. <input type=text name="lifegainlimit" size=3 value="%(options.lifegainlimit or 1)s"> life per level
+ </td>
+ </tr>
+ </table>
+</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td>
+ <table width="100%%" cellpadding=7>
+ <tr>
+ <td width="20%%" bgcolor="#000080" align="center"><font size=+1 color="#FFFF00"><strong>Permanent server</strong></font></td>
+ <td bgcolor="#C0C0FF">
+ <input type=checkbox %(nameval("autoreset", "y"))s>&nbsp;Automatically restart the server after the end is reached, forever</input><br>
+ <font size=-1>Non-permanent servers time out after 2 hours of inactivity</font>
+ </td>
+ </tr>
+ </table>
+</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td>
+ <table width="100%%" cellpadding=7>
+ <tr>
+ <td width="20%%" bgcolor="#008000" align="center"><font size=+1 color="#FFFF00"><strong>Internet game</strong></font></td>
+ <td bgcolor="#C0FFC0">
+ <table border=0>
+ <tr>
+ <td width=48><img src="sfbub.png"></td>
+ <td><p><input type=checkbox %(nameval("metapublish", "y", 1))s>&nbsp;register the server on the Bub-'n-Bros meta-server, allowing it to appear on everybody's Internet Games list</input></p></td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ </table>
+</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF" align="center">
+<input type=submit value=" Start Server ">
+</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+%(
+if running: print '''
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF" align="center">
+<strong><font color="#FF0000">Note:</font> this will replace the server already running on this machine.</strong>
+</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+''')s
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#F0E0E0">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#E0C0C0">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#F0E0E0">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#000000" align="center"><img src="header.png"></td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+
+</table></center>
+</form>
+
+</body>
+</html>
diff --git a/http2/data/options.html b/http2/data/options.html
new file mode 100644
index 0000000..8ded787
--- /dev/null
+++ b/http2/data/options.html
@@ -0,0 +1,285 @@
+<html>
+<head>
+<meta http-equiv="Pragma" content="no-cache">
+<meta http-equiv="Cache-Control" content="no-cache">
+<meta http-equiv="Expires" content="0">
+<title>Settings - The Bub's Brothers</title></head>
+<body text="#000000" bgcolor="#FFFFFF" link="#0000EE" vlink="#000099" alink="#FF0000">
+&nbsp;
+
+%(
+def nameval(type, name, value=None, default=None, mangling=1, options=options):
+ if mangling:
+ name = mode.unique_id() + '_' + name
+ s = getattr(options, name) or default
+ checked = s == value
+ if type == 'option':
+ return 'option %svalue="%s"' % (checked and 'selected ' or '', value)
+ elif type == 'select':
+ return 'select name="%s"' % name
+ elif type == 'text':
+ return 'input type=text name="%s" value="%s"' % (
+ name, htmlquote(s))
+ else:
+ return 'input type=%s %sname="%s" value="%s"' % (
+ type, checked and 'checked ' or '', name, value)
+
+def begingroup(text, fgcolor, bgcolor, lightbgcolor, img, nbitems):
+ global groupinfo
+ print '<tr>'
+ print '<td width="20%"',
+ print 'rowspan="%d" bgcolor="%s" align="center">' % (nbitems or 1, bgcolor)
+ print '<font size=+1 color="%s"><strong>%s</strong></font>' % (fgcolor, text)
+ print '</td>'
+ groupinfo = [lightbgcolor, bgcolor, fgcolor, 0, 0, img]
+ if not nbitems:
+ begingroupitem()
+ endgroupitem()
+def begingroupitem(highlight=0):
+ if groupinfo[4]:
+ print '<tr>'
+ groupinfo[4] += 1
+ groupinfo[3] = highlight
+ print '<td width="80%%" bgcolor="%s">' % groupinfo[highlight]
+ print '<table width="100%" border=0><tr>'
+def endgroupitem():
+ print '<td width=40 align="right" valign="top">'
+ if groupinfo[3]:
+ print '<img src="%s">' % groupinfo[-1]
+ print '</td></tr></table>'
+ print '</td></tr>'
+def endgroup():
+ pass
+
+def beginmode():
+ highlight = mode in currentmodes
+ begingroupitem(highlight)
+
+ print '<td width=36 align="right" valign="center">'
+ err = mode.imperror()
+ if highlight:
+ url = None
+ err = err or "selected"
+ print '<img alt="selected" src="checked.png">'
+ elif err:
+ url = None
+ print '<img alt="%s" src="disabled.png">' % err
+ else:
+ url = "options.html?%s=%s&savetime=%s" % (mode.prefix, mode.name,
+ time.time())
+ err = "select"
+ print '<a href="%s"><img alt="select" src="unchecked.png"></a>' % url
+ print '</td>'
+
+ print '<td width="20%" valign="center">'
+ if url: print '<a href="%s">' % url,
+ print htmlquote(err),
+ if url: print '</a>',
+ print '</td>'
+
+ print '<td width="80%"><font size=+1><strong>',
+ print htmlquote(mode.name),
+ print '</strong></font>'
+ if mode.url:
+ print '&nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;<a href="%s"><i>web site</i></a>' % mode.url
+ print '<br>'
+ print htmlquote(mode.descr)
+ return highlight
+
+def beginmodeoptions():
+ print '<br>'
+ print '<table border=0><tr><td>'
+
+def endmodeoptions():
+ print '</td><td align="center" valign="bottom">'
+ print '<input type=submit value=" Save ">'
+ print '</td></tr></table>'
+
+def endmode():
+ print '</td>'
+ endgroupitem()
+
+def modeitems(modelist):
+ global mode
+ for mode in modelist:
+ if beginmode():
+ txt = mode.htmloptionstext(nameval)
+ if txt:
+ beginmodeoptions()
+ print txt
+ endmodeoptions()
+ endmode()
+)s
+
+
+<center><table CELLSPACING=0 CELLPADDING=0 BORDER=0 WIDTH="95%%">
+
+<tr>
+<td width="6%%" bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+<td width="6%%" bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#E0E0D0">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#D0D0D0">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#C0C0C0" align="center"><font size=+3><strong>Settings</strong></font></td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#D0D0D0">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#E0E0D0">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#E0E0D0" align="right">
+ <a href="http://bub-n-bros.sourceforge.net/help.html">Technical documentation</a> -
+ <a href="index.html?time=%(time.time())s">Back to the main page</a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#E0E0D0">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#E0E0D0">
+
+<form name="options" action="options.html" method="get">
+<input type=hidden name="savetime" value="%(time.time())s">
+<table border=0 cellspacing=4>
+
+
+
+%(
+graphicmodes = self.graphicmodeslist()
+currentmodes = self.localmodes()
+begingroup('Display driver', '#800000', '#FFC000', '#C0C0C0',
+ 'lbeb.png', len(graphicmodes))
+modeitems(graphicmodes)
+endgroup()
+)s
+
+<tr><td>&nbsp;</td><td>&nbsp;</td></tr>
+
+
+
+%(
+soundmodes = self.soundmodeslist()
+java = graphicmodes[0] in currentmodes
+begingroup('Sound driver', '#800000', '#FFFF00', '#C0C0C0',
+ 'lbiob.png', java and 1 or len(soundmodes))
+if java:
+ begingroupitem(1)
+ print '<td><font size=-1>Java Applet always does sounds, but',
+ print 'background music is not implemented</font></td>'
+ endgroupitem()
+else:
+ modeitems(soundmodes)
+endgroup()
+)s
+
+<tr><td>&nbsp;</td><td>&nbsp;</td></tr>
+
+%(
+begingroup('Network options', '#004000', '#80FF00', None, 'lbib.png', 1)
+begingroupitem(1)
+)s
+<td>
+ <table border=0><tr>
+ <td>
+ <p>Network ports are automatically assigned, but you can optionally choose fixed
+ ones and let them in through your firewall. <font size=-1>Clients using the
+ "Internet games" meta-server can usually connect even through firewalls.
+ Moreover servers can re-route UDP traffic to clients behind firewalls over TCP.
+%(
+if java:
+ print "These settings don't apply to the Java applet."
+)s </font></p>
+
+ <p>TCP game server port: <%(nameval("text", "port_LISTEN", default="", mangling=0))s><br>
+ HTTP server port: <%(nameval("text", "port_HTTP", default="", mangling=0))s></p>
+
+ <p>Client incoming UDP port (or <code>host:port</code> if redirected): <%(nameval("text", "port_CLIENT", default="", mangling=0))s><br>
+ <%(nameval("radio", "datachannel", "ucp", mangling=0))s>always UDP</input>
+ <%(nameval("radio", "datachannel", "tcp", mangling=0))s>no UDP, only TCP</input>
+ <%(nameval("radio", "datachannel", "auto", default="auto", mangling=0))s>Auto-detect</input><br>
+ Client incoming TCP port (metaserver-directed back-connections): <%(nameval("text", "port_BACK", default="", mangling=0))s></p>
+ </td>
+ <td width=12></td>
+ <td align="center" valign="bottom">
+ <a href="http://bub-n-bros.sourceforge.net/help.html#port">Help!</a><br><br>
+ <input type=submit value=" Save ">
+ </td>
+ </tr></table>
+</td>
+%(
+endgroupitem()
+endgroup()
+)s
+
+</table>
+</form>
+
+</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#E0E0D0">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#E0E0D0" align="right">
+<form name="reset" action="options.html" method="get">
+ <input type=hidden name="time" value="%(time.time())s">
+ <input type=submit name="reset" value=" Restore all defaults ">
+</form>
+</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+
+</table></center>
+
+</body>
+</html>
diff --git a/http2/data/sfbob.png b/http2/data/sfbob.png
new file mode 100644
index 0000000..388d446
--- /dev/null
+++ b/http2/data/sfbob.png
Binary files differ
diff --git a/http2/data/sfbub.png b/http2/data/sfbub.png
new file mode 100644
index 0000000..142f3b3
--- /dev/null
+++ b/http2/data/sfbub.png
Binary files differ
diff --git a/http2/data/stop.html b/http2/data/stop.html
new file mode 100644
index 0000000..464ed36
--- /dev/null
+++ b/http2/data/stop.html
@@ -0,0 +1,101 @@
+<html>
+<head>
+<meta http-equiv="Pragma" content="no-cache">
+<meta http-equiv="Cache-Control" content="no-cache">
+<meta http-equiv="Expires" content="0">
+<title>See you</title>
+</head>
+<body text="#000000" bgcolor="#C04040" link="#0000EE" vlink="#000099" alink="#FF0000">
+
+<br>
+<table width="100%%" border=0>
+<tr>
+ <td width="90%%" bgcolor="#800000" align="right"><font color="#FFFF00" size=+1><strong><i>See you !</i>&nbsp;&nbsp;&nbsp;</strong></font></td>
+ <td width="48" align="center"><img src="lbab.png"></td>
+</form>
+</tr>
+</table>
+
+<br>
+<br>
+<hr><br>
+<center><table width="70%%" cellspacing=0 cellpadding=0 border=0>
+
+<tr>
+<td width="3%%" bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#000000" align="center"><img src="header.png"></td>
+<td width="3%%" bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF" align="center">
+<i>I hope you enjoyed this game!</i>
+<br><br>
+<a href="http://bub-n-bros.sourceforge.net"><i>http://bub-n-bros.sourceforge.net</i></a>
+</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF" align="center">
+Almost all sprite images, sounds, background musics and
+some of the levels are directly taken from the MacOS version of
+Bub & Bob 1 by McSebi, and redistributed with his gracious
+permission.
+Most graphics have been improved or remade by David Gowers.
+
+<br><br>
+ <a href="http://www.mcsebi.de">http://www.mcsebi.de</a>
+<br><br>
+
+</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#FFFFFF">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#E0E0FF"><br>
+<h2 align="center">Authors</h2>
+
+<ul>
+<li>Programming: Armin Rigo
+<li>Art: David Gowers, based on graphics from McSebi
+<li>Levels: Gio & Odie & Michel-St&eacute;phane & Armin
+<li>Special thanks: Odie & Brachamutanda
+<li>Beta-testers: IMA Connection
+</ul>
+
+<br></td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+<tr>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+<td bgcolor="#000000">&nbsp;</td>
+</tr>
+
+</table></center>
+
+</body>
+</html>
diff --git a/http2/data/unchecked.png b/http2/data/unchecked.png
new file mode 100644
index 0000000..0b20e95
--- /dev/null
+++ b/http2/data/unchecked.png
Binary files differ
diff --git a/http2/data/wave1.png b/http2/data/wave1.png
new file mode 100644
index 0000000..3424216
--- /dev/null
+++ b/http2/data/wave1.png
Binary files differ
diff --git a/http2/data/wave2.png b/http2/data/wave2.png
new file mode 100644
index 0000000..758f47d
--- /dev/null
+++ b/http2/data/wave2.png
Binary files differ
diff --git a/http2/data/wave3.png b/http2/data/wave3.png
new file mode 100644
index 0000000..93fd6d3
--- /dev/null
+++ b/http2/data/wave3.png
Binary files differ
diff --git a/http2/header.png b/http2/header.png
new file mode 100644
index 0000000..c9a8566
--- /dev/null
+++ b/http2/header.png
Binary files differ
diff --git a/http2/httppages.py b/http2/httppages.py
new file mode 100644
index 0000000..3f90093
--- /dev/null
+++ b/http2/httppages.py
@@ -0,0 +1,705 @@
+import os, sys, random
+from cStringIO import StringIO
+import socket, time
+
+PLAYERNAMES = ['Bub', 'Bob', 'Boob', 'Beb',
+ 'Biob', 'Bab', 'Bib',
+ 'Baub', 'Beab', 'Biab']
+
+try:
+ FILE = __file__
+except NameError:
+ FILE = sys.argv[0]
+LOCALDIR = os.path.abspath(os.path.dirname(FILE))
+
+sys.path.insert(0, os.path.abspath(os.path.join(LOCALDIR, os.pardir)))
+sys.path.insert(0, os.path.abspath(os.path.join(LOCALDIR, os.pardir,'common')))
+import gamesrv, httpserver, hostchooser
+from metaserver import metaclient
+from httpserver import HTTPRequestError
+
+
+class Options:
+ def __init__(self, dict={}):
+ self.update(dict)
+ def dict(self):
+ return self.__dict__.copy()
+ def update(self, dict):
+ self.__dict__.update(dict)
+ def copy(self):
+ return Options(self.__dict__)
+ def clear(self):
+ self.__dict__.clear()
+ def __getattr__(self, attr):
+ if not attr.startswith('_'):
+ return None
+ else:
+ raise AttributeError, attr
+
+
+class PageServer:
+ CONFIGFILE = 'config.txt'
+ localservers = None
+
+ def __init__(self, Game):
+ self.Game = Game
+ self.seed = hex(random.randrange(0x1000, 0x10000))
+ #self.unique_actions = {}
+ self.localhost = gamesrv.HOSTNAME
+ self.filename = os.path.join(LOCALDIR, self.CONFIGFILE)
+ data = self.loadoptionfile()
+ self.globaloptions = Options(data.get('*', {}))
+ self.localoptions = Options(data.get(self.localhost, {}))
+ self.reloadports()
+ #self.inetserverlist = None
+ #self.inetservers = {}
+ #self.has_been_published = 0
+
+ def registerpages(self):
+ prefix = '%s/' % self.seed
+ #httpserver.register('controlcenter.html', self.controlcenterloader)
+ httpserver.register(prefix, self.indexloader)
+ httpserver.register(prefix+'index.html', self.indexloader)
+ #httpserver.register(prefix+'list.html', self.listloader)
+ httpserver.register(prefix+'new.html', self.newloader)
+ httpserver.register(prefix+'run.html', self.runloader)
+ httpserver.register(prefix+'stop.html', self.stoploader)
+ httpserver.register(prefix+'join.html', self.joinloader)
+ #httpserver.register(prefix+'register.html',self.registerloader)
+ httpserver.register(prefix+'options.html', self.optionsloader)
+ httpserver.register(prefix+'name.html', self.nameloader)
+ for fn in os.listdir(os.path.join(LOCALDIR, 'data')):
+ path = prefix + fn
+ if not httpserver.is_registered(path):
+ httpserver.register(path, httpserver.fileloader(
+ os.path.join(LOCALDIR, 'data', fn)))
+
+ def opensocket(self):
+ hs = gamesrv.openhttpsocket()
+ if hs is None:
+ return 0
+ self.httpport = port = gamesrv.displaysockport(hs)
+ self.indexurl = 'http://127.0.0.1:%d/%s/' % (port, self.seed)
+ if self.Game:
+ print self.Game.FnDesc,
+ print 'server is ready at', self.indexurl
+ return 1
+
+ def getlocalservers(self):
+ if self.localservers is None:
+ self.searchlocalservers()
+ return self.localservers
+
+ def searchlocalservers(self):
+ servers = hostchooser.find_servers().items()
+ servers = filter(self.filterserver, servers)
+ servers.sort()
+ self.localservers = servers
+
+## def parse_inetserv(self, s):
+## try:
+## host, port, udpport, httpport = s.split(':')
+## return host, int(port), int(udpport)
+## except (ValueError, IndexError):
+## return None, None, None
+
+## def getinetservers(self):
+## if self.inetserverlist is None:
+## return None
+## result = []
+## for s in self.inetserverlist:
+## host, port, udpport = self.parse_inetserv(s)
+## addr = host, port
+## if addr in self.inetservers:
+## result.append((addr, self.inetservers[addr]))
+## return result
+
+## def setinetserverlist(self, lst):
+## self.inetserverlist = lst
+
+## def checkinetserverlist(self):
+## ulist = []
+## for s in self.inetserverlist:
+## host, port, udpport = self.parse_inetserv(s)
+## if host is not None:
+## ulist.append((host, udpport))
+## srvs = hostchooser.find_servers(ulist, delay=0.8)
+## self.inetservers = {}
+## for srv in srvs.items():
+## if not self.filterserver(srv):
+## continue
+## (host, port), info = srv
+## try:
+## host = socket.gethostbyaddr(host)[0]
+## except socket.error:
+## pass
+## self.inetservers[host, port] = info
+## #print 'hostchooser:', self.inetserverlist, '->', self.inetservers
+
+ def filterserver(self, ((host, port), info)):
+ for c in host+str(port):
+ if c not in "-.0123456789:@ABCDEFGHIJKLMNOPQRSTUVWXYZ^_abcdefghijklmnopqrstuvwxyz":
+ return 0
+ return 1
+
+## def statusservers(self):
+## result = [], []
+## for s in self.inetserverlist:
+## host, port, udpport = self.parse_inetserv(s)
+## addr = host, port
+## found = addr in self.inetservers
+## result[found].append(s)
+## return result
+
+ def loadoptionfile(self):
+ try:
+ f = open(self.filename, 'r')
+ data = f.read().strip()
+ f.close()
+ except IOError:
+ data = None
+ return eval(data or '{}', {}, {})
+
+ def saveoptions(self):
+ data = self.loadoptionfile()
+ data['*'] = self.globaloptions.dict()
+ data[self.localhost] = self.localoptions.dict()
+ try:
+ f = open(self.filename, 'w')
+ print >> f, `data`
+ f.close()
+ except IOError, e:
+ print >> sys.stderr, "! Cannot save config file: " + str(e)
+
+ def reloadports(self):
+ import msgstruct
+ msgstruct.PORTS.clear()
+ for key, value in self.localoptions.dict().items():
+ if key.startswith('port_'):
+ key = key[5:]
+ if key == 'CLIENT' and type(value) == str and ':' in value:
+ udphostname, value = value.split(':')
+ msgstruct.PORTS['sendudpto'] = udphostname
+ try:
+ value = int(value)
+ except:
+ continue
+ msgstruct.PORTS[key] = value
+
+ def startgame(self):
+ self.reloadports()
+ options = self.globaloptions
+ kwds = {}
+ if options.beginboard is not None:
+ kwds['beginboard'] = int(options.beginboard)
+ if options.lvlend is not None and options.lvlend.startswith('n'):
+ kwds['finalboard'] = int(options.finalboard)
+ if options.stepboard is not None:
+ kwds['stepboard'] = int(options.stepboard)
+ if options.limit is not None and options.limit.startswith('y'):
+ kwds['limitlives'] = int(options.lives)
+ if options.limitlifegain is not None:
+ kwds['lifegainlimit'] = int(options.lifegainlimit)
+ if options.extralife is not None:
+ kwds['extralife'] = int(options.extralife)
+ if options.autoreset is not None:
+ kwds['autoreset'] = options.autoreset.startswith('y')
+ if options.metapublish is not None:
+ kwds['metaserver'] = options.metapublish.startswith('y')
+ self.Game(options.file, **kwds)
+
+ ### loaders ###
+
+ def metaserverpage(self, headers):
+ metaserver_url = metaclient.METASERVER_URL
+ myhost = my_host(headers)
+ joinurl = quote_plus('%s/%s' % (myhost, self.seed))
+ return metaserver_url + '?join=%s&time=%s' % (joinurl, time.time())
+
+ def mainpage(self, headers, juststarted=0, justconnected=0):
+ running = my_server()
+ count = len(gamesrv.clients)
+ tim = time.time()
+ #if running:
+ # metapublish = my_server_meta_address()
+ # fndesc = quote_plus(gamesrv.game.FnDesc)
+ #else:
+ # metapublish = None
+ return httpserver.load(os.path.join(LOCALDIR, 'data', 'index.html'),
+ 'text/html', locals=locals())
+
+ def indexloader(self, headers, cheat=[], **options):
+ if cheat:
+ import __builtin__
+ for c in cheat:
+ getattr(__builtin__, '__cheat')(c)
+ else:
+ self.localservers = None
+ return self.mainpage(headers, juststarted=('juststarted' in options))
+
+ def controlcenterloader(self, headers, **options):
+ host = headers['remote host']
+ host = socket.gethostbyname(host)
+ if host != '127.0.0.1':
+ raise HTTPRequestError, "Access denied"
+ return None, self.indexurl
+
+## def listloader(self, headers, s=[], **options):
+## self.setinetserverlist(s)
+## self.checkinetserverlist()
+## query = []
+## missing, found = self.statusservers()
+## for s in missing:
+## query.append('d=' + s)
+## for s in found:
+## query.append('a=' + s)
+## return self.mainpage(headers, query)
+
+ def newloader(self, headers, **options):
+ if not self.Game:
+ raise HTTPRequestError, "Complete bub-n-bros installation needed"
+ locals = {
+ 'Game': self.Game,
+ 'options': self.globaloptions,
+ 'running': gamesrv.game is not None,
+ }
+ return httpserver.load(os.path.join(LOCALDIR, 'data', 'new.html'),
+ 'text/html', locals=locals)
+
+ def runloader(self, headers, **options):
+ self.globaloptions.metapublish = 'n'
+ self.globaloptions.autoreset = 'n'
+ for key, value in options.items():
+ if len(value) == 1:
+ setattr(self.globaloptions, key, value[0])
+ self.saveoptions()
+ self.startgame()
+ return None, 'index.html?juststarted=%s' % time.time()
+
+ def stoploader(self, headers, really=[], **options):
+ count = len(gamesrv.clients)
+ if count == 0 or really:
+ locals = {
+ 'self': self,
+ #'metaserver': METASERVER,
+ #'metapublish': gamesrv.game and my_server_meta_address(),
+ #'localdir': LOCALDIR,
+ }
+ gamesrv.closeeverything()
+ return httpserver.load(os.path.join(LOCALDIR, 'data', 'stop.html'),
+ 'text/html', locals=locals)
+ else:
+ locals = {
+ 'count': count,
+ }
+ return httpserver.load(os.path.join(LOCALDIR, 'data', 'confirm.html'),
+ 'text/html', locals=locals)
+
+## def registerloader(self, headers, a=[], d=[], **options):
+## if a: # the lists 'a' and 'd' contain dummies !!
+## self.globaloptions.metapublish = 'y'
+## self.has_been_published = 1
+## kwd = 'a'
+## else:
+## self.globaloptions.metapublish = 'n'
+## kwd = 'd'
+## url = "%s?cmd=register&%s=%s" % (METASERVER,
+## kwd, my_server_meta_address())
+## if a and gamesrv.game:
+## url += '&desc=' + quote_plus(gamesrv.game.FnDesc)
+## return None, url
+
+ def joinloader(self, headers, host=[], port=[], httpport=[],
+ m=[], **options):
+ args = self.buildclientoptions()
+ assert len(host) == 1
+ host = host[0]
+ if len(port) == 1:
+ port = port[0]
+ else:
+ try:
+ host, port = host.split(':')
+ except:
+ port = None
+ if args is None:
+ # redirect to the Java applet
+ try:
+ httpport = int(httpport[0])
+ except (ValueError, IndexError):
+ if port:
+ raise HTTPRequestError, "This server is not running HTTP."
+ else:
+ raise HTTPRequestError, "Sorry, I cannot connect the Java applet to a server using this field."
+ return None, 'http://%s:%s/' % (host, httpport)
+
+ # now is a good time to generate the color files if we can
+ file = os.path.join(LOCALDIR, os.pardir, 'bubbob', 'images',
+ 'buildcolors.py')
+ if os.path.exists(file):
+ g = {'__name__': '__auto__', '__file__': file}
+ execfile(file, g)
+
+ if port:
+ address = '%s:%s' % (host, port)
+ else:
+ address = host
+ nbclients = len(gamesrv.clients)
+ script = os.path.join(LOCALDIR, os.pardir, 'display', 'Client.py')
+ script = no_quote_worries(script)
+ if m:
+ args.insert(0, '-m')
+ args = [script] + args + [address]
+ schedule_launch(args)
+ if m:
+ time.sleep(0.5)
+ s = 'Connecting to %s.' % address
+ return None, self.metaserverpage(headers) + '&head=' + quote_plus(s)
+ #elif my_server_address() == address:
+ # endtime = time.time() + 3.0
+ # while gamesrv.recursiveloop(endtime, []):
+ # if len(gamesrv.clients) > nbclients:
+ # break
+ return self.mainpage(headers, justconnected=1)
+
+ def optionsloader(self, headers, reset=[], savetime=[], **options):
+ if reset:
+ self.localoptions.clear()
+ self.globaloptions.clear()
+ self.saveoptions()
+ elif savetime:
+ self.localoptions.port_CLIENT = None
+ self.localoptions.port_LISTEN = None
+ self.localoptions.port_HTTP = None
+ for key, value in options.items():
+ setattr(self.localoptions, key, value[0])
+ self.saveoptions()
+ locals = {
+ 'self' : self,
+ 'options': self.localoptions,
+ }
+ return httpserver.load(os.path.join(LOCALDIR, 'data', 'options.html'),
+ 'text/html', locals=locals)
+
+ def nameloader(self, headers, **options):
+ MAX = len(PLAYERNAMES)
+ if options:
+ anyname = None
+ for id in range(MAX):
+ keyid = 'player%d' % id
+ if keyid in options:
+ value = options[keyid][0]
+ anyname = anyname or value
+ teamid = 'team%d' % id
+ if teamid in options:
+ team = options[teamid][0]
+ if len(team) == 1:
+ value = '%s (%s)' % (value, team)
+ setattr(self.localoptions, keyid, value)
+ if 'c' in options:
+ for id in range(MAX):
+ keyid = 'player%d' % id
+ try:
+ delattr(self.localoptions, keyid)
+ except AttributeError:
+ pass
+ if 'f' in options:
+ for id in range(MAX):
+ keyid = 'player%d' % id
+ if not getattr(self.localoptions, keyid):
+ setattr(self.localoptions, keyid,
+ anyname or PLAYERNAMES[id])
+ else:
+ anyname = getattr(self.localoptions, keyid)
+ self.saveoptions()
+ if 's' in options:
+ return self.mainpage(headers)
+ locals = {
+ 'options': self.localoptions.dict(),
+ }
+ return httpserver.load(os.path.join(LOCALDIR, 'data', 'name.html'),
+ 'text/html', locals=locals)
+
+ def graphicmodeslist(self):
+ try:
+ return self.GraphicModesList
+ except AttributeError:
+ import display.modes
+ self.GraphicModesList = display.modes.graphicmodeslist()
+ javamode = display.modes.GraphicMode(
+ 'java', 'Java Applet (for Java browsers)', [])
+ javamode.low_priority = 1
+ javamode.getmodule = lambda : None
+ self.GraphicModesList.insert(0, javamode)
+ return self.GraphicModesList
+
+ def soundmodeslist(self):
+ try:
+ return self.SoundModesList
+ except AttributeError:
+ import display.modes
+ self.SoundModesList = display.modes.soundmodeslist()
+ return self.SoundModesList
+
+ def localmodes(self):
+ import display.modes
+ currentmodes = []
+ options = self.localoptions
+ for name, lst in [(options.dpy_, self.graphicmodeslist()),
+ (options.snd_, self.soundmodeslist())]:
+ try:
+ mode = display.modes.findmode(name, lst)
+ except KeyError:
+ try:
+ mode = display.modes.findmode(None, lst)
+ except KeyError, e:
+ print >> sys.stderr, str(e) # no mode!
+ mode = None
+ currentmodes.append(mode)
+ return currentmodes
+
+ def buildclientoptions(self):
+ dpy, snd = self.localmodes()
+ if dpy.getmodule() is None:
+ return None # redirect to the Java applet
+ if dpy is None or snd is None:
+ raise HTTPRequestError, "No installed graphics or sounds drivers. See the settings page."
+ options = self.localoptions
+ result = ['--cfg='+no_quote_worries(self.filename)]
+ for key, value in options.dict().items():
+ if key.startswith('port_') and value:
+ result.append('--port')
+ result.append('%s=%s' % (key[5:], value))
+ if options.datachannel == 'tcp': result.append('--tcp')
+ if options.datachannel == 'udp': result.append('--udp')
+ if options.music == 'no': result.append('--music=no')
+ for optname, mode in [('--display', dpy),
+ ('--sound', snd)]:
+ result.append(optname + '=' + mode.name)
+ uid = mode.unique_id() + '_'
+ for key, value in options.dict().items():
+ if key.startswith(uid):
+ result.append('--%s=%s' % (key[len(uid):], value))
+ return result
+
+def my_host(headers):
+ return headers.get('host') or httpserver.my_host()
+
+def my_server():
+ if gamesrv.game:
+ s = gamesrv.opentcpsocket()
+ return ((gamesrv.HOSTNAME, gamesrv.displaysockport(s)),
+ gamesrv.game.FnDesc)
+ else:
+ return None
+
+def my_server_address():
+ running = my_server()
+ if running:
+ (host, port), info = running
+ return '%s:%d' % (host, port)
+ else:
+ return None
+
+##def my_server_meta_address():
+## s = gamesrv.opentcpsocket()
+## ps = gamesrv.openpingsocket()
+## hs = gamesrv.openhttpsocket()
+## fullname = gamesrv.HOSTNAME
+## try:
+## fullname = socket.gethostbyaddr(fullname)[0]
+## except socket.error:
+## pass
+## return '%s:%s:%s:%s' % (fullname,
+## gamesrv.displaysockport(s),
+## gamesrv.displaysockport(ps),
+## gamesrv.displaysockport(hs))
+
+##def meta_register():
+## # Note: this tries to open a direct HTTP connection to the meta-server
+## # which may not work if the proxy is not configured in $http_proxy
+## try:
+## import urllib
+## except ImportError:
+## print >> sys.stderr, "cannot register with the meta-server: Python's urllib missing"
+## return
+## print "registering with the meta-server...",
+## sys.stdout.flush()
+## addr = my_server_meta_address()
+## try:
+## f = urllib.urlopen('%s?a=%s&desc=%s' % (
+## METASERVER, addr, quote_plus(gamesrv.game.FnDesc)))
+## f.close()
+## except Exception, e:
+## print
+## print >> sys.stderr, "cannot contact the meta-server (check $http_proxy):"
+## print >> sys.stderr, "%s: %s" % (e.__class__.__name__, e)
+## else:
+## print "ok"
+## unregister_at_exit(addr)
+
+##def meta_unregister(addr):
+## import urllib
+## print "unregistering from the meta-server...",
+## sys.stdout.flush()
+## try:
+## f = urllib.urlopen(METASERVER + '?d=' + addr)
+## f.close()
+## except Exception, e:
+## print "failed"
+## else:
+## print "ok"
+
+##def unregister_at_exit(addr, firsttime=[1]):
+## if firsttime:
+## import atexit
+## atexit.register(meta_unregister, addr)
+## del firsttime[:]
+
+QuoteTranslation = {}
+for c in ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+ 'abcdefghijklmnopqrstuvwxyz'
+ '0123456789' '_.-'):
+ QuoteTranslation[c] = c
+del c
+QuoteTranslation[' '] = '+'
+
+def quote_plus(s):
+ """Quote the query fragment of a URL; replacing ' ' with '+'"""
+ getter = QuoteTranslation.get
+ return ''.join([getter(c, '%%%02X' % ord(c)) for c in s])
+
+
+def main(Game, save_url_to=None, quiet=0):
+ #gamesrv.openpingsocket(0) # try to reserve the standard UDP port
+ srv = PageServer(Game)
+ srv.registerpages()
+ if not srv.opensocket():
+ print >> sys.stderr, "server aborted."
+ sys.exit(1)
+ if quiet:
+ if Game:
+ Game.Quiet = 1
+ import stdlog
+ f = stdlog.LogFile()
+ if f:
+ print "Logging to", f.filename
+ sys.stdout = sys.stderr = f
+ if save_url_to:
+ data = srv.indexurl + '\n'
+ def try_to_unlink(fn):
+ try:
+ os.unlink(fn)
+ except:
+ pass
+ import atexit
+ atexit.register(try_to_unlink, save_url_to)
+ try:
+ fno = os.open(save_url_to, os.O_CREAT | os.O_TRUNC | os.O_WRONLY,
+ 0600)
+ if os.write(fno, data) != len(data):
+ raise OSError
+ os.close(fno)
+ except:
+ f = open(save_url_to, 'w')
+ f.write(data)
+ f.close()
+ #if webbrowser:
+ # srv.launchbrowser()
+
+
+# ____________________________________________________________
+# Hack hack hack - workaround for the fact that on Windows
+# the socket is inherited by the subprocess, which is quite
+# bad because it keeps the browser-server connexion alive
+# and the browser gets confused
+
+
+def schedule_launch(args):
+ httpserver.actions_when_finished.append(lambda args=args: launch(args))
+
+def launch(args):
+ # platform-specific hacks
+ print 'Running client -> ', ' '.join(args)
+ if 0: # OLD CODE sys.platform == 'darwin': # must start as a UI process
+ import tempfile
+ cmdname = tempfile.mktemp('_BubBob.py')
+ f = open(cmdname, 'w')
+ print >> f, 'import sys, os'
+ print >> f, 'try: os.unlink(%r)' % cmdname
+ print >> f, 'except OSError: pass'
+ print >> f, 'sys.argv[:] = %r' % (args,)
+ print >> f, '__file__ = %r' % cmdname
+ print >> f, 'execfile(%r)' % args[0]
+ f.close()
+ os.system('/usr/bin/open -a PythonLauncher "%s"' % cmdname)
+ else:
+ args.insert(0, sys.executable)
+ # try to close the open fds first
+ if hasattr(os, 'fork'):
+ try:
+ from resource import getrlimit, RLIMIT_NOFILE, error
+ except ImportError:
+ pass
+ else:
+ try:
+ soft, hard = getrlimit(RLIMIT_NOFILE)
+ except error:
+ pass
+ else:
+ if os.fork():
+ return # in parent -- done, continue
+ # in child
+ for fd in range(3, min(16384, hard)):
+ try:
+ os.close(fd)
+ except OSError:
+ pass
+ os.execv(args[0], args)
+ # this point should never be reached
+ # fall-back
+ # (quoting sucks on Windows) ** 42
+ if sys.platform == 'win32':
+ args[0] = '"%s"' % (args[0],)
+ if hasattr(os, 'P_DETACH'):
+ mode = os.P_DETACH
+ elif hasattr(os, 'P_NOWAIT0'):
+ mode = os.P_NOWAIT0
+ else:
+ mode = os.P_NOWAIT
+ os.spawnv(mode, sys.executable, args)
+
+if sys.platform != "win32":
+ def no_quote_worries(s):
+ return s
+else:
+ def no_quote_worries(s): # quoting !&?+*:-(
+ s = os.path.normpath(os.path.abspath(s))
+ absroot = os.path.join(LOCALDIR, os.pardir)
+ absroot = os.path.normpath(os.path.abspath(absroot))
+ ROOTDIR = os.curdir
+ while os.path.normpath(os.path.abspath(ROOTDIR)) != absroot:
+ if ROOTDIR == os.curdir:
+ ROOTDIR = os.pardir
+ else:
+ ROOTDIR = os.path.join(ROOTDIR, os.pardir)
+ if len(ROOTDIR) > 200:
+ # cannot find relative path! try with absolute one anyway
+ ROOTDIR = absroot
+ break
+ assert s.startswith(absroot)
+ if absroot.endswith(os.sep): # 'C:\'
+ absroot = absroot[:-1]
+ assert s[len(absroot)] == os.sep
+ relpath = s[len(absroot)+1:]
+ result = os.path.join(ROOTDIR, relpath)
+ print "no_quote_worries %r => %r" % (s, result)
+ return result
+
+
+if __name__ == '__main__':
+ if (len(sys.argv) != 3 or sys.argv[1] != '--quiet' or
+ not sys.argv[2].startswith('--saveurlto=')):
+ print >> sys.stderr, "This script should only be launched by BubBob.py."
+ sys.exit(2)
+ main(None, sys.argv[2][len('--saveurlto='):], quiet=1)
+ gamesrv.mainloop()
diff --git a/http2/sf/bb12.py b/http2/sf/bb12.py
new file mode 100755
index 0000000..7ea6e6e
--- /dev/null
+++ b/http2/sf/bb12.py
@@ -0,0 +1,274 @@
+#! /usr/bin/python
+
+import cgi, os, string, time
+form = cgi.FieldStorage()
+
+def txtfilter(s):
+ l = filter(lambda c: c in "!$*,-.0123456789:@ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz{|}", s)
+ return string.join(l, '')
+
+def identity(s):
+ return s
+
+def fieldlist(name, filter=txtfilter):
+ result = []
+ if form.has_key(name):
+ field = form[name]
+ if type(field) is type([]):
+ for f in field:
+ result.append(filter(f.value))
+ else:
+ result.append(filter(field.value))
+ return result
+
+def goodmatch(addr1, addr2):
+ l1 = string.split(addr1, '.')
+ if len(l1) >= 4: del l1[-1]
+ l2 = string.split(addr2, '.')
+ if len(l2) >= 4: del l2[-1]
+ return l1 == l2
+
+
+class Entry:
+ Notice = ''
+ FIELDS = [('server', 128), ('desc', 128), ('icon', 64),
+ ('orig', 64), ('dele1', 64), ('dele2', 64)]
+
+ def __init__(self, f):
+ self.pos = f.tell()
+ for fname, flen in self.FIELDS:
+ s = f.read(flen)
+ if not s:
+ self.filled = 0
+ break
+ setattr(self, fname, string.rstrip(s))
+ else:
+ self.filled = 1
+
+ def __nonzero__(self):
+ return self.filled
+
+ def write(self, f):
+ if not getattr(self, 'icon', ''):
+ try:
+ import random
+ lst = os.listdir('../htdocs/images')
+ except:
+ pass
+ else:
+ lst = filter(lambda x: x[-4:]=='.png' and 'A'<=x[0]<='Z', lst)
+ if lst:
+ self.icon = random.choice(lst)
+ f.seek(self.pos)
+ for fname, flen in self.FIELDS:
+ data = getattr(self, fname, '')[:flen]
+ f.write(data + ' '*(flen-len(data)))
+
+
+def main():
+ DATABASE = 'servers'
+ SIZEMAX = 65536
+ Serv = 0
+ Orig = 1
+ Dele1 = 2
+ REMOTE_ADDR = os.environ['REMOTE_ADDR']
+ REMOTE_ID = REMOTE_ADDR + time.strftime('@%d%m', time.localtime(time.time()))
+ OPS = {}
+
+ for srv in fieldlist('d'):
+ OPS[srv] = 'd'
+ for srv in fieldlist('a'):
+ OPS[srv] = 'a'
+ desc = (fieldlist('desc', identity) or [''])[0]
+
+ f = open(DATABASE, 'r+b')
+ freelist = []
+ published = []
+ while 1:
+ e = Entry(f)
+ if not e:
+ break
+ if e.server:
+ if OPS.get(e.server) == 'a':
+ validdesc = desc and goodmatch(REMOTE_ADDR, e.orig)
+ if e.dele1 or e.dele2 or (validdesc and desc != e.desc):
+ e.dele1 = e.dele2 = '' # re-enable server
+ if validdesc: e.desc = desc
+ e.write(f)
+ del OPS[e.server]
+ elif OPS.get(e.server) == 'd':
+ if goodmatch(REMOTE_ADDR, e.orig) or (
+ REMOTE_ID != e.dele1 and REMOTE_ID != e.dele2):
+ if goodmatch(REMOTE_ADDR, e.orig):
+ e.server = '' # remove server
+ elif e.dele1 == '':
+ e.dele1 = REMOTE_ID
+ elif e.dele2 == '':
+ e.dele2 = REMOTE_ID
+ else:
+ e.server = '' # remove server
+ e.write(f)
+ Entry.Notice = 'd'
+ if e.server:
+ published.append((e.pos, e))
+ else:
+ freelist.append(e)
+
+ for srv, action in OPS.items():
+ if action == 'a':
+ if freelist:
+ e = freelist[-1]
+ del freelist[-1]
+ else:
+ f.seek(0, 2)
+ e = Entry(f)
+ if e.pos >= SIZEMAX:
+ raise Exception("Sorry, server database too big")
+ hostname = string.split(srv, ':')[0]
+ if '.' not in hostname:
+ Entry.Notice = 'Server hostname "%s" incomplete.' % hostname
+ else:
+ import socket
+ try:
+ result = socket.gethostbyaddr(hostname)
+ except socket.error, e:
+ Entry.Notice = ('%s: %s' % (hostname, e))
+ else:
+ if result[0] == 'projects.sourceforge.net': # ????
+ Entry.Notice = ('Server hostname "%s" does not exist.' %
+ hostname)
+ else:
+ e.server = srv
+ e.icon = ''
+ e.desc = desc
+ e.orig = REMOTE_ADDR
+ e.dele1 = e.dele2 = ''
+ e.write(f)
+ published.append((e.pos, e))
+ Entry.Notice = 'a'
+
+ f.close()
+
+ published.sort()
+ return map(lambda (pos, e): e, published)
+
+
+def publish_list(serverlist):
+ url = (fieldlist('url', identity) or ['http://127.0.0.1:8000'])[0]
+ query = []
+ for s in fieldlist('redirected'):
+ query.append('redirected=' + s)
+ for s in serverlist:
+ query.append('s=' + txtfilter(s.server))
+ url = url + '?' + string.join(query, '&')
+ for s in fieldlist('frag'):
+ url = url + '#' + s
+ print 'Content-Type: text/html'
+ print 'Location:', url
+ print
+ print '<html><head></head><body>'
+ print 'Please <a href="%s">click here</a> to continue.' % url
+ print '</body></html>'
+
+def publish_default(serverlist):
+ import htmlentitydefs
+ text_to_html = {}
+ for key, value in htmlentitydefs.entitydefs.items():
+ text_to_html[value] = '&' + key + ';'
+ for i in range(32):
+ text_to_html[chr(i)] = '?'
+ def htmlquote(s, getter=text_to_html.get):
+ lst = []
+ for c in s: lst.append(getter(c, c))
+ return string.join(lst, '')
+
+ REMOTE_ADDR = os.environ['REMOTE_ADDR']
+ f = open('started.html', 'r')
+ header, row, footer = string.split(f.read(), '\\')
+ f.close()
+ import sys
+ print 'Content-Type: text/html'
+ print
+ sys.stdout.write(header % "List of registered Internet Servers")
+ counter = 0
+ for s in serverlist:
+ if s.icon:
+ s1 = '<img width=32 height=32 src="/images/%s">' % s.icon
+ else:
+ s1 = ''
+ lst = string.split(txtfilter(s.server), ':')
+ hostname, port, udpport, httpport = (lst+['?','?','?','?'])[:4]
+ s2 = '<strong>%s</strong>' % hostname
+ try:
+ int(httpport)
+ except ValueError:
+ pass
+ else:
+ s2 = '<a href="http://%s:%s/">%s:%s</a>' % (hostname, httpport,
+ s2, port)
+ s2 = '<font size=+1>' + s2 + '</font>'
+ if s.desc:
+ s2 = s2 + '&nbsp;&nbsp;&nbsp;playing&nbsp;&nbsp;<strong>%s</strong>' % htmlquote(s.desc)
+ if goodmatch(REMOTE_ADDR, s.orig):
+ s2 = s2 + '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="bb12.py?d=%s"><i>(if this server is dead, click here to remove it)</i></a>' % s.server
+ sys.stdout.write(row % (s1, ('#C0D0D0', '#E0D0A8')[counter&1], s2))
+ counter = counter + 1
+ if not serverlist:
+ sys.stdout.write(row % ('', '#FFFFFF', 'There is no registered server at the moment.'))
+ sys.stdout.write(footer % "If your browser understands Java, you can click on a server to join the game. Note however that you will still need to install the whole Python client to benefit from the background musics, as this feature is missing from the Java client.<br><br>This list might contain already-dead servers; such 'zombies' disappear after some time.")
+
+def publish_raw(serverlist):
+ print 'Content-Type: text/plain'
+ print
+ print 'Raw list produced for', os.environ['REMOTE_ADDR']
+ print
+ for s in serverlist:
+ print repr((s.server, s.desc, s.icon, s.orig))
+
+def publish_img(serverlist):
+ import sys
+ print 'Content-Type: image/png'
+ print
+ f = open('sfbub.png', 'rb')
+ sys.stdout.write(f.read())
+ f.close()
+
+def publish_register(serverlist):
+ if Entry.Notice == 'a':
+ banner = 'The game server is now registered to SourceForge.'
+ elif Entry.Notice == 'd':
+ banner = ('Server <font color="#FF8000">unregistered</font> '
+ 'from SourceForge.')
+ elif Entry.Notice == '':
+ if fieldlist('a'):
+ banner = "The game server is already registered to SourceForge."
+ elif fieldlist('d'):
+ banner = 'The game server was <font color="#FF8000">already absent</font> from SourceForge.'
+ else:
+ publish_default(serverlist)
+ return
+ else: # errors
+ banner = ('%s<br><br>' % Entry.Notice +
+ 'If you are behind a firewall or NAT device (e.g. ADSL routers) you can still make your server reachable but it requires manual configuration. (Instructions not available yet -- sorry)')
+ f = open('started.html', 'r')
+ header, row, footer = string.split(f.read(), '\\')
+ f.close()
+ import sys
+ print 'Content-Type: text/html'
+ print
+ sys.stdout.write(header % banner)
+ sys.stdout.write(footer % 'Press <a href="javascript: back()">Back</a> to come back to the main page.')
+
+
+try:
+ slist = main()
+ cmd = (fieldlist('cmd') or ['?'])[0]
+ publish = globals().get('publish_'+cmd, publish_default)
+ publish(slist)
+except:
+ import traceback, sys
+ print "Content-Type: text/plain"
+ print
+ print "ERROR REPORT"
+ print
+ traceback.print_exc(file=sys.stdout)
diff --git a/http2/sf/sfbub.png b/http2/sf/sfbub.png
new file mode 100644
index 0000000..142f3b3
--- /dev/null
+++ b/http2/sf/sfbub.png
Binary files differ
diff --git a/http2/sf/started.html b/http2/sf/started.html
new file mode 100644
index 0000000..14c24e3
--- /dev/null
+++ b/http2/sf/started.html
@@ -0,0 +1,44 @@
+<html>
+<head><title>The (old) Bub's Brothers on SourceForge</title>
+</head>
+<body text="#000000" bgcolor="#C0FFC0" link="#0000EE" vlink="#000099" alink="#FF0000">
+
+<h1>The (old) Bub's Brothers on SourceForge</h1>
+
+<br>
+
+<h2>This meta-server is now obsolete</h2>
+<p>It has never been too useful because nowadays everybody is behind firewalls and NAT translation devices (e.g. ADSL routers). So:</p>
+<p><strong>There is a much better meta-server <a href="http://ctpug.org.za:8050/">somewhere else</a> that works with the <a href="http://bub-n-bros.sourceforge.net/download.html">next version of Bub-n-bros</a>. Upgrade to Bub-n-bros 1.3 and enjoy !</strong></p>
+<p>This page is left here just in case it is useful to someone, but you should really update if you want to enjoy a cool meta-server <code>:-)</code></p>
+
+<br><hr><br><hr><br>
+
+<table width="100%%" border=0 cellspacing=6>
+<tr>
+ <td width="10%%" align="center"><img src="../images/sfbub.png"></td>
+ <td width="90%%" bgcolor="#008000"><font color="#FFFF00" size=+1><strong>&nbsp;&nbsp;&nbsp;%s</strong></font></td>
+</form>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <td>&nbsp;</td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <td bgcolor="#FFFFFF"><table width="100%%" border=0 cellspacing=5>
+\
+<tr><td align="right">%s&nbsp;&nbsp;&nbsp;</td>
+<td bgcolor="%s" valign="center">&nbsp;&nbsp;&nbsp;&nbsp;%s</td>
+</tr>
+\
+ </table></td>
+</tr>
+
+</table>
+<br><hr><br>
+<p><font size=-1>%s</font></p>
+<p align="right"><a href="http://sourceforge.net"><img src="http://sourceforge.net/sflogo.php?group_id=72187&amp;type=2" width="125" height="37" border="0" alt="SourceForge.net Logo" /></a></p>
+</body></html>
diff --git a/java/.cvsignore b/java/.cvsignore
new file mode 100644
index 0000000..539da74
--- /dev/null
+++ b/java/.cvsignore
@@ -0,0 +1 @@
+*.py[co]
diff --git a/java/Makefile b/java/Makefile
new file mode 100644
index 0000000..a24bc68
--- /dev/null
+++ b/java/Makefile
@@ -0,0 +1,2 @@
+pclient.class: pclient.java
+ javac -target 1.1 pclient.java
diff --git a/java/pclient.java b/java/pclient.java
new file mode 100644
index 0000000..7e75536
--- /dev/null
+++ b/java/pclient.java
@@ -0,0 +1,1196 @@
+import java.applet.*;
+import java.awt.*;
+import java.awt.image.*;
+import java.awt.event.*;
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import java.util.zip.*;
+import java.lang.*;
+
+
+public class pclient extends Applet {
+
+ // Utilities
+
+ public static String[] splitString(String s, char delim) {
+ // StringTokenizer drops empty tokens :-(
+ int count = 1;
+ int length = s.length();
+ for (int i=0; i<length; i++) {
+ if (s.charAt(i) == delim)
+ count++;
+ }
+ String[] result = new String[count];
+ int origin = 0;
+ count = 0;
+ for (int i=0; i<length; i++) {
+ if (s.charAt(i) == delim) {
+ result[count++] = s.substring(origin, i);
+ origin = i+1;
+ }
+ }
+ result[count] = s.substring(origin);
+ return result;
+ }
+
+ public static String readLine(InputStream st) throws IOException {
+ String result = "";
+ int c;
+ while ((c = st.read()) != (byte) '\n') {
+ if (c < 0)
+ throw new IOException("unexpected end of stream");
+ result += (char) c;
+ }
+ return result.trim();
+ }
+
+ public static void readAll(InputStream st, byte[] buffer, int off, int len)
+ throws IOException {
+ while (len > 0) {
+ int count = st.read(buffer, off, len);
+ if (count <= 0)
+ throw new IOException("unexpected end of data");
+ off += count;
+ len -= count;
+ }
+ }
+
+ public static Color makeColor(int color) {
+ return new Color(color & 0xFF,
+ (color >> 8) & 0xFF,
+ (color >> 16) & 0xFF);
+ }
+
+ public static InputStream decompresser(byte[] data, int off, int len) {
+ return new InflaterInputStream(new ByteArrayInputStream(data, off, len));
+ }
+
+ /*class ArraySlice {
+ public byte[] array;
+ public int ofs, size;
+ ArraySlice(byte[] aarray, int aofs, int asize) {
+ array = aarray;
+ ofs = aofs;
+ size = asize;
+ }
+ }*/
+
+ public void debug(Throwable e) {
+ showStatus(e.toString());
+ e.printStackTrace();
+ }
+
+ // Bitmaps and icons
+
+ class Bitmap {
+
+ int w, h;
+ public int[] pixelData;
+
+ Bitmap(pclient client, InputStream st, int keycol) throws IOException {
+ String line = readLine(st);
+ if (!"P6".equals(line)) throw new IOException("not a P6 PPM image");
+ while ((line = readLine(st)).startsWith("#"))
+ ;
+ String[] wh = splitString(line, ' ');
+ if (wh.length != 2) throw new IOException("invalid PPM image size");
+ w = Integer.parseInt(wh[0]);
+ h = Integer.parseInt(wh[1]);
+ line = readLine(st);
+ if (!"255".equals(line))
+ throw new IOException("not a 255-levels PPM image");
+
+ // over-allocate an extra uninitialized line at the bottom of the
+ // image to work around a bug in the MemoryImageSource constructor
+ pixelData = new int[w*(h+1)];
+ int target = 0;
+ int w3 = 3*w;
+ byte[] lineBuffer = new byte[w3];
+ for (int y=0; y<h; y++) {
+ readAll(st, lineBuffer, 0, w3);
+ for (int x3=0; x3<w3; x3+=3) {
+ int rgb = (((((int) lineBuffer[x3] ) & 0xFF) << 16) |
+ ((((int) lineBuffer[x3+1]) & 0xFF) << 8 ) |
+ ((((int) lineBuffer[x3+2]) & 0xFF) ));
+ if (rgb == keycol)
+ rgb = 0;
+ else
+ rgb |= 0xFF << 24;
+ pixelData[target++] = rgb;
+ }
+ }
+ }
+
+ public Image extractIcon(int x1, int y1, int w1, int h1) {
+ return createImage(new MemoryImageSource(w1, h1, pixelData,
+ x1 + y1*w, w));
+ }
+ }
+
+ // Host choosing
+
+ public static final int defaultPort = 8056;
+ public static final String pingMessage = "pclient-game-ping";
+ public static final String pongMessage = "server-game-pong";
+
+ public Socket pickHost(String udphostname, int port)
+ throws IOException {
+ InetAddress addr = InetAddress.getByName(udphostname);
+ byte[] msg = pingMessage.getBytes("UTF8");
+ DatagramPacket outp = new DatagramPacket(msg, msg.length, addr, port);
+ byte[] buffer = new byte[200];
+ DatagramPacket inp = new DatagramPacket(buffer, buffer.length);
+ DatagramSocket s = new DatagramSocket();
+ showStatus("Looking for a game server on "+udphostname+":"+
+ Integer.toString(port)+"...");
+ s.send(outp);
+
+ s.receive(inp);
+ String inpmsg = new String(inp.getData(), 0, inp.getLength(), "UTF8");
+ String[] data = splitString(inpmsg, ':');
+ //System.out.println(inpmsg);
+ //System.out.println(data.length);
+ //System.out.println("<<<"+data[0]+">>>");
+ if (data.length >= 4 && pongMessage.equals(data[0])) {
+ InetAddress result;
+ if (data[2].length() == 0) {
+ result = inp.getAddress();
+ }
+ else {
+ result = InetAddress.getByName(data[2]);
+ }
+ port = Integer.parseInt(data[3]);
+ showStatus("Connecting to "+data[1]+" at "+
+ result.toString()+":"+Integer.toString(port)+"...");
+ return new Socket(result, port);
+ }
+ else
+ throw new IOException("got an unexpected answer from " +
+ inp.getAddress().toString());
+ }
+
+ // Game state
+
+ class Player {
+ public int pid;
+ public boolean playing;
+ public boolean local;
+ public Image icon;
+ public int xmin, xmax;
+ }
+
+ class KeyName {
+ public String keyname;
+ public int keyid;
+ public Image[] keyicons;
+ public KeyName next;
+ public int newkeycode;
+ }
+
+ public Player[] players = new Player[0];
+ public KeyName keys = null;
+ public Hashtable keycodes = new Hashtable();
+ public boolean taskbarfree = false;
+ public boolean taskbarmode = false;
+ public KeyName keydefinition_k = null;
+ public int keydefinition_pid;
+ public Image[] iconImages = new Image[0];
+ public Bitmap[] bitmaps = new Bitmap[0];
+
+ public Player getPlayer(int id) {
+ if (id >= players.length) {
+ Player[] newply = new Player[id+1];
+ System.arraycopy(players, 0, newply, 0, players.length);
+ players = newply;
+ }
+ if (players[id] == null) {
+ players[id] = new Player();
+ players[id].pid = id;
+ players[id].playing = false;
+ players[id].local = false;
+ }
+ return players[id];
+ }
+
+ public Player nextPlayer(Player prev) {
+ int i;
+ for (i=prev.pid+1; i<players.length; i++)
+ if (players[i] != null)
+ return players[i];
+ return null;
+ }
+
+ public Player firstPlayer() {
+ int i;
+ for (i=0; i<players.length; i++)
+ if (players[i] != null)
+ return players[i];
+ return null;
+ }
+
+ public void setTaskbar(boolean nmode) {
+ if (taskbarfree) {
+ Player p;
+ boolean nolocalplayer = true;
+ for (p=firstPlayer(); p!=null; p=nextPlayer(p))
+ if (p.local)
+ nolocalplayer = false;
+ boolean prevmode = taskbarmode;
+ taskbarmode = nmode || nolocalplayer || keydefinition_k != null;
+ if (prevmode != taskbarmode)
+ repaint();
+ }
+ }
+
+ public final Image getIcon(int ico) {
+ if (ico < 0 || ico >= iconImages.length)
+ return null;
+ else
+ return iconImages[ico];
+ }
+
+ public void setIcon(int ico, Image img) {
+ if (ico >= iconImages.length) {
+ Image[] newico = new Image[ico+1];
+ System.arraycopy(iconImages, 0, newico, 0, iconImages.length);
+ iconImages = newico;
+ }
+ iconImages[ico] = img;
+ }
+
+ public void setBitmap(int n, Bitmap bmp) {
+ if (n >= bitmaps.length) {
+ Bitmap[] newbmp = new Bitmap[n+1];
+ System.arraycopy(bitmaps, 0, newbmp, 0, bitmaps.length);
+ bitmaps = newbmp;
+ }
+ bitmaps[n] = bmp;
+ }
+
+ // Sprites
+
+ class Sprite {
+ public int x, y, ico;
+ public Image bkgnd;
+
+ public final boolean draw(pclient client, Image backBuffer,
+ Graphics backGC) {
+ Image iconImage = client.getIcon(ico);
+ if (iconImage == null) {
+ ico = -1;
+ return false;
+ }
+ int w = iconImage.getWidth(client);
+ int h = iconImage.getHeight(client);
+
+ if (bkgnd == null || bkgnd.getWidth(client) != w ||
+ bkgnd.getHeight(client) != h) {
+ bkgnd = client.createImage(w, h);
+ }
+ bkgnd.getGraphics().drawImage(backBuffer, -x, -y, client);
+ backGC.drawImage(iconImage, x, y, client);
+ //System.out.println("Draw at "+Integer.toString(x)+", "+
+ // Integer.toString(y));
+ return true;
+ }
+
+ public final void erase(pclient client, Graphics backGC) {
+ if (ico != -1) {
+ //System.out.println("Erase at "+Integer.toString(x)+", "+
+ // Integer.toString(y));
+ backGC.drawImage(bkgnd, x, y, client);
+ }
+ }
+ }
+
+ // Playfield
+
+ class Playfield {
+ public static final int TASKBAR_HEIGHT = 48;
+
+ public pclient client;
+ public int pfwidth, pfheight;
+ public Image backBuffer;
+ public Sprite[] sprites = new Sprite[0];
+ public int numSprites = 0;
+ public byte[] pendingBuffer = new byte[SocketDisplayer.UDP_BUF_SIZE];
+ public int pendingBufOfs = 0;
+ public int pendingBufLen = 0;
+ public byte[] spriteData = new byte[0];
+ public int validDataLen = 0;
+ public Image tbCache;
+ public AudioClip[] samples = new AudioClip[0];
+ public int[] playingSounds = new int[0];
+
+ Playfield(pclient aclient, int width, int height, Color bkgnd) {
+ client = aclient;
+ pfwidth = width;
+ pfheight = height;
+ backBuffer = createImage(width, height);
+ Graphics backGC = backBuffer.getGraphics();
+ backGC.setColor(bkgnd);
+ backGC.fillRect(0, 0, width, height);
+ backGC.dispose();
+ client.resize(width, height);
+ client.setBackground(bkgnd);
+
+ int[] pixelData = new int[32*TASKBAR_HEIGHT];
+ int target = 0;
+ for (int y=0; y<TASKBAR_HEIGHT; y++) {
+ int alpha = y * 256 / TASKBAR_HEIGHT;
+ int rgb = 0x8080FF | (alpha<<24);
+ for (int x=0; x<32; x++)
+ pixelData[target++] = rgb;
+ }
+ tbCache = createImage(new MemoryImageSource(32, TASKBAR_HEIGHT,
+ pixelData, 0, 32));
+ }
+
+ public synchronized byte[] setSprites(byte[] buf, int buflen) {
+ byte[] old = pendingBuffer;
+ pendingBuffer = buf;
+ //System.out.println("UDP packet for "+Integer.toString(buflen/6));
+
+ /* sound support -- no volume */
+ int base = 0;
+ int[] currentSounds = new int[samples.length];
+ while (base+6 <= buflen &&
+ pendingBuffer[base+4] == -1 &&
+ pendingBuffer[base+5] == -1) {
+ int key = pendingBuffer[base+1];
+ key = (key & 0xFF) | (((int) pendingBuffer[base]) << 8);
+ if (0 <= key && key <= 9999) { /* safety bound check */
+ if (key >= samples.length) {
+ AudioClip[] newclip = new AudioClip[key+5];
+ System.arraycopy(samples, 0, newclip, 0, samples.length);
+ samples = newclip;
+ }
+ if (samples[key] == null) {
+ String filename = "sample.wav?code=" +
+ Integer.toString(key);
+ samples[key] = getAudioClip(getCodeBase(), filename);
+ }
+ else if (playingSounds.length > key &&
+ playingSounds[key] > 0) {
+ currentSounds[key] = playingSounds[key] - 1;
+ }
+ else {
+ samples[key].play();
+ currentSounds[key] = 4;
+ }
+ }
+ base += 6;
+ }
+ playingSounds = currentSounds;
+ pendingBufOfs = base;
+ pendingBufLen = buflen - base;
+ return old;
+ }
+
+ public synchronized int fetchSprites() {
+ int valid;
+ int count = (pendingBufLen < validDataLen) ? pendingBufLen
+ : validDataLen;
+
+ for (valid=0; valid<count; valid++)
+ if (spriteData[valid] != pendingBuffer[pendingBufOfs+valid])
+ break;
+ validDataLen = valid;
+
+ if (pendingBufLen > spriteData.length) {
+ spriteData = new byte[pendingBufLen+90];
+ valid = 0;
+ }
+ System.arraycopy(pendingBuffer, pendingBufOfs+valid,
+ spriteData, valid, pendingBufLen-valid);
+ return pendingBufLen;
+ }
+
+ public void paint(Graphics g) {
+ int buflen = fetchSprites();
+ byte[] buffer = spriteData;
+ int count = validDataLen / 6;
+ int base = count * 6;
+ int nspr = buflen / 6;
+ Image tback;
+
+ if (nspr > sprites.length) {
+ Sprite[] newspr = new Sprite[nspr+15];
+ System.arraycopy(sprites, 0, newspr, 0, sprites.length);
+ for (int i=sprites.length; i<newspr.length; i++)
+ newspr[i] = new Sprite();
+ sprites = newspr;
+ }
+
+ //System.out.println("drawImage: -"+
+ // Integer.toString(numSprites-count)+
+ // " +"+Integer.toString(nspr-count));
+
+ // erase extra sprites
+ Graphics backGC = backBuffer.getGraphics();
+ for (int i=numSprites-1; i>=count; i--) {
+ sprites[i].erase(client, backGC);
+ }
+
+ // draw new sprites
+ validDataLen = pendingBufLen;
+ while (count < nspr) {
+ Sprite s = sprites[count++];
+ int x = buffer[base+1];
+ s.x = (x & 0xFF) | (((int) buffer[base ]) << 8);
+ int y = buffer[base+3];
+ s.y = (y & 0xFF) | (((int) buffer[base+2]) << 8);
+ int ico = buffer[base+5];
+ s.ico = (ico & 0xFF) | (((int) buffer[base+4]) << 8);
+ if (!s.draw(client, backBuffer, backGC)) {
+ if (base < validDataLen)
+ validDataLen = base;
+ //System.out.println(Integer.toString(s.x)+';'+
+ // Integer.toString(s.y)+';'+
+ // Integer.toString(s.ico));
+ }
+ base += 6;
+ }
+ numSprites = count;
+
+ if (client.taskbarmode)
+ tback = paintTaskbar(backGC);
+ else
+ tback = null;
+
+ g.drawImage(backBuffer, 0, 0, client);
+
+ if (tback != null)
+ eraseTaskbar(backGC, tback);
+ backGC.dispose();
+ }
+
+ public Image paintTaskbar(Graphics g) {
+ boolean animated = false;
+ int y0 = pfheight - TASKBAR_HEIGHT;
+ Image bkgnd = client.createImage(pfwidth, TASKBAR_HEIGHT);
+ bkgnd.getGraphics().drawImage(backBuffer, 0, -y0, client);
+ for (int i=0; i<pfwidth; i+=32)
+ g.drawImage(tbCache, i, y0, client);
+
+ double f = 0.0015 * new Date().getTime();
+ f = f - (int) f;
+ double f2 = f * (1.0-f) * 4.0;
+
+ int lpos = 0;
+ int rpos = pfwidth;
+ for (Player p=firstPlayer(); p!=null; p=nextPlayer(p))
+ if (p.icon != null) {
+ Image ico = p.icon;
+ int x, y;
+ int w = ico.getWidth(client);
+ int h = ico.getHeight(client);
+ int dx = w * 5 / 3;
+ p.xmin = p.xmax = 0;
+
+ if (p.local) {
+ rpos -= dx;
+ int dy = TASKBAR_HEIGHT - h - 1;
+ x = rpos;
+ y = pfheight - h - (int)(dy*f2);
+ animated = true;
+ }
+ else {
+ lpos += dx;
+ if (p.playing)
+ continue;
+ x = lpos - w;
+ y = pfheight - h;
+ if (keydefinition_k != null &&
+ keydefinition_pid == p.pid) {
+ Image[] icons = keydefinition_k.keyicons;
+ if (icons.length > 0) {
+ int index = (int)(f*icons.length);
+ ico = icons[index % icons.length];
+ animated = true;
+ }
+ y = y0 + (TASKBAR_HEIGHT-h)/2;
+ }
+ }
+ p.xmin = x;
+ p.xmax = x + w;
+ g.drawImage(ico, x, y, client);
+ }
+
+ if (animated)
+ repaint(50);
+ return bkgnd;
+ }
+
+ public void eraseTaskbar(Graphics g, Image bkgnd) {
+ int y0 = pfheight - TASKBAR_HEIGHT;
+ g.drawImage(bkgnd, 0, y0, client);
+ }
+ }
+
+ // Socket listener
+
+ class SocketListener extends Thread {
+
+ public pclient client;
+ public Socket socket;
+ public InputStream socketInput;
+ public OutputStream socketOutput;
+
+ public static final String MSG_WELCOME = "Welcome to gamesrv.py(3) !\n";
+ public static final byte MSG_DEF_PLAYFIELD = (byte) 'p';
+ public static final byte MSG_DEF_KEY = (byte) 'k';
+ public static final byte MSG_DEF_ICON = (byte) 'r';
+ public static final byte MSG_DEF_BITMAP = (byte) 'm';
+ public static final byte MSG_DEF_SAMPLE = (byte) 'w';
+ public static final byte MSG_DEF_MUSIC = (byte) 'z';
+ public static final byte MSG_PLAY_MUSIC = (byte) 'Z';
+ public static final byte MSG_FADEOUT = (byte) 'f';
+ public static final byte MSG_PLAYER_JOIN = (byte) '+';
+ public static final byte MSG_PLAYER_KILL = (byte) '-';
+ public static final byte MSG_PLAYER_ICON = (byte) 'i';
+ public static final byte MSG_PING = (byte) 'g';
+ public static final byte MSG_PONG = (byte) 'G';
+ public static final byte MSG_INLINE_FRAME = (byte) '\\';
+
+ public static final byte CMSG_KEY = (byte) 'k';
+ public static final byte CMSG_ADD_PLAYER = (byte) '+';
+ public static final byte CMSG_REMOVE_PLAYER= (byte) '-';
+ public static final byte CMSG_UDP_PORT = (byte) '<';
+ public static final byte CMSG_ENABLE_SOUND = (byte) 's';
+ public static final byte CMSG_ENABLE_MUSIC = (byte) 'm';
+ public static final byte CMSG_PING = (byte) 'g';
+ public static final byte CMSG_PONG = (byte) 'G';
+ public static final byte CMSG_PLAYER_NAME = (byte) 'n';
+
+ public void connectionClosed() throws IOException {
+ throw new IOException("connection closed");
+ }
+
+ public void protocolError() throws IOException {
+ throw new IOException("protocol error");
+ }
+
+ SocketListener(pclient aclient, Socket asocket) throws IOException {
+ setDaemon(true);
+ byte[] msgWelcome = MSG_WELCOME.getBytes("UTF8");
+
+ client = aclient;
+ socket = asocket;
+ socketInput = socket.getInputStream();
+ socketOutput = socket.getOutputStream();
+
+ for (int i=0; i<msgWelcome.length; i++) {
+ int recv = socketInput.read();
+ if (recv != msgWelcome[i])
+ throw new IOException
+ ("connected to something not a game server");
+ }
+ showStatus("Let's "+client.readLine(socketInput)+"!");
+ }
+
+ public void sendData(byte[] buffer, int ofs, int size)
+ throws IOException {
+ socketOutput.write(buffer, ofs, size);
+ }
+
+ public byte[] codeMessage(int p, byte msgcode, int[] args,
+ String lastarg) {
+ int bufsize = 1;
+ String mode = "";
+ for (int i=0; i<args.length; i++) {
+ mode = mode + "l";
+ bufsize = bufsize + 4;
+ }
+ if (lastarg != null) {
+ mode = mode + Integer.toString(lastarg.length()) + "s";
+ bufsize = bufsize + lastarg.length();
+ }
+ byte[] buffer = new byte[p + 1 + mode.length() + bufsize];
+ buffer[p++] = (byte) mode.length();
+ for (int i=0; i<mode.length(); i++) {
+ buffer[p++] = (byte) mode.charAt(i);
+ }
+ buffer[p++] = msgcode;
+ for (int i=0; i<args.length; i++) {
+ int n;
+ int value = args[i];
+ buffer[p++] = (byte) (value >> 24);
+ n = value >> 16;
+ buffer[p++] = (byte)(((n&0x80) == 0) ? n&0x7F : n|0xFFFFFF80);
+ n = value >> 8;
+ buffer[p++] = (byte)(((n&0x80) == 0) ? n&0x7F : n|0xFFFFFF80);
+ n = value;
+ buffer[p++] = (byte)(((n&0x80) == 0) ? n&0x7F : n|0xFFFFFF80);
+ }
+ if (lastarg != null) {
+ for (int i=0; i<lastarg.length(); i++) {
+ buffer[p++] = (byte) lastarg.charAt(i);
+ }
+ }
+ return buffer;
+ }
+
+ public void sendMessage(byte msgcode, int[] args) throws IOException {
+ sendMessageEx(msgcode, args, null);
+ }
+
+ public void sendMessageEx(byte msgcode, int[] args, String lastarg)
+ throws IOException {
+ byte[] buffer = codeMessage(0, msgcode, args, lastarg);
+ sendData(buffer, 0, buffer.length);
+ }
+
+ public int decodeMessage(byte[] buffer, int ofs, int end)
+ throws IOException {
+ if (ofs == end) return -1;
+ int typecodes = buffer[ofs]; typecodes &= 0xFF;
+ int base = ofs+1+typecodes;
+ if (base >= end) return -1;
+ byte msgcode = buffer[base++];
+ int[] args = new int[typecodes];
+ int repeatcount = 0;
+ int nargs = 0;
+ for (int i=0; i<typecodes; i++) {
+ byte c = buffer[ofs+1+i];
+ if (c == (byte) 'B') {
+ if (base+1 > end)
+ return -1;
+ args[nargs++] = ((int) buffer[base++]) & 0xFF;
+ }
+ else if (c == (byte) 'l') {
+ if (base+4 > end)
+ return -1;
+ int n4 = buffer[base++];
+ int n3 = buffer[base++]; n3 &= 0xFF;
+ int n2 = buffer[base++]; n2 &= 0xFF;
+ int n1 = buffer[base++]; n1 &= 0xFF;
+ int value = n1 | (n2<<8) | (n3<<16) | (n4<<24);
+ //System.out.println(n4);
+ //System.out.println(n3);
+ //System.out.println(n2);
+ //System.out.println(n1);
+ //System.out.println(value);
+ //System.out.println();
+ args[nargs++] = value;
+ }
+ else if ((byte) '0' <= c && c <= (byte) '9') {
+ repeatcount = repeatcount*10 + (c - (byte) '0');
+ }
+ else if (c == (byte) 's') {
+ if (base+repeatcount > end)
+ return -1;
+ args[nargs++] = base;
+ args[nargs++] = repeatcount;
+ base += repeatcount;
+ repeatcount = 0;
+ }
+ else
+ protocolError();
+ }
+
+ //System.out.print("Message ");
+ //System.out.print((char) msgcode);
+ //for (int i=0; i<nargs; i++)
+ // System.out.print(" " + Integer.toString(args[i]));
+ //System.out.println();
+
+ switch (msgcode) {
+
+ case MSG_PLAYER_JOIN: {
+ int id = args[0];
+ int local = args[1];
+ Player p = client.getPlayer(id);
+ p.playing = true;
+ p.local = local != 0;
+ if (p.local)
+ client.setTaskbar(false);
+ break;
+ }
+ case MSG_PLAYER_KILL: {
+ int id = args[0];
+ Hashtable nkeycodes = new Hashtable();
+ Player p = client.getPlayer(id);
+ p.playing = false;
+ p.local = false;
+ for (Enumeration e = client.keycodes.keys();
+ e.hasMoreElements(); ) {
+ Object key = e.nextElement();
+ byte[] msg = (byte[]) keycodes.get(key);
+ if (msg[0] != id)
+ nkeycodes.put(key, msg);
+ }
+ client.keycodes = nkeycodes;
+ break;
+ }
+ case MSG_DEF_PLAYFIELD: {
+ int width = args[0];
+ int height = args[1];
+ Color bkgnd = client.makeColor(args[2]);
+ client.playfield = new pclient.Playfield(client, width, height,
+ bkgnd);
+ int[] singleint = new int[1];
+ singleint[0] = -1;
+ sendMessage(CMSG_ENABLE_SOUND, singleint);
+ sendMessage(CMSG_PING, new int[0]);
+ break;
+ }
+ case MSG_DEF_KEY: {
+ int i;
+ int nameofs = args[0];
+ int namelen = args[1];
+ int keyid = args[2];
+ int nicons = nargs - 3;
+ KeyName key = new KeyName();
+ key.keyname = new String(buffer, nameofs, namelen, "UTF8");
+ key.keyid = keyid;
+ key.keyicons= new Image[nicons];
+ for (i=0; i<nicons; i++)
+ key.keyicons[i] = client.getIcon(args[3+i]);
+ if (client.keys == null || keyid < client.keys.keyid) {
+ key.next = client.keys;
+ client.keys = key;
+ }
+ else {
+ KeyName k = client.keys;
+ while (k.next != null && k.next.keyid < keyid)
+ k = k.next;
+ key.next = k.next;
+ k.next = key;
+ }
+ break;
+ }
+ case MSG_DEF_ICON: {
+ int bmpcode = args[0];
+ int icocode = args[1];
+ int ix = args[2];
+ int iy = args[3];
+ int iw = args[4];
+ int ih = args[5];
+ if (bmpcode < client.bitmaps.length) {
+ Bitmap bmp = client.bitmaps[bmpcode];
+ if (bmp != null)
+ client.setIcon(icocode,
+ bmp.extractIcon(ix, iy, iw, ih));
+ }
+ break;
+ }
+ case MSG_DEF_BITMAP: {
+ int bmpcode = args[0];
+ int dataofs = args[1];
+ int datalen = args[2];
+ int colorkey = (nargs > 3) ? args[3] : -1;
+ InputStream st = decompresser(buffer, dataofs, datalen);
+ client.setBitmap(bmpcode, new Bitmap(client, st, colorkey));
+ break;
+ }
+ case MSG_PLAYER_ICON: {
+ int pid = args[0];
+ int icocode = args[1];
+ Player p = client.getPlayer(pid);
+ p.icon = client.getIcon(icocode);
+ break;
+ }
+ case MSG_PING: {
+ buffer[ofs+1+typecodes] = CMSG_PONG;
+ sendData(buffer, ofs, base-ofs);
+ if (nargs > 0 && !client.udpovertcp) {
+ int udpkbytes = args[0];
+ /* switch to udp_over_tcp if the udp socket didn't
+ receive at least 60% of the packets sent by the server,
+ or if the socketdisplayer thread died */
+ if (sockdisplayer != null && !sockdisplayer.isAlive()) {
+ showStatus("routing UDP traffic over TCP (no UDP socket)");
+ client.start_udp_over_tcp();
+ }
+ else if (udpkbytes * 1024.0 * 0.60 > client.udpbytecounter) {
+ client.udpsock_low += 1;
+ if (client.udpsock_low >= 4) {
+ double inp =client.udpbytecounter/(udpkbytes*1024.0);
+ int loss = (int)(100.0*(1.0-inp));
+ showStatus("routing UDP traffic over TCP (" +
+ Integer.toString(loss) +
+ "% packet loss)");
+ client.start_udp_over_tcp();
+ }
+ }
+ else
+ client.udpsock_low = 0;
+ }
+ break;
+ }
+ case MSG_PONG: {
+ if (!client.taskbarfree && !client.taskbarmode) {
+ client.taskbarfree = true;
+ client.setTaskbar(true);
+ }
+ break;
+ }
+ case MSG_INLINE_FRAME: {
+ if (client.uinflater != null) {
+ int dataofs = args[0];
+ int datalen = args[1];
+ int len;
+ byte[] pkt = client.uinflater_buffer;
+ client.uinflater.setInput(buffer, dataofs, datalen);
+ try {
+ len = client.uinflater.inflate(pkt);
+ }
+ catch (DataFormatException e) {
+ len = 0;
+ }
+ Playfield pf = client.playfield;
+ if (len > 0 && pf != null) {
+ client.uinflater_buffer = pf.setSprites(pkt, len);
+ client.repaint();
+ }
+ }
+ break;
+ }
+ default: {
+ System.err.println("Note: unknown message " +
+ Byte.toString(msgcode));
+ break;
+ }
+ }
+ return base;
+ }
+
+ public void run() {
+ try {
+ byte[] buffer = new byte[0xC000];
+ int begin = 0;
+ int end = 0;
+ try {
+ while (true) {
+ if (end + 0x6000 > buffer.length) {
+ // compact buffer
+ byte[] newbuf;
+ end = end-begin;
+ if (end + 0x8000 > buffer.length)
+ newbuf = new byte[end + 0x8000];
+ else
+ newbuf = buffer;
+ System.arraycopy(buffer, begin, newbuf, 0, end);
+ begin = 0;
+ buffer = newbuf;
+ }
+ int count = socketInput.read(buffer, end, 0x6000);
+ if (isInterrupted())
+ break;
+ if (count <= 0)
+ connectionClosed();
+ end += count;
+ while ((count=decodeMessage(buffer, begin, end)) >= 0) {
+ begin = count;
+ }
+ }
+ }
+ catch (InterruptedIOException e) {
+ }
+ socket.close();
+ }
+ catch (IOException e) {
+ client.debug(e);
+ }
+ }
+ }
+
+ // UDP Socket messages
+
+ class SocketDisplayer extends Thread {
+ public static final int UDP_BUF_SIZE = 0x10000;
+
+ public pclient client;
+ public DatagramSocket socket;
+
+ SocketDisplayer(pclient aclient) {
+ setDaemon(true);
+ client = aclient;
+ }
+
+ public void run() {
+ /* This thread may die early, typically because of JVM
+ security restrictions. */
+ byte[] buffer = new byte[UDP_BUF_SIZE];
+ DatagramPacket pkt = new DatagramPacket(buffer, UDP_BUF_SIZE);
+ try {
+ {
+ socket = new DatagramSocket();
+ int[] args = new int[1];
+ args[0] = socket.getLocalPort();
+ client.socklistener.sendMessage(SocketListener.CMSG_UDP_PORT,
+ args);
+ }
+ try {
+ while (true) {
+ socket.receive(pkt);
+ if (isInterrupted())
+ break;
+ client.udpbytecounter += (double) pkt.getLength();
+ Playfield pf = client.playfield;
+ if (pf != null) {
+ pkt.setData(pf.setSprites(pkt.getData(),
+ pkt.getLength()));
+ pkt.setLength(UDP_BUF_SIZE);
+ client.repaint();
+ }
+ }
+ }
+ catch (InterruptedIOException e) {
+ }
+ socket.close();
+ }
+ catch (IOException e) {
+ client.debug(e);
+ }
+ }
+ }
+
+ // Applet methods
+
+ public SocketListener socklistener = null;
+ public SocketDisplayer sockdisplayer = null;
+ public Playfield playfield = null;
+
+ public void init() {
+ try {
+ Socket link;
+ String param;
+
+ String gamesrv = getParameter("gamesrv");
+ if (gamesrv == null) {
+ gamesrv = getDocumentBase().getHost();
+ }
+ param = getParameter("gameport");
+ if (param != null) {
+ // direct TCP connexion to the game server
+ link = new Socket(gamesrv, Integer.parseInt(param));
+ }
+ else {
+ // UCP query
+ param = getParameter("port");
+ int port = (param != null) ? Integer.parseInt(param) : defaultPort;
+ link = pickHost(gamesrv, port);
+ }
+ socklistener = new SocketListener(this, link);
+ socklistener.start();
+ }
+ catch (IOException e) {
+ debug(e);
+ }
+ }
+
+ public void destroy() {
+ if (socklistener != null) {
+ socklistener.interrupt();
+ socklistener = null;
+ }
+ }
+
+ public void start() {
+ enableEvents(AWTEvent.KEY_EVENT_MASK |
+ AWTEvent.MOUSE_EVENT_MASK |
+ AWTEvent.MOUSE_MOTION_EVENT_MASK);
+ if (socklistener != null) {
+ sockdisplayer = new SocketDisplayer(this);
+ sockdisplayer.start();
+ }
+ }
+
+ public void stop() {
+ if (sockdisplayer != null) {
+ sockdisplayer.interrupt();
+ sockdisplayer = null;
+ }
+ }
+
+ public void update(Graphics g) {
+ paint(g);
+ }
+
+ public void paint(Graphics g) {
+ Playfield pf = playfield;
+ if (pf != null) {
+ pf.paint(g);
+ }
+ else {
+ int appWidth = getSize().width;
+ int appHeight = getSize().height;
+ g.clearRect(0, 0, appWidth, appHeight);
+ }
+ }
+
+ protected void processKeyEvent(KeyEvent e) {
+ int num;
+ byte[] msg;
+ e.consume();
+ switch (e.getID()) {
+ case KeyEvent.KEY_PRESSED:
+ num = e.getKeyCode();
+ break;
+ case KeyEvent.KEY_RELEASED:
+ num = -e.getKeyCode();
+ break;
+ default:
+ return;
+ }
+ msg = (byte[]) keycodes.get(new Integer(num));
+ if (msg != null && socklistener != null) {
+ Player p = getPlayer(msg[0]);
+ if (p.local) {
+ try {
+ socklistener.sendData(msg, 1, msg.length-1);
+ }
+ catch (IOException ioe) {
+ debug(ioe);
+ }
+ return;
+ }
+ }
+ if (keydefinition_k != null && e.getID() == KeyEvent.KEY_PRESSED)
+ defineKey(num);
+ }
+
+ public void nextKey() {
+ KeyName k = keydefinition_k;
+ if (k == null)
+ k = keys;
+ else
+ k = k.next;
+ while (k != null && k.keyname.charAt(0) == '-') {
+ k.newkeycode = 0;
+ k = k.next;
+ }
+ keydefinition_k = k;
+ }
+
+ public void defineKey(int num) {
+ KeyName k;
+ for (k=keys; k!=keydefinition_k; k=k.next)
+ if (k.newkeycode == num)
+ return;
+ k.newkeycode = num;
+ nextKey();
+ if (keydefinition_k == null) {
+ if (socklistener != null) {
+ try {
+ byte[] buffer;
+ int[] args = new int[1];
+ args[0] = keydefinition_pid;
+ socklistener.sendMessage(SocketListener.CMSG_ADD_PLAYER,
+ args);
+ String param = "player" +
+ Integer.toString(keydefinition_pid);
+ param = getParameter(param);
+ if (param != null) {
+ socklistener.sendMessageEx(
+ SocketListener.CMSG_PLAYER_NAME,
+ args,
+ param);
+ }
+ args = new int[2];
+ args[0] = keydefinition_pid;
+ for (k=keys; k!=null; k=k.next) {
+ if (k.keyname.charAt(0) == '-') {
+ String test = k.keyname.substring(1);
+ for (KeyName r=keys; r!=null; r=r.next)
+ if (r.keyname.equals(test))
+ k.newkeycode = -r.newkeycode;
+ }
+ args[1] = k.keyid;
+ buffer = socklistener.codeMessage
+ (1, SocketListener.CMSG_KEY, args, null);
+ buffer[0] = (byte) keydefinition_pid;
+ keycodes.put(new Integer(k.newkeycode), buffer);
+ }
+ }
+ catch (IOException ioe) {
+ debug(ioe);
+ }
+ }
+ }
+ repaint();
+ }
+
+ protected void processMouseMotionEvent(MouseEvent e) {
+ Playfield pf = playfield;
+ if (pf != null)
+ setTaskbar(e.getY() >= pf.pfheight - pf.TASKBAR_HEIGHT);
+ e.consume();
+ }
+
+ protected void processMouseEvent(MouseEvent e) {
+ if (e.getID() != MouseEvent.MOUSE_PRESSED) {
+ e.consume();
+ return;
+ }
+ requestFocus();
+ keydefinition_k = null;
+ Playfield pf = playfield;
+ if (pf != null && e.getY() >= pf.pfheight - pf.TASKBAR_HEIGHT) {
+ int x = e.getX();
+ for (Player p=firstPlayer(); p!=null; p=nextPlayer(p))
+ if (p.xmin <= x && x < p.xmax) {
+ if (p.local) {
+ if (socklistener != null) {
+ try {
+ int[] args = new int[1];
+ args[0] = p.pid;
+ socklistener.sendMessage
+ (SocketListener.CMSG_REMOVE_PLAYER, args);
+ }
+ catch (IOException ioe) {
+ debug(ioe);
+ }
+ }
+ }
+ else {
+ keydefinition_pid = p.pid;
+ nextKey();
+ }
+ break;
+ }
+ }
+ e.consume();
+ repaint();
+ }
+
+ // UDP-over-TCP
+ public double udpbytecounter = 0.0;
+ public int udpsock_low = 0;
+ public boolean udpovertcp = false;
+ public Inflater uinflater = null;
+ public byte[] uinflater_buffer = null;
+
+ public void start_udp_over_tcp()
+ {
+ udpovertcp = true;
+ int[] args = new int[1];
+ args[0] = 0;
+ try {
+ socklistener.sendMessage(SocketListener.CMSG_UDP_PORT, args);
+ }
+ catch (IOException e) {
+ return;
+ }
+ uinflater_buffer = new byte[SocketDisplayer.UDP_BUF_SIZE];
+ uinflater = new Inflater();
+ if (sockdisplayer != null) {
+ sockdisplayer.interrupt();
+ sockdisplayer = null;
+ }
+ }
+
+// // ImageObserver interface
+
+// public boolean imageUpdate(Image img,
+// int infoflags,
+// int x,
+// int y,
+// int width,
+// int height) {
+// return false;
+// }
+}
diff --git a/metaserver/.cvsignore b/metaserver/.cvsignore
new file mode 100644
index 0000000..687980a
--- /dev/null
+++ b/metaserver/.cvsignore
@@ -0,0 +1,4 @@
+*.py[co]
+build
+*.so
+*.pyd
diff --git a/metaserver/__init__.py b/metaserver/__init__.py
new file mode 100644
index 0000000..1bb8bf6
--- /dev/null
+++ b/metaserver/__init__.py
@@ -0,0 +1 @@
+# empty
diff --git a/metaserver/home.png b/metaserver/home.png
new file mode 100644
index 0000000..cc7a46e
--- /dev/null
+++ b/metaserver/home.png
Binary files differ
diff --git a/metaserver/index.html b/metaserver/index.html
new file mode 100644
index 0000000..fdb5583
--- /dev/null
+++ b/metaserver/index.html
@@ -0,0 +1,51 @@
+<html>
+<head><title>The Bub's Brothers Server List</title>
+</head>
+<body text="#000000" bgcolor="#C0FFC0" link="#0000EE" vlink="#000099" alink="#FF0000">
+
+<h1>The Bub's Brothers Server List</h1>
+%s
+<br>
+
+<table border=1 cellspacing=1>
+<tr><td bgcolor="#FFFFFF">
+<table border=0 cellspacing=6>
+<tr>
+ <td bgcolor="#008000" colspan=2 align="center"><img src="mbub.png">
+ <font color="#FFFF00" size=+1><strong>&nbsp;&nbsp;&nbsp;Running servers</strong></font></td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <td>&nbsp;</td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <td bgcolor="#FFFFFF"><table border=0 cellspacing=5>
+\
+<tr>
+<td valign="bottom"><font size=-1>%(stime)s</font></td>
+<td valign="bottom">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;%(icon)s&nbsp;&nbsp;&nbsp;</td>
+<td bgcolor="%(bgcolor)s"><font size=+1>%(hosthtml)s</font>
+ <br><strong>%(desc)s</strong> (%(extradesc)s)</td>
+</tr>
+\
+ </table></td>
+</tr>
+
+</table>
+</td></tr></table>
+<font size=-2>%(tbfiles)s</font>
+<br><hr><br>
+<p><font size=-1>
+%(bottommsg)s
+<br>
+<a href="javascript: location.reload()">Reload</a> this page for an up-to-date version!
+</font></p>
+%(extrafooter)s
+<p><img src="home.png"> I click on the servers but nothing happens - <a href="http://bub-n-bros.sourceforge.net/help.html#meta">why?</a></p>
+<p><img src="home.png"> Chat about the Bub's Brothers? Go to our IRC channel <a href="irc://irc.freenode.net:6667/bub-n-bros">#bub-n-bros on irc.freenode.net</a>.</p>
+<p><img src="home.png"> <a href="http://bub-n-bros.sourceforge.net">The Bub's Brothers Home Page</a></p>
+<p><img src="home.png"> Thanks to <a href="http://ctpug.org.za">CTPUG</a> for hosting the meta-server.</p>
+</body></html>
diff --git a/metaserver/mbub.png b/metaserver/mbub.png
new file mode 100644
index 0000000..d7aaae9
--- /dev/null
+++ b/metaserver/mbub.png
Binary files differ
diff --git a/metaserver/metaclient.py b/metaserver/metaclient.py
new file mode 100644
index 0000000..aedb99c
--- /dev/null
+++ b/metaserver/metaclient.py
@@ -0,0 +1,596 @@
+import sys, os, time, random
+from select import select
+from socket import *
+from metastruct import *
+
+_SERVER = 'ctpug.org.za'
+
+METASERVER = (_SERVER, 8055)
+METASERVER_UDP = (_SERVER, 8055)
+METASERVER_URL = 'http://%s:8050/bub-n-bros.html' % (_SERVER,)
+VERSION_TAG = 1601
+
+def connect(failure=[]):
+ if len(failure) >= 2:
+ return None
+ print >> sys.stderr, 'Connecting to the meta-server %s:%d...' % METASERVER
+ try:
+ s = socket(AF_INET, SOCK_STREAM)
+ s.connect(METASERVER)
+ except error, e:
+ print >> sys.stderr, '*** cannot contact meta-server:', str(e)
+ failure.append(e)
+ return None
+ else:
+ print >> sys.stderr, 'connected.'
+ return s
+
+sys.setcheckinterval(4096)
+
+
+def float2str(f):
+ # don't trust locale issues and write a string with a '.'
+ s = str(long(f*1000000.0))
+ return s[:-6] + '.' + s[-6:]
+
+def str2float(s):
+ try:
+ return float(s)
+ except:
+ # locale issues may prevent float() from decoding the string
+ s = s.strip()
+ try:
+ i = s.index('.')
+ except ValueError:
+ try:
+ i = s.index(',')
+ except ValueError:
+ i = len(s)
+ frac = s[i+1:]
+ return float(s[:i] or '0') + float(frac or '0')/(10**len(frac))
+
+
+# ____________________________________________________________
+# Game Servers
+
+class MetaClientSrv(MessageSocket):
+
+ def __init__(self, s, game):
+ MessageSocket.__init__(self, s)
+ self.game = game
+ self.lastwakeup = None
+ self.synsockets = {}
+ import gamesrv
+ gamesrv.addsocket('META', s, self.receive)
+ self.closed = 0
+
+ def close(self):
+ if not self.closed:
+ self.disconnect()
+ try:
+ self.s.shutdown(2)
+ except:
+ pass
+
+ def disconnect(self):
+ import gamesrv
+ gamesrv.removesocket('META', self.s)
+ self.closed = 1
+ print >> sys.stderr, 'disconnected from the meta-server'
+
+ def send_traceback(self):
+ if not self.closed:
+ import traceback, cStringIO, sys
+ f = cStringIO.StringIO()
+ print >> f, sys.version
+ print >> f, 'platform: ', sys.platform
+ print >> f, 'executable: ', sys.executable
+ print >> f, 'argv: ', sys.argv
+ print >> f, 'cwd: ', os.getcwd()
+ print >> f, 'version tag:', VERSION_TAG
+ print >> f
+ traceback.print_exc(file = f)
+ self.s.sendall(message(MMSG_TRACEBACK, f.getvalue()))
+
+ def msg_wakeup(self, origin, *rest):
+ if self.lastwakeup is None or time.time()-self.lastwakeup > 4.0:
+ def fastresponses(wakeup):
+ sys.setcheckinterval(64)
+ time.sleep(12.01)
+ if self.lastwakeup == wakeup:
+ sys.setcheckinterval(4096)
+ self.synsockets.clear()
+ import thread
+ self.lastwakeup = time.time()
+ thread.start_new_thread(fastresponses, (self.lastwakeup,))
+
+ def msg_connect(self, origin, port, *rest):
+ def connect(origin, port):
+ host, _ = origin.split(':')
+ addr = host, port
+ s = socket(AF_INET, SOCK_STREAM)
+ print >> sys.stderr, 'backconnecting to', addr
+ try:
+ s.connect(addr)
+ except error, e:
+ print >> sys.stderr, 'backconnecting:', str(e)
+ else:
+ self.game.newclient_threadsafe(s, addr)
+ import thread
+ thread.start_new_thread(connect, (origin, port))
+
+ def msg_udp_conn(self, origin, secret, port, *rest):
+ def connect(origin, secret, port):
+ host, _ = origin.split(':')
+ addr = host, port
+ s = socket(AF_INET, SOCK_DGRAM)
+ print >> sys.stderr, 'udp connecting to', addr
+ s.connect(addr)
+ mysecret = random.randrange(0, 65536)
+ packet = ('B' + chr( secret & 0xFF) + chr( secret >> 8)
+ + chr(mysecret & 0xFF) + chr(mysecret >> 8))
+ from socketoverudp import SocketOverUdp
+ from socketoverudp import SOU_RANGE_START, SOU_RANGE_STOP
+ for i in range(5):
+ #print 'sending', repr(packet)
+ s.send(packet)
+ iwtd, owtd, ewtd = select([s], [], [], 0.25)
+ if s in iwtd:
+ #print 'reading'
+ try:
+ inbuf = s.recv(SocketOverUdp.PACKETSIZE)
+ except error:
+ inbuf = ''
+ # try again?
+ iwtd, owtd, ewtd = select([s], [], [], 0.35)
+ if s in iwtd:
+ try:
+ inbuf = s.recv(SocketOverUdp.PACKETSIZE)
+ except error:
+ pass
+ #print 'got', repr(inbuf)
+ if (inbuf and
+ SOU_RANGE_START <= ord(inbuf[0]) < SOU_RANGE_STOP):
+ break
+ else:
+ print >> sys.stderr, 'udp connecting: no answer, giving up'
+ return
+ sock = SocketOverUdp(s, (mysecret, secret))
+ data = sock._decode(inbuf)
+ #print 'decoded as', repr(data)
+ expected = '[bnb c->s]' + packet[3:5]
+ while len(data) < len(expected) + 2:
+ #print 'waiting for more'
+ iwtd, owtd, ewtd = select([sock], [], [], 5.0)
+ if sock not in iwtd:
+ print >> sys.stderr, 'udp connecting: timed out'
+ return
+ #print 'decoding more'
+ data += sock.recv()
+ #print 'now data is', repr(data)
+ if data[:-2] != expected:
+ print >> sys.stderr, 'udp connecting: bad data'
+ return
+ sock.sendall('[bnb s->c]' + data[-2:])
+ sock.flush()
+ #print 'waiting for the last dot...'
+ while 1:
+ iwtd, owtd, ewtd = select([sock], [], [], 5.0)
+ if sock not in iwtd:
+ print >> sys.stderr, 'udp connecting: timed out'
+ return
+ data = sock.recv(200)
+ if data:
+ break
+ if data != '^':
+ print >> sys.stderr, 'udp connecting: bad data'
+ return
+ #print 'done!'
+ self.game.newclient_threadsafe(sock, addr)
+
+ import thread
+ thread.start_new_thread(connect, (origin, secret, port))
+
+ def msg_ping(self, origin, *rest):
+ # ping time1 --> pong time2 time1
+ self.s.sendall(message(MMSG_ROUTE, origin,
+ RMSG_PONG, float2str(time.time()), *rest))
+
+ def msg_sync(self, origin, clientport, time3, time2, time1, *rest):
+ time4 = time.time()
+ s = socket(AF_INET, SOCK_STREAM)
+ s.bind(('', INADDR_ANY))
+ _, serverport = s.getsockname()
+ self.s.sendall(message(MMSG_ROUTE, origin,
+ RMSG_CONNECT, serverport, clientport))
+ #print 'times:', time1, time2, time3, time4
+ doubleping = (str2float(time3)-str2float(time1)) + (time4-str2float(time2))
+ connecttime = time4 + doubleping / 4.0
+ def connect(origin, port, connecttime, s):
+ host, _ = origin.split(':')
+ addr = host, port
+ delay = connecttime - time.time()
+ #print 'sleep(%r)' % delay
+ if 0.0 <= delay <= 10.0:
+ time.sleep(delay)
+ print >> sys.stderr, 'synconnecting to', addr
+ try:
+ s.connect(addr)
+ except error, e:
+ print >> sys.stderr, 'synconnecting:', str(e)
+ else:
+ self.game.newclient_threadsafe(s, addr)
+ import thread
+ thread.start_new_thread(connect, (origin, clientport, connecttime, s))
+
+ MESSAGES = {
+ RMSG_CONNECT: msg_connect,
+ RMSG_WAKEUP: msg_wakeup,
+ RMSG_PING: msg_ping,
+ RMSG_SYNC: msg_sync,
+ RMSG_UDP_CONN:msg_udp_conn,
+ }
+
+metaclisrv = None
+
+def meta_register(game):
+ global metaclisrv
+ import gamesrv
+ info = {}
+ if game.FnDesc:
+ info['desc'] = game.FnDesc or ''
+ info['extradesc'] = game.FnExtraDesc() or ''
+
+ s = gamesrv.opentcpsocket()
+ hs = gamesrv.openhttpsocket()
+ port = int(gamesrv.displaysockport(s))
+ info['httpport'] = gamesrv.displaysockport(hs)
+
+ if not metaclisrv or metaclisrv.closed:
+ s = connect()
+ if not s:
+ return
+ metaclisrv = MetaClientSrv(s, game)
+ metaclisrv.s.sendall(message(MMSG_INFO, encodedict(info)) +
+ message(MMSG_START, port))
+
+def meta_unregister(game):
+ global metaclisrv
+ if metaclisrv:
+ metaclisrv.close()
+ metaclisrv = None
+
+
+# ____________________________________________________________
+# Game Clients
+
+class Event:
+ def __init__(self):
+ import thread
+ self.lock = thread.allocate_lock()
+ self.lock.acquire()
+ def signal(self):
+ try:
+ self.lock.release()
+ except:
+ pass
+ def wait1(self):
+ self.lock.acquire()
+
+
+class MetaClientCli:
+ fatalerror = False
+
+ def __init__(self, serverkey, backconnectport):
+ self.resultsocket = None
+ self.serverkey = serverkey
+ self.backconnectport = backconnectport
+ self.threads = {}
+
+ def run(self):
+ import thread
+ print >> sys.stderr, 'Trying to connect to', self.serverkey
+ self.ev = Event()
+ self.ev2 = Event()
+ self.buffer = ""
+ self.sendlock = thread.allocate_lock()
+ self.recvlock = thread.allocate_lock()
+ self.inputmsgqueue = []
+ self.gotudpport = None
+ if not (PORTS.get('CLIENT') or PORTS.get('sendudpto')):
+ self.s = connect()
+ thread.start_new_thread(self.acquire_udp_port, ())
+ else:
+ self.s = None
+ self.ev2.signal()
+ self.startthread(self.try_udp_connect)
+
+ thread.start_new_thread(self.bipbip, ())
+ self.startthread(self.try_direct_connect, 0.75)
+ self.startthread(self.try_indirect_connect, 1.50)
+ while self.resultsocket is None:
+ self.threadsleft()
+ self.ev.wait1()
+ self.ev2.wait1()
+ return self.resultsocket
+
+ def done(self):
+ sys.setcheckinterval(4096)
+
+ def bipbip(self):
+ while self.resultsocket is None:
+ time.sleep(0.31416)
+ self.ev.signal()
+
+ def startthread(self, fn, sleep=0.0, args=()):
+ import thread
+ def bootstrap(fn, atom, sleep, args):
+ try:
+ time.sleep(sleep)
+ if self.resultsocket is None:
+ fn(*args)
+ finally:
+ del self.threads[atom]
+ self.ev.signal()
+ atom = object()
+ self.threads[atom] = time.time()
+ thread.start_new_thread(bootstrap, (fn, atom, sleep, args))
+
+ def threadsleft(self):
+ if self.fatalerror:
+ sys.exit(1)
+ now = time.time()
+ TIMEOUT = 11
+ for starttime in self.threads.values():
+ if now < starttime + TIMEOUT:
+ break
+ else:
+ if self.threads:
+ print >> sys.stderr, '*** time out, giving up.'
+ else:
+ print >> sys.stderr, '*** failed to connect.'
+ sys.exit(1)
+
+ def try_direct_connect(self):
+ host, port = self.serverkey.split(':')
+ port = int(port)
+ s = socket(AF_INET, SOCK_STREAM)
+ try:
+ s.connect((host, port))
+ except error, e:
+ print >> sys.stderr, 'direct connexion failed:', str(e)
+ else:
+ print >> sys.stderr, 'direct connexion accepted.'
+ self.resultsocket = s
+
+ def try_indirect_connect(self):
+ import thread, time
+ if not self.s:
+ self.s = connect()
+ if not self.s:
+ return
+ self.routemsg(RMSG_WAKEUP)
+ self.startthread(self.try_backconnect)
+ self.socketcache = {}
+ tries = [0.6, 0.81, 1.2, 1.69, 2.6, 3.6, 4.9, 6.23]
+ for delay in tries:
+ self.startthread(self.send_ping, delay)
+ while self.resultsocket is None:
+ msg = self.inputmsg()
+ now = time.time()
+ if self.resultsocket is not None:
+ break
+ if msg[0] == RMSG_CONNECT:
+ # connect serverport clientport
+ self.startthread(self.try_synconnect, args=msg[1:])
+ if msg[0] == RMSG_PONG:
+ # pong time2 time1 --> sync port time3 time2 time1
+ if len(self.socketcache) < len(tries):
+ s = socket(AF_INET, SOCK_STREAM)
+ s.bind(('', INADDR_ANY))
+ _, port = s.getsockname()
+ self.socketcache[port] = s
+ self.routemsg(RMSG_SYNC, port, float2str(now), *msg[2:])
+
+ def sendmsg(self, data):
+ self.sendlock.acquire()
+ try:
+ self.s.sendall(data)
+ finally:
+ self.sendlock.release()
+
+ def routemsg(self, *rest):
+ self.sendmsg(message(MMSG_ROUTE, self.serverkey, *rest))
+
+ def _readnextmsg(self, blocking):
+ self.recvlock.acquire()
+ try:
+ while 1:
+ msg, self.buffer = decodemessage(self.buffer)
+ if msg is not None:
+ if msg[0] == RMSG_UDP_ADDR:
+ if len(msg) > 2:
+ self.gotudpport = msg[1], int(msg[2])
+ continue
+ if msg[0] == RMSG_NO_HOST and msg[1] == self.serverkey:
+ print >> sys.stderr, ('*** server %r is not registered'
+ ' on the meta-server' % (msg[1],))
+ self.fatalerror = True
+ sys.exit()
+ self.inputmsgqueue.append(msg)
+ return
+ iwtd, owtd, ewtd = select([self.s], [], [], 0)
+ if not iwtd:
+ if self.inputmsgqueue or not blocking:
+ return
+ data = self.s.recv(2048)
+ if not data:
+ print >> sys.stderr, 'disconnected from the meta-server'
+ sys.exit()
+ self.buffer += data
+ finally:
+ self.recvlock.release()
+
+ def inputmsg(self):
+ self._readnextmsg(blocking=True)
+ return self.inputmsgqueue.pop(0)
+
+ def try_backconnect(self):
+ s1 = socket(AF_INET, SOCK_STREAM)
+ s1.bind(('', self.backconnectport or INADDR_ANY))
+ s1.listen(1)
+ _, port = s1.getsockname()
+ self.routemsg(RMSG_CONNECT, port)
+ print >> sys.stderr, 'listening for backward connection'
+ iwtd, owtd, ewtd = select([s1], [], [], 7.5)
+ if s1 in iwtd:
+ s, addr = s1.accept()
+ print >> sys.stderr, 'accepted backward connection from', addr
+ self.resultsocket = s
+
+ def send_ping(self):
+ sys.stderr.write('. ')
+ self.routemsg(RMSG_PING, float2str(time.time()))
+
+ def try_synconnect(self, origin, remoteport, localport, *rest):
+ sys.stderr.write('+ ')
+ s = self.socketcache[localport]
+ remotehost, _ = origin.split(':')
+ remoteaddr = remotehost, remoteport
+ try:
+ s.connect(remoteaddr)
+ except error, e:
+ print >> sys.stderr, 'SYN connect failed:', str(e)
+ return
+ print >> sys.stderr, ('simultaneous SYN connect succeeded with %s:%d' %
+ remoteaddr)
+ self.resultsocket = s
+
+ def try_udp_connect(self):
+ for i in range(3): # three attempts
+ self.attempt_udp_connect()
+ if self.resultsocket is not None:
+ break
+
+ def attempt_udp_connect(self):
+ if '*udpsock*' in PORTS:
+ s, (host, port) = PORTS['*udpsock*']
+ else:
+ s = socket(AF_INET, SOCK_DGRAM)
+ s.bind(('', PORTS.get('CLIENT', INADDR_ANY)))
+ host, port = s.getsockname()
+ if 'sendudpto' in PORTS:
+ host = PORTS['sendudpto']
+ secret = originalsecret = random.randrange(0, 65536)
+ self.routemsg(RMSG_UDP_CONN, secret, port)
+ secret = 'B' + chr(secret & 0xFF) + chr(secret >> 8)
+ while True:
+ iwtd, owtd, ewtd = select([s], [], [], 2.94)
+ if s not in iwtd:
+ return
+ packet, addr = s.recvfrom(200)
+ if packet.startswith(secret) and len(packet) == 5:
+ break
+ s.connect(addr)
+ #print 'got', repr(packet)
+ remotesecret = ord(packet[3]) | (ord(packet[4]) << 8)
+ secret = random.randrange(0, 65536)
+ secret = chr(secret & 0xFF) + chr(secret >> 8)
+ packet = '[bnb c->s]' + packet[3:5] + secret
+ for name in ('*udpsock*', 'CLIENT'):
+ if name in PORTS:
+ del PORTS[name]
+ from socketoverudp import SocketOverUdp
+ sock = SocketOverUdp(s, (originalsecret, remotesecret))
+ #print 'sending', repr(packet)
+ sock.sendall(packet)
+ sock.flush()
+ data = ''
+ expected = '[bnb s->c]' + secret
+ while len(data) < len(expected):
+ #print 'waiting'
+ iwtd, owtd, ewtd = select([sock], [], [], 2.5)
+ if sock not in iwtd:
+ print >> sys.stderr, 'socket-over-udp timed out'
+ return
+ #print 'we get:'
+ data += sock.recv()
+ #print repr(data)
+ if data != expected:
+ print >> sys.stderr, 'bad udp data from', addr
+ else:
+ sock.sendall('^')
+ sock.flush()
+ print 'udp connexion handshake succeeded'
+ self.resultsocket = sock
+
+ def acquire_udp_port(self):
+ try:
+ s = socket(AF_INET, SOCK_DGRAM)
+ s.bind(('', INADDR_ANY))
+ randomdata = hex(random.randrange(0, sys.maxint))
+ for i in range(5):
+ s.sendto(randomdata, METASERVER_UDP)
+ time.sleep(0.05)
+ self.sendmsg(message(MMSG_UDP_ADDR, randomdata))
+ time.sleep(0.05)
+ self._readnextmsg(blocking=False)
+ if self.gotudpport:
+ PORTS['*udpsock*'] = s, self.gotudpport
+ udphost, udpport = self.gotudpport
+ print >> sys.stderr, ('udp port %d is visible from '
+ 'outside on %s:%d' % (
+ s.getsockname()[1], udphost, udpport))
+ self.startthread(self.try_udp_connect)
+ break
+ finally:
+ self.ev2.signal()
+
+
+def meta_connect(serverkey, backconnectport=None):
+ global METASERVER
+ if PORTS.get('SSH_RELAY'):
+ METASERVER = PORTS['SSH_RELAY']
+ c = MetaClientCli(serverkey, backconnectport)
+ s = c.run()
+ c.done()
+ return s
+
+def print_server_list():
+ s = connect()
+ if s is not None:
+ s.sendall(message(MMSG_LIST))
+ buffer = ""
+ while decodemessage(buffer)[0] is None:
+ buffer += s.recv(8192)
+ s.close()
+ msg = decodemessage(buffer)[0]
+ assert msg[0] == RMSG_LIST
+ entries = decodedict(msg[1])
+ if not entries:
+ print >> sys.stderr, 'No registered server.'
+ else:
+ print
+ print ' %-25s | %-30s | %s' % (
+ 'server', 'game', 'players')
+ print '-'*27+'+'+'-'*32+'+'+'-'*11
+ for key, value in entries.items():
+ if ':' in key:
+ try:
+ addr, _, _ = gethostbyaddr(key[:key.index(':')])
+ except:
+ pass
+ else:
+ addr = '%-27s' % (addr,)
+ if len(addr) < 28: addr += '|'
+ addr = '%-60s' % (addr,)
+ if len(addr) < 61: addr += '|'
+ print addr
+ value = decodedict(value)
+ print ' %-25s | %-30s | %s' % (
+ key, value.get('desc', '<no description>'),
+ value.get('extradesc', ''))
+ print
+
+if __name__ == '__main__':
+ print_server_list()
diff --git a/metaserver/metaserver.py b/metaserver/metaserver.py
new file mode 100644
index 0000000..cb4ea19
--- /dev/null
+++ b/metaserver/metaserver.py
@@ -0,0 +1,365 @@
+from socket import *
+import os, sys, time, random
+from select import select
+from cStringIO import StringIO
+from weakref import WeakValueDictionary
+from metastruct import *
+from common import httpserver, stdlog
+
+if __name__ == '__main__':
+ os.chdir(os.path.dirname(sys.argv[0]) or os.curdir)
+
+META_SERVER_HTTP_PORT = 8050
+META_SERVER_PORT = 8055
+META_SERVER_UDP_PORT = 8055
+IMAGE_DIR = "../bubbob/doc/images"
+ICONS = [open(os.path.join(IMAGE_DIR, s), 'rb').read()
+ for s in os.listdir(IMAGE_DIR) if s.endswith('.png')]
+assert ICONS, "you need to run ../bubbob/doc/bonus-doc.py"
+MAX_SERVERS = 50
+MAX_CONNEXIONS = 60
+
+
+serversockets = {}
+
+class MetaServer:
+
+ def __init__(self, port=META_SERVER_PORT, udpport=META_SERVER_UDP_PORT):
+ s = socket(AF_INET, SOCK_STREAM)
+ s.bind(('', port))
+ s.listen(5)
+ self.parentsock = s
+ self.ServersDict = {}
+ self.ServersList = []
+ serversockets[s] = self.clientconnect, sys.exit
+ self.udpsock = socket(AF_INET, SOCK_DGRAM)
+ self.udpsock.bind(('', udpport))
+ serversockets[self.udpsock] = self.udp_message, None
+ self.udpdata = []
+
+ def detach(self):
+ pid = os.fork()
+ if pid:
+ print pid
+ os._exit(0)
+ # in the child process
+ os.setsid()
+ logfile = stdlog.LogFile(limitsize=131072)
+ if logfile:
+ print >> logfile
+ print "Logging to", logfile.filename
+ fd = logfile.f.fileno()
+ try:
+ # detach from parent
+ os.dup2(fd, 1)
+ os.dup2(fd, 2)
+ os.dup2(fd, 0)
+ except OSError:
+ pass
+ logfile.close()
+ # record pid
+ f = open('pid', 'w')
+ print >> f, os.getpid()
+ f.close()
+
+ def clientconnect(self):
+ s, addr = self.parentsock.accept()
+ Connexion(s, addr)
+
+ def publish(self, server):
+ key = server.serverkey
+ if key in self.ServersDict:
+ current = self.ServersDict[key]
+ if current is server:
+ return
+ self.ServersList.remove(current)
+ elif len(self.ServersDict) >= MAX_SERVERS:
+ raise OverflowError
+ self.ServersList.append(server)
+ self.ServersDict[key] = server
+ print '+', key
+
+ def unpublish(self, server):
+ key = server.serverkey
+ if key in self.ServersDict:
+ current = self.ServersDict[key]
+ if current is server:
+ del self.ServersDict[key]
+ self.ServersList.remove(server)
+ print '-', key
+
+ def makelist(self):
+ items = {}
+ for srv in self.ServersList:
+ items[srv.serverkey] = encodedict(srv.serverinfo)
+ return encodedict(items)
+
+ def getserver(self, key):
+ return self.ServersDict[key]
+
+ def udp_message(self):
+ data, addr = self.udpsock.recvfrom(32)
+ self.udpdata.append((data, addr))
+ if len(self.udpdata) > 50:
+ del self.udpdata[0]
+
+
+class Connexion(MessageSocket):
+
+ def __init__(self, s, addr):
+ MessageSocket.__init__(self, s)
+ self.serverinfo = {
+ 'time': int(time.time()),
+ 'icon': random.choice(ICONS),
+ 'iconformat': 'png',
+ }
+ self.addr = addr
+ self.key = '%s:%d' % addr
+ self.serverkey = None
+ print '[', self.key
+ self.backlinks = WeakValueDictionary()
+ if len(serversockets) >= MAX_CONNEXIONS:
+ self.disconnect()
+ raise OverflowError
+ serversockets[s] = self.receive, self.disconnect
+
+ def disconnect(self):
+ metaserver.unpublish(self)
+ try:
+ del serversockets[self.s]
+ except KeyError:
+ pass
+ print ']', self.key
+
+ def msg_serverinfo(self, info, *rest):
+ print '|', self.key
+ if len(info) > 15000:
+ raise OverflowError
+ info = decodedict(info)
+ self.serverinfo.update(info)
+
+ def msg_startserver(self, port, *rest):
+ serverkey = '%s:%d' % (self.addr[0], port)
+ if self.serverkey and self.serverkey != serverkey:
+ metaserver.unpublish(self)
+ self.serverkey = serverkey
+ metaserver.publish(self)
+
+ def msg_stopserver(self, *rest):
+ metaserver.unpublish(self)
+
+ def msg_list(self, *rest):
+ self.s.sendall(message(RMSG_LIST, metaserver.makelist()))
+
+ def msg_route(self, targetkey, *rest):
+ try:
+ target = metaserver.getserver(targetkey)
+ except KeyError:
+ try:
+ target = self.backlinks[targetkey]
+ except KeyError:
+ self.s.sendall(message(RMSG_NO_HOST, targetkey))
+ return
+ target.route(self, *rest)
+
+ def route(self, origin, msgcode, *rest):
+ self.backlinks[origin.key] = origin
+ self.s.sendall(message(msgcode, origin.key, *rest))
+
+ def msg_traceback(self, tb, *rest):
+ f = stdlog.LogFile('tb-%s.log' % (self.addr[0],))
+ if f:
+ print >> f, tb
+ f.close()
+
+ def msg_udp_addr(self, pattern, *rest):
+ for data, addr in metaserver.udpdata:
+ if data == pattern:
+ try:
+ host, port = addr
+ port = int(port)
+ except ValueError:
+ continue
+ self.s.sendall(message(RMSG_UDP_ADDR, host, port))
+ return
+ else:
+ self.s.sendall(message(RMSG_UDP_ADDR))
+
+ MESSAGES = {
+ MMSG_INFO: msg_serverinfo,
+ MMSG_START: msg_startserver,
+ MMSG_STOP: msg_stopserver,
+ MMSG_LIST: msg_list,
+ MMSG_ROUTE: msg_route,
+ MMSG_TRACEBACK: msg_traceback,
+ MMSG_UDP_ADDR: msg_udp_addr,
+ }
+
+
+# ____________________________________________________________
+
+import htmlentitydefs
+text_to_html = {}
+for key, value in htmlentitydefs.entitydefs.items():
+ text_to_html[value] = '&' + key + ';'
+for i in range(32):
+ text_to_html[chr(i)] = '?'
+def htmlquote(s, getter=text_to_html.get):
+ lst = [getter(c, c) for c in s if ' ' <= c < '\x7F']
+ return ''.join(lst)
+def txtfilter(s, maxlen=200):
+ s = str(s)[:maxlen]
+ l = [c for c in s if c in "!$*,-.0123456789:@ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "^_`abcdefghijklmnopqrstuvwxyz{|}"]
+ return ''.join(l)
+
+f = open('index.html', 'r')
+HEADER, ROW, FOOTER = f.read().split('\\')
+f.close()
+
+def makehosthtml(srv, bottommsg, join):
+ info = srv.serverinfo
+ hostname, port = srv.serverkey.split(':')
+ try:
+ fullhostname = gethostbyaddr(hostname)[0]
+ except:
+ fullhostname = hostname
+ url = None
+ if join:
+ url = "http://%s/join.html?host=%s&port=%s&httpport=%s&m=%s" % (
+ join, hostname, port, info.get('httpport') or 'off', time.time())
+ else:
+ try:
+ httpport = int(info.get('httpport'))
+ except:
+ pass
+ else:
+ url = "http://%s:%s/" % (hostname, httpport)
+ javamsg = """
+<p>Click on a server above to join the game. This only works if:
+<ul><li>your browser understands Java;
+ <li>the server is not behind a firewall;
+ <li>you don't mind not hearing the nice background music.
+</ul></p>
+<p>Alternatively, install the
+<a href="http://bub-n-bros.sourceforge.net/download.html">Python version</a>
+of the client, which can cope with all of the above problems.</p>
+<br>"""
+ if javamsg not in bottommsg:
+ bottommsg.append(javamsg)
+ result = '<strong>%s</strong>:%s' % (fullhostname, port)
+ if url:
+ result = '<a href="%s">%s</a>' % (url, result)
+ return result
+
+def indexloader(headers, join=[], head=[], **options):
+ if join:
+ join = join[0]
+ data = [HEADER % (head and head[0] or '')]
+ bottommsg = []
+ if metaserver.ServersList:
+ counter = 0
+ for srv in metaserver.ServersList:
+ info = srv.serverinfo
+ icon = '<img src="ico?key=%s">' % srv.serverkey
+ bgcolor = ('#C0D0D0', '#E0D0A8')[counter&1]
+ hosthtml = makehosthtml(srv, bottommsg, join)
+ desc = htmlquote(info.get('desc')) or ''
+ extradesc = htmlquote(info.get('extradesc')) or ''
+ if isinstance(info.get('time'), int):
+ stime = time.strftime('%a %b %d<br>%H:%M GMT',
+ time.gmtime(info['time']))
+ else:
+ stime = ''
+ data.append(ROW % locals())
+ counter += 1
+ else:
+ data.append('''<tr><td bgcolor="#FFFFFF">
+ Sorry, there is no registered server at the moment.
+ </td></tr>''')
+ if join:
+ extrafooter = '''<p><img src="home.png">
+ <a href="http://%s/?time=%s">Back to local games</a></p>''' % (
+ join, time.time())
+ else:
+ extrafooter = ''
+ bottommsg = '\n'.join(bottommsg)
+ tbfiles = [s for s in os.listdir('.') if s.startswith('tb-')]
+ if tbfiles:
+ tbfiles = len(tbfiles)
+ else:
+ tbfiles = ''
+ data.append(FOOTER % locals())
+ return StringIO(''.join(data)), 'text/html'
+
+def icoloader(key, **options):
+ srv = metaserver.getserver(key[0])
+ iconformat = txtfilter(srv.serverinfo['iconformat'], 32)
+ return StringIO(srv.serverinfo['icon']), 'image/' + iconformat
+
+httpserver.register('', indexloader)
+httpserver.register('index.html', indexloader)
+httpserver.register('bub-n-bros.html', indexloader)
+httpserver.register('ico', icoloader)
+httpserver.register('mbub.png', httpserver.fileloader('mbub.png', 'image/png'))
+httpserver.register('home.png', httpserver.fileloader('home.png', 'image/png'))
+
+def openhttpsocket(port = META_SERVER_HTTP_PORT):
+ from BaseHTTPServer import HTTPServer
+ class ServerClass(HTTPServer):
+ def get_request(self):
+ sock, addr = self.socket.accept()
+ sock.settimeout(5.0)
+ return sock, addr
+ HandlerClass = httpserver.MiniHandler
+ server_address = ('', port)
+ httpd = ServerClass(server_address, HandlerClass)
+ s = httpd.socket
+ serversockets[s] = httpd.handle_request, None
+
+
+# ____________________________________________________________
+
+def mainloop():
+ while 1:
+ iwtd = serversockets.keys()
+ iwtd, owtd, ewtd = select(iwtd, [], iwtd)
+ #print iwtd, owtd, ewtd, serversockets
+ for s in iwtd:
+ if s in serversockets:
+ input, close = serversockets[s]
+ try:
+ input()
+ except:
+ import traceback
+ print "-"*60
+ traceback.print_exc()
+ print "-"*60
+ for s in ewtd:
+ if s in serversockets:
+ input, close = serversockets[s]
+ try:
+ close()
+ except:
+ import traceback
+ print "-"*60
+ traceback.print_exc()
+ print "-"*60
+
+
+if __name__ == '__main__':
+ metaserver = MetaServer()
+ if sys.argv[1:2] == ['-f']:
+ metaserver.detach()
+ try:
+ openhttpsocket()
+ print 'listening to client port tcp %d / http %d / udp %d.' % (
+ META_SERVER_PORT,
+ META_SERVER_HTTP_PORT,
+ META_SERVER_UDP_PORT)
+ mainloop()
+ finally:
+ if metaserver.ServersList:
+ print '*** servers still connected, waiting 5 seconds'
+ time.sleep(5)
+ print '*** leaving at', time.ctime()
diff --git a/metaserver/metastruct.py b/metaserver/metastruct.py
new file mode 100644
index 0000000..13240cc
--- /dev/null
+++ b/metaserver/metastruct.py
@@ -0,0 +1,75 @@
+import sys, os
+LOCALDIR = __file__
+LOCALDIR = os.path.abspath(os.path.dirname(LOCALDIR))
+sys.path.insert(0, os.path.dirname(LOCALDIR))
+
+from common.msgstruct import *
+from socket import error
+
+MMSG_INFO = 'I'
+MMSG_START = '+'
+MMSG_STOP = '-'
+MMSG_LIST = 'L'
+MMSG_ROUTE = 'R'
+MMSG_TRACEBACK= 'T'
+MMSG_UDP_ADDR = 'U'
+
+RMSG_WAKEUP = 'w'
+RMSG_PING = 'p'
+RMSG_PONG = 'o'
+RMSG_SYNC = 'y'
+RMSG_CONNECT = 'c'
+RMSG_LIST = 'l'
+RMSG_UDP_ADDR = 'u'
+RMSG_UDP_CONN = 'd'
+RMSG_NO_HOST = '?'
+
+
+def encodedict(dict):
+ data = []
+ for key, value in dict.items():
+ data.append(message('#', key, value))
+ return ''.join(data)
+
+def encodelist(list):
+ return message('[', *list)
+
+def decodedict(buffer):
+ result = {}
+ while 1:
+ msg, buffer = decodemessage(buffer)
+ if msg is None or len(msg) < 3 or msg[0] != '#':
+ break
+ result[msg[1]] = msg[2]
+ return result
+
+def decodelist(buffer):
+ msg, buffer = decodemessage(buffer)
+ assert msg[0] == '['
+ return list(msg[1:])
+
+
+class MessageSocket:
+
+ def __init__(self, s):
+ self.s = s
+ self.buffer = ""
+
+ def receive(self):
+ try:
+ data = self.s.recv(2048)
+ except error:
+ data = ''
+ if not data:
+ self.disconnect()
+ return
+ self.buffer += data
+ while 1:
+ msg, self.buffer = decodemessage(self.buffer)
+ if msg is None:
+ break
+ if msg[0] not in self.MESSAGES:
+ print >> sys.stderr, 'unknown message %r' % (msg[0],)
+ else:
+ fn = self.MESSAGES[msg[0]]
+ fn(self, *msg[1:])
diff --git a/metaserver/pipelayer.py b/metaserver/pipelayer.py
new file mode 100644
index 0000000..9baf78a
--- /dev/null
+++ b/metaserver/pipelayer.py
@@ -0,0 +1,337 @@
+#import os
+import struct
+from collections import deque
+from zlib import crc32
+
+
+class InvalidPacket(Exception):
+ pass
+
+
+FLAG_NAK1 = 0xE0
+FLAG_NAK = 0xE1
+FLAG_REG = 0xE2
+FLAG_CFRM = 0xE3
+
+FLAG_RANGE_START = 0xE0
+FLAG_RANGE_STOP = 0xE4
+
+max_old_packets = 200 # must be <= 256
+
+
+class PipeLayer(object):
+ timeout = 1
+ headersize = 4
+
+ def __init__(self, initialcrcs=(0, 0)):
+ #self.localid = os.urandom(4)
+ #self.remoteid = None
+ self.cur_time = 0
+ self.out_queue = deque()
+ self.out_nextseqid = 0
+ self.out_nextrepeattime = None
+ self.in_nextseqid = 0
+ self.in_outoforder = {}
+ self.out_oldpackets = deque()
+ self.out_flags = FLAG_REG
+ self.out_resend = 0
+ self.out_resend_skip = False
+ self.in_crc, self.out_crc = initialcrcs
+
+ def queue(self, data):
+ if data:
+ self.out_queue.appendleft(data)
+
+ def queue_size(self):
+ total = 0
+ for data in self.out_queue:
+ total += len(data)
+ return total
+
+ def in_sync(self):
+ return not self.out_queue and self.out_nextrepeattime is None
+
+ def settime(self, curtime):
+ self.cur_time = curtime
+ if self.out_queue:
+ if len(self.out_oldpackets) < max_old_packets:
+ return 0 # more data to send now
+ if self.out_nextrepeattime is not None:
+ return max(0, self.out_nextrepeattime - curtime)
+ else:
+ return None
+
+ def is_congested(self):
+ return len(self.out_oldpackets) >= max_old_packets
+
+ def encode(self, maxlength):
+ #print ' '*self._dump_indent, '--- OUTQ', self.out_resend, self.out_queue
+ if len(self.out_oldpackets) >= max_old_packets:
+ # congestion, stalling
+ payload = 0
+ else:
+ payload = maxlength - 8
+ if payload <= 0:
+ raise ValueError("encode(): buffer too small")
+ if (self.out_nextrepeattime is not None and
+ self.out_nextrepeattime <= self.cur_time):
+ # no ACK received so far, send a packet (possibly empty)
+ if not self.out_queue:
+ payload = 0
+ else:
+ if not self.out_queue: # no more data to send
+ return None
+ if payload == 0: # congestion
+ return None
+ # prepare a packet
+ seqid = self.out_nextseqid
+ flags = self.out_flags
+ self.out_flags = FLAG_REG # clear out the flags for the next time
+ #if flags in (FLAG_NAK, FLAG_NAK1):
+ # print 'out_flags NAK', hex(flags)
+ if payload > 0:
+ self.out_nextseqid = (seqid + 1) & 0xFFFF
+ data = self.out_queue.pop()
+ packetlength = len(data)
+ if self.out_resend > 0:
+ if packetlength > payload + 4:
+ raise ValueError("XXX need constant buffer size for now")
+ self.out_resend -= 1
+ if self.out_resend_skip:
+ if self.out_resend > 0:
+ self.out_queue.pop()
+ self.out_resend -= 1
+ self.out_nextseqid = (seqid + 2) & 0xFFFF
+ self.out_resend_skip = False
+ packetpayload = data
+ else:
+ packet = []
+ while packetlength <= payload:
+ packet.append(data)
+ if not self.out_queue:
+ break
+ data = self.out_queue.pop()
+ packetlength += len(data)
+ else:
+ rest = len(data) + payload - packetlength
+ packet.append(data[:rest])
+ self.out_queue.append(data[rest:])
+ packetpayload = ''.join(packet)
+ self.out_crc = crc32(packetpayload, self.out_crc)
+ packetpayload += struct.pack("!I", self.out_crc & 0xffffffff)
+ self.out_oldpackets.appendleft(packetpayload)
+ #print ' '*self._dump_indent, '--- OLDPK', self.out_oldpackets
+ else:
+ # a pure ACK packet, no payload
+ if self.out_oldpackets and flags == FLAG_REG:
+ flags = FLAG_CFRM
+ packetpayload = ''
+ packet = struct.pack("!BBH", flags,
+ self.in_nextseqid & 0xFF,
+ seqid) + packetpayload
+ if self.out_oldpackets:
+ self.out_nextrepeattime = self.cur_time + self.timeout
+ else:
+ self.out_nextrepeattime = None
+ #self.dump('OUT', packet)
+ return packet
+
+ def decode(self, rawdata):
+ if len(rawdata) < 4:
+ raise InvalidPacket
+ #print ' '*self._dump_indent, '------ out %d (+%d) in %d' % (self.out_nextseqid, self.out_resend, self.in_nextseqid)
+ #self.dump('IN ', rawdata)
+ in_flags, ack_seqid, in_seqid = struct.unpack("!BBH", rawdata[:4])
+ if not (FLAG_RANGE_START <= in_flags < FLAG_RANGE_STOP):
+ raise InvalidPacket
+ in_diff = (in_seqid - self.in_nextseqid ) & 0xFFFF
+ ack_diff = (self.out_nextseqid + self.out_resend - ack_seqid) & 0xFF
+ if in_diff >= max_old_packets:
+ return '' # invalid, but can occur as a late repetition
+ if ack_diff != len(self.out_oldpackets):
+ # forget all acknowledged packets
+ if ack_diff > len(self.out_oldpackets):
+ return '' # invalid, but can occur with packet reordering
+ while len(self.out_oldpackets) > ack_diff:
+ #print ' '*self._dump_indent, '--- POP', repr(self.out_oldpackets[-1])
+ self.out_oldpackets.pop()
+ if self.out_oldpackets:
+ self.out_nextrepeattime = self.cur_time + self.timeout
+ else:
+ self.out_nextrepeattime = None # all packets ACKed
+ if in_flags == FLAG_NAK or in_flags == FLAG_NAK1:
+ #print 'recv NAK', hex(in_flags)
+ # this is a NAK: resend the old packets as far as they've not
+ # also been ACK'ed in the meantime (can occur with reordering)
+ while self.out_resend < len(self.out_oldpackets):
+ self.out_queue.append(self.out_oldpackets[self.out_resend])
+ self.out_resend += 1
+ self.out_nextseqid = (self.out_nextseqid - 1) & 0xFFFF
+ #print ' '*self._dump_indent, '--- REP', self.out_nextseqid, repr(self.out_queue[-1])
+ self.out_resend_skip = in_flags == FLAG_NAK1
+ elif in_flags == FLAG_CFRM:
+ # this is a CFRM: request for confirmation
+ self.out_nextrepeattime = self.cur_time
+ # receive this packet's payload if it is the next in the sequence
+ if in_diff == 0:
+ if len(rawdata) > 8:
+ #print ' '*self._dump_indent, 'RECV ', self.in_nextseqid, repr(rawdata[4:])
+ payload = rawdata[4:-4]
+ crc, = struct.unpack("!I", rawdata[-4:])
+ if crc != (crc32(payload, self.in_crc) & 0xffffffff):
+ self.bad_crc()
+ return '' # bad crc! drop packet
+ self.in_nextseqid = (self.in_nextseqid + 1) & 0xFFFF
+ self.in_crc = crc
+ result = [payload]
+ while self.in_nextseqid in self.in_outoforder:
+ rawdata = self.in_outoforder.pop(self.in_nextseqid)
+ payload = rawdata[4:-4]
+ crc, = struct.unpack("!I", rawdata[-4:])
+ if crc != (crc32(payload, self.in_crc) & 0xffffffff):
+ # bad crc! clear all out-of-order packets
+ self.bad_crc()
+ break
+ self.in_nextseqid = (self.in_nextseqid + 1) & 0xFFFF
+ self.in_crc = crc
+ result.append(payload)
+ return ''.join(result)
+ else:
+ # we missed at least one intermediate packet: send a NAK
+ if len(rawdata) > 4:
+ self.in_outoforder[in_seqid] = rawdata
+ if ((self.in_nextseqid + 1) & 0xFFFF) in self.in_outoforder:
+ self.out_flags = FLAG_NAK1
+ else:
+ self.out_flags = FLAG_NAK
+ self.out_nextrepeattime = self.cur_time
+ return ''
+
+ def bad_crc(self):
+ import sys
+ print >> sys.stderr, "warning: bad crc on udp connexion"
+ self.in_outoforder.clear()
+ self.out_flags = FLAG_NAK
+ self.out_nextrepeattime = self.cur_time
+
+ _dump_indent = 0
+ def dump(self, dir, rawdata):
+ in_flags, ack_seqid, in_seqid = struct.unpack("!BBH", rawdata[:4])
+ print ' ' * self._dump_indent, dir,
+ if in_flags == FLAG_NAK:
+ print 'NAK',
+ elif in_flags == FLAG_NAK1:
+ print 'NAK1',
+ elif in_flags == FLAG_CFRM:
+ print 'CFRM',
+ #print ack_seqid, in_seqid, '(%d bytes)' % (len(rawdata)-4,)
+ print ack_seqid, in_seqid, repr(rawdata[4:])
+
+
+def pipe_over_udp(udpsock, send_fd=-1, recv_fd=-1,
+ timeout=1.0, inactivity_timeout=None):
+ """Example: send all data showing up in send_fd over the given UDP
+ socket, and write incoming data into recv_fd. The send_fd and
+ recv_fd are plain file descriptors. When an EOF is read from
+ send_fd, this function returns (after making sure that all data was
+ received by the remote side).
+ """
+ import os
+ from select import select
+ from time import time
+ p = PipeLayer()
+ p.timeout = timeout
+ iwtdlist = [udpsock]
+ if send_fd >= 0:
+ iwtdlist.append(send_fd)
+ running = True
+ while running or not p.in_sync():
+ delay = delay1 = p.settime(time())
+ if delay is None:
+ delay = inactivity_timeout
+ iwtd, owtd, ewtd = select(iwtdlist, [], [], delay)
+ if iwtd:
+ if send_fd in iwtd:
+ data = os.read(send_fd, 1500 - p.headersize)
+ if not data:
+ # EOF
+ iwtdlist.remove(send_fd)
+ running = False
+ else:
+ #print 'queue', len(data)
+ p.queue(data)
+ if udpsock in iwtd:
+ packet = udpsock.recv(65535)
+ #print 'decode', len(packet)
+ p.settime(time())
+ data = p.decode(packet)
+ i = 0
+ while i < len(data):
+ i += os.write(recv_fd, data[i:])
+ elif delay1 is None:
+ break # long inactivity
+ p.settime(time())
+ packet = p.encode(1500)
+ if packet:
+ #print 'send', len(packet)
+ #if os.urandom(1) >= '\x08': # emulate packet losses
+ udpsock.send(packet)
+
+
+class PipeOverUdp(object):
+
+ def __init__(self, udpsock, timeout=1.0):
+ import thread, os
+ self.os = os
+ self.sendpipe = os.pipe()
+ self.recvpipe = os.pipe()
+ thread.start_new_thread(pipe_over_udp, (udpsock,
+ self.sendpipe[0],
+ self.recvpipe[1],
+ timeout))
+
+ def __del__(self):
+ os = self.os
+ if self.sendpipe:
+ os.close(self.sendpipe[0])
+ os.close(self.sendpipe[1])
+ self.sendpipe = None
+ if self.recvpipe:
+ os.close(self.recvpipe[0])
+ os.close(self.recvpipe[1])
+ self.recvpipe = None
+
+ close = __del__
+
+ def send(self, data):
+ if not self.sendpipe:
+ raise IOError("I/O operation on a closed PipeOverUdp")
+ return self.os.write(self.sendpipe[1], data)
+
+ def sendall(self, data):
+ i = 0
+ while i < len(data):
+ i += self.send(data[i:])
+
+ def recv(self, bufsize):
+ if not self.recvpipe:
+ raise IOError("I/O operation on a closed PipeOverUdp")
+ return self.os.read(self.recvpipe[0], bufsize)
+
+ def recvall(self, bufsize):
+ buf = []
+ while bufsize > 0:
+ data = self.recv(bufsize)
+ buf.append(data)
+ bufsize -= len(data)
+ return ''.join(buf)
+
+ def fileno(self):
+ if not self.recvpipe:
+ raise IOError("I/O operation on a closed PipeOverUdp")
+ return self.recvpipe[0]
+
+ def ofileno(self):
+ if not self.sendpipe:
+ raise IOError("I/O operation on a closed PipeOverUdp")
+ return self.sendpipe[1]
diff --git a/metaserver/socketoverudp.py b/metaserver/socketoverudp.py
new file mode 100644
index 0000000..2df4a75
--- /dev/null
+++ b/metaserver/socketoverudp.py
@@ -0,0 +1,174 @@
+from time import time as now
+from pipelayer import PipeLayer, InvalidPacket
+from pipelayer import FLAG_RANGE_START, FLAG_RANGE_STOP
+import socket, struct
+
+SOU_RANGE_START = FLAG_RANGE_START
+SOU_MIXED_DATA = FLAG_RANGE_STOP + 0
+SOU_SHUTDOWN = FLAG_RANGE_STOP + 1
+SOU_RANGE_STOP = FLAG_RANGE_STOP + 2
+
+SHUTDOWN_PACKET = chr(SOU_SHUTDOWN) + '**' # < 4 characters
+
+CONGESTION_TIMEOUT = 20.0
+#CONSOLIDATE_DELAY = 0.1
+
+
+class SocketOverUdp(object):
+ RECV_CAN_RETURN_EMPTY = True
+ PACKETSIZE = 996
+ MIXEDPACKETSIZE = 1080
+
+ def __init__(self, udpsock, initialcrcs):
+ self.udpsock = udpsock
+ self.pl = PipeLayer(initialcrcs)
+ self.congested_since = None
+ #self.consolidate_sends = None
+ #self.encode_delayed_until = now()
+
+ def close(self):
+ try:
+ self.udpsock.send(SHUTDOWN_PACKET)
+ except socket.error:
+ pass
+ self.udpsock.close()
+
+ def _progress(self):
+ if self.pl.settime(now()) == 0.0:
+ self._encode()
+
+ def _encode(self):
+ #if self.consolidate_sends:
+ # if self.pl.cur_time < self.encode_delayed_until:
+ # return False
+ # self.encode_delayed_until = self.pl.cur_time + CONSOLIDATE_DELAY
+ packet = self.pl.encode(self.PACKETSIZE)
+ if packet is not None:
+ #print 'send:', repr(packet)
+ if self.pl.is_congested():
+ if self.congested_since is None:
+ self.congested_since = now()
+ else:
+ if now() > self.congested_since + CONGESTION_TIMEOUT:
+ self.udpsock.send(SHUTDOWN_PACKET)
+ raise socket.error("peer not responding, timing out")
+ else:
+ self.congested_since = None
+ #print repr(packet[:10])
+ #print "out:", len(packet)
+ #print ' ---'
+ self.udpsock.send(packet)
+
+ def _decode(self, packet):
+ try:
+ data = self.pl.decode(packet)
+ #print ' ~~~'
+ return data
+ except InvalidPacket:
+ if len(packet) >= 4:
+ hdr, reserved, size = struct.unpack("!BBH", packet[:4])
+ if hdr == SOU_MIXED_DATA:
+ #print ' ~~~[unmix%d/%d]' % (len(packet[4+size:]),
+ # len(packet))
+ self.udp_over_udp_decoder(packet[4:4+size])
+ return self._decode(packet[4+size:])
+ else:
+ # non-tiny packets with no recognized hdr byte are
+ # assumed to be pure video traffic
+ #print ' ~~~[video]'
+ self.udp_over_udp_decoder(packet)
+ return ''
+ elif packet == SHUTDOWN_PACKET:
+ raise socket.error("received an end-of-connexion packet")
+ else:
+ #print ' ~~~[INVALID%d]' % (len(packet),)
+ return ''
+
+ def fileno(self):
+ self._progress()
+ return self.udpsock.fileno()
+
+ def flush(self):
+ while self.pl.settime(now()) == 0.0:
+ #self.encode_delayed_until = self.pl.cur_time
+ self._encode()
+
+ def recv(self, _ignoredbufsize=None):
+ #print 'recv:'
+ packet = self.udpsock.recv(65535)
+ #print " in:", len(packet), hex(ord(packet[0]))
+ #print repr(packet)
+ self.pl.settime(now())
+ data = self._decode(packet)
+ #print 'which is really', repr(data)
+ self._encode()
+ #if data:
+ # print " IN:", len(data)
+ return data
+
+ def sendall(self, data):
+ #print 'queuing', repr(data)
+ #print ' OUT:', len(data)
+ self.pl.queue(data)
+ #self._progress()
+ return len(data)
+
+ send = sendall
+
+ def send_video_data(self, udpdata):
+ forced_embedded = SOU_RANGE_START <= ord(udpdata[0]) < SOU_RANGE_STOP
+ self.pl.settime(now())
+ packet = self.pl.encode(self.PACKETSIZE) or ''
+ if not forced_embedded and not packet:
+ # no PipeLayer packet, send as plain udp data
+ datagram = udpdata
+ elif len(packet) + len(udpdata) <= self.MIXEDPACKETSIZE:
+ # fits in a single mixed data packet
+ datagram = (struct.pack("!BBH", SOU_MIXED_DATA, 0, len(udpdata))
+ + udpdata + packet)
+ #print ' ---[mix%d/%d]' % (len(packet), len(datagram))
+ else:
+ # two packets needed
+ #print repr(packet[:10])
+ #print "out:", len(packet)
+ #print ' ---'
+ self.udpsock.send(packet)
+ datagram = udpdata
+ #print repr(datagram[:10])
+ #print "out:", len(datagram), hex(ord(datagram[0]))
+ self.udpsock.send(datagram)
+ #self.encode_delayed_until = self.pl.cur_time + CONSOLIDATE_DELAY
+ #if self.consolidate_sends is None:
+ # self.consolidate_sends = True
+ return len(udpdata)
+
+ def udp_over_udp_mixer(self):
+ return UdpOverUdpMixer(self)
+
+ def udp_over_udp_decoder(self, data):
+ pass # method overridden by pclient.py
+
+ def getpeername(self):
+ return self.udpsock.getpeername()
+
+ def getsockname(self):
+ return self.udpsock.getsockname()
+
+ def setsockopt(self, level, opt, value):
+ # note that TCP_NODELAY is set by the bub-n-bros client, not the server
+ #if level == socket.SOL_TCP and opt == socket.TCP_NODELAY:
+ # self.consolidate_sends = not value
+ #else:
+ # ignored
+ pass
+
+ def setblocking(self, _ignored):
+ pass # XXX good enough for common/gamesrv.py
+
+
+class UdpOverUdpMixer(object):
+ def __init__(self, sockoverudp):
+ self.send = sockoverudp.send_video_data
+
+ def setsockopt(self, *args):
+ pass # ignored