changeset 447:78e1c3864e73

Make pytouhou.game.game an extension type.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Sat, 17 Aug 2013 06:29:53 +0200
parents 3a33ed7f3b85
children 3bc37791f0a2
files pytouhou/game/bullet.pxd pytouhou/game/bullet.pyx pytouhou/game/effect.pyx pytouhou/game/enemy.pxd pytouhou/game/enemy.pyx pytouhou/game/game.pxd pytouhou/game/game.py pytouhou/game/game.pyx pytouhou/game/item.pxd pytouhou/game/item.pyx pytouhou/game/player.pxd pytouhou/game/player.pyx
diffstat 11 files changed, 183 insertions(+), 88 deletions(-) [+]
line wrap: on
line diff
--- a/pytouhou/game/bullet.pxd
+++ b/pytouhou/game/bullet.pxd
@@ -1,15 +1,17 @@
 from pytouhou.game.element cimport Element
+from pytouhou.game.game cimport Game
 from pytouhou.utils.interpolator cimport Interpolator
 
 cdef class Bullet(Element):
     cdef public unsigned long state, flags, frame, sprite_idx_offset, damage
     cdef public double dx, dy, angle, speed
     cdef public bint player_bullet, was_visible, grazed
-    cdef public object target, _game, _bullet_type
+    cdef public object target, _bullet_type
     cdef public tuple hitbox
     cdef public list attributes
 
     cdef Interpolator speed_interpolator
+    cdef Game _game
 
     cdef bint is_visible(self, unsigned int screen_width, unsigned int screen_height)
     cpdef set_anim(self, sprite_idx_offset=*)
--- a/pytouhou/game/bullet.pyx
+++ b/pytouhou/game/bullet.pyx
@@ -22,7 +22,7 @@ LAUNCHING, LAUNCHED, CANCELLED = range(3
 
 cdef class Bullet(Element):
     def __init__(self, pos, bullet_type, unsigned long sprite_idx_offset,
-                       double angle, double speed, attributes, unsigned long flags, target, game,
+                       double angle, double speed, attributes, unsigned long flags, target, Game game,
                        bint player_bullet=False, unsigned long damage=0, hitbox=None):
         cdef double launch_mult
 
--- a/pytouhou/game/effect.pyx
+++ b/pytouhou/game/effect.pyx
@@ -15,6 +15,8 @@
 from pytouhou.game.sprite cimport Sprite
 from pytouhou.vm.anmrunner import ANMRunner
 
+from pytouhou.game.game cimport Game
+
 
 cdef class Effect(Element):
     def __init__(self, pos, index, anm):
@@ -35,7 +37,7 @@ cdef class Effect(Element):
 
 
 cdef class Particle(Effect):
-    def __init__(self, pos, index, anm, long amp, game, bint reverse=False, long duration=24):
+    def __init__(self, pos, index, anm, long amp, Game game, bint reverse=False, long duration=24):
         Effect.__init__(self, pos, index, anm)
 
         self.frame = 0
--- a/pytouhou/game/enemy.pxd
+++ b/pytouhou/game/enemy.pxd
@@ -1,4 +1,5 @@
 from pytouhou.game.element cimport Element
+from pytouhou.game.game cimport Game
 from pytouhou.utils.interpolator cimport Interpolator
 
 cdef class Enemy(Element):
@@ -9,8 +10,9 @@ cdef class Enemy(Element):
     cdef public dict laser_by_id
     cdef public list aux_anm
     cdef public Interpolator interpolator, speed_interpolator
-    cdef public object _game, _anms, process
+    cdef public object _anms, process
 
+    cdef Game _game
     cdef double[2] hitbox_half_size
 
     cpdef play_sound(self, index)
@@ -34,7 +36,7 @@ cdef class Enemy(Element):
     cpdef set_pos(self, x, y, z)
     cpdef move_to(self, duration, x, y, z, formula)
     cpdef stop_in(self, duration, formula)
-    cpdef bint is_visible(self, long screen_width, long screen_height)
+    cdef bint is_visible(self, long screen_width, long screen_height)
     cdef void check_collisions(self)
     cdef void handle_callbacks(self)
     cpdef update(self)
--- a/pytouhou/game/enemy.pyx
+++ b/pytouhou/game/enemy.pyx
@@ -22,7 +22,7 @@ from pytouhou.game.effect cimport Effect
 
 
 cdef class Enemy(Element):
-    def __init__(self, pos, long life, long _type, long bonus_dropped, long die_score, anms, game):
+    def __init__(self, pos, long life, long _type, long bonus_dropped, long die_score, anms, Game game):
         Element.__init__(self)
 
         self._game = game
@@ -272,7 +272,7 @@ cdef class Enemy(Element):
                                                formula)
 
 
-    cpdef bint is_visible(self, long screen_width, long screen_height):
+    cdef bint is_visible(self, long screen_width, long screen_height):
         cdef double tw, th
 
         if self.sprite is not None:
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/game.pxd
@@ -0,0 +1,28 @@
+from pytouhou.game.effect cimport Effect
+from pytouhou.game.player cimport Player
+
+cdef class Game:
+    cdef public long width, height, nb_bullets_max, stage, rank, difficulty, difficulty_counter, difficulty_min, difficulty_max, frame, last_keystate
+    cdef public list bullet_types, laser_types, item_types, players, enemies, effects, bullets, lasers, cancelled_bullets, players_bullets, players_lasers, items, labels, faces, texts, hints, bonus_list
+    cdef public object interface, boss, msg_runner, prng, sfx_player
+    cdef public double continues
+    cdef public Effect spellcard_effect
+    cdef public tuple spellcard
+    cdef public bint time_stop, msg_wait
+    cdef public unsigned short deaths_count, next_bonus
+
+    cpdef modify_difficulty(self, long diff)
+    cpdef drop_bonus(self, double x, double y, long _type, end_pos=*)
+    cdef void autocollect(self, Player player)
+    cpdef cancel_bullets(self)
+    cpdef new_particle(self, pos, long anim, long amp, long number=*, bint reverse=*, long duration=*)
+    cpdef new_label(self, pos, str text)
+    cdef void update_background(self)
+    cdef void update_enemies(self)
+    cdef void update_msg(self, long keystate) except *
+    cdef void update_players(self, long keystate) except *
+    cdef void update_effects(self)
+    cdef void update_hints(self)
+    cdef void update_faces(self)
+    cdef void update_bullets(self)
+    cdef void cleanup(self)
rename from pytouhou/game/game.py
rename to pytouhou/game/game.pyx
--- a/pytouhou/game/game.py
+++ b/pytouhou/game/game.pyx
@@ -12,22 +12,22 @@
 ## GNU General Public License for more details.
 ##
 
-from itertools import chain
-
 from pytouhou.vm.msgrunner import MSGRunner
 
+from pytouhou.game.element cimport Element
+from pytouhou.game.bullet cimport Bullet
 from pytouhou.game.bullet import LAUNCHED, CANCELLED
-from pytouhou.game.enemy import Enemy
-from pytouhou.game.item import Item
-from pytouhou.game.effect import Effect, Particle
+from pytouhou.game.enemy cimport Enemy
+from pytouhou.game.item cimport Item
+from pytouhou.game.effect cimport Particle
 from pytouhou.game.text import Text
 from pytouhou.game.face import Face
 
 
-class Game(object):
-    def __init__(self, players, stage, rank, difficulty, bullet_types,
-                 laser_types, item_types, nb_bullets_max=None, width=384,
-                 height=448, prng=None, interface=None, continues=0,
+cdef class Game:
+    def __init__(self, players, long stage, long rank, long difficulty, bullet_types,
+                 laser_types, item_types, long nb_bullets_max=0, long width=384,
+                 long height=448, prng=None, interface=None, double continues=0,
                  hints=None):
         self.width, self.height = width, height
 
@@ -66,6 +66,7 @@ class Game(object):
                            1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 2]
         self.prng = prng
         self.frame = 0
+        self.sfx_player = None
 
         self.spellcard_effect = None
 
@@ -77,14 +78,14 @@ class Game(object):
 
 
     def msg_sprites(self):
-        return [face for face in self.faces if face] if self.msg_runner and not self.msg_runner.ended else []
+        return [face for face in self.faces if face is not None] if self.msg_runner is not None and not self.msg_runner.ended else []
 
 
     def lasers_sprites(self):
-        return [laser for laser in self.players_lasers if laser]
+        return [laser for laser in self.players_lasers if laser is not None]
 
 
-    def modify_difficulty(self, diff):
+    cpdef modify_difficulty(self, long diff):
         self.difficulty_counter += diff
         while self.difficulty_counter < 0:
             self.difficulty -= 1
@@ -108,23 +109,26 @@ class Game(object):
         self.spellcard_effect = None
 
 
-    def drop_bonus(self, x, y, _type, end_pos=None):
-        player = self.players[0] #TODO
+    cpdef drop_bonus(self, double x, double y, long _type, end_pos=None):
         if _type > 6:
             return
         if len(self.items) >= self.nb_bullets_max:
             return #TODO: check
         item_type = self.item_types[_type]
-        item = Item((x, y), _type, item_type, self, end_pos=end_pos)
-        self.items.append(item)
+        self.items.append(Item((x, y), _type, item_type, self, end_pos=end_pos))
 
 
-    def autocollect(self, player):
+    cdef void autocollect(self, Player player):
+        cdef Item item
+
         for item in self.items:
             item.autocollect(player)
 
 
-    def cancel_bullets(self):
+    cpdef cancel_bullets(self):
+        cdef Bullet bullet
+        #TODO: cdef Laser laser
+
         for bullet in self.bullets:
             bullet.cancel()
         for laser in self.lasers:
@@ -132,6 +136,9 @@ class Game(object):
 
 
     def change_bullets_into_star_items(self):
+        cdef Player player
+        cdef Bullet bullet
+
         player = self.players[0] #TODO
         item_type = self.item_types[6]
         self.items.extend(Item((bullet.x, bullet.y), 6, item_type, self, player=player)
@@ -144,6 +151,9 @@ class Game(object):
 
 
     def change_bullets_into_bonus(self):
+        cdef Player player
+        cdef Bullet bullet
+
         player = self.players[0] #TODO
         score = 0
         bonus = 2000
@@ -157,6 +167,8 @@ class Game(object):
 
 
     def kill_enemies(self):
+        cdef Enemy enemy
+
         for enemy in self.enemies:
             if enemy.boss:
                 pass # Bosses are immune to 96
@@ -168,13 +180,13 @@ class Game(object):
                 enemy.death_callback = -1
 
 
-    def new_effect(self, pos, anim, anm=None, number=1):
+    def new_effect(self, pos, long anim, anm=None, long number=1):
         number = min(number, self.nb_bullets_max - len(self.effects))
         for i in xrange(number):
             self.effects.append(Effect(pos, anim, anm or self.etama[1]))
 
 
-    def new_particle(self, pos, anim, amp, number=1, reverse=False, duration=24):
+    cpdef new_particle(self, pos, long anim, long amp, long number=1, bint reverse=False, long duration=24):
         number = min(number, self.nb_bullets_max - len(self.effects))
         for i in xrange(number):
             self.effects.append(Particle(pos, anim, self.etama[1], amp, self, reverse=reverse, duration=duration))
@@ -191,7 +203,7 @@ class Game(object):
         self.msg_runner.run_iteration()
 
 
-    def new_label(self, pos, text):
+    cpdef new_label(self, pos, str text):
         label = Text(pos, self.interface.ascii_anm, text=text, xspacing=8, shift=48)
         label.set_timeout(60, effect='move')
         self.labels.append(label)
@@ -217,7 +229,7 @@ class Game(object):
         return face
 
 
-    def run_iter(self, keystate):
+    def run_iter(self, long keystate):
         # 1. VMs.
         for runner in self.ecl_runners:
             runner.run_iter()
@@ -227,12 +239,11 @@ class Game(object):
             self.modify_difficulty(+100)
 
         # 3. Filter out destroyed enemies
-        self.enemies = [enemy for enemy in self.enemies if not enemy.removed]
-        self.effects = [effect for effect in self.effects if not effect.removed]
-        self.bullets = [bullet for bullet in self.bullets if not bullet.removed]
-        self.cancelled_bullets = [bullet for bullet in self.cancelled_bullets if not bullet.removed]
-        self.items = [item for item in self.items if not item.removed]
-
+        self.enemies = filter_removed(self.enemies)
+        self.effects = filter_removed(self.effects)
+        self.bullets = filter_removed(self.bullets)
+        self.cancelled_bullets = filter_removed(self.cancelled_bullets)
+        self.items = filter_removed(self.items)
 
         # 4. Let's play!
         # In the original game, updates are done in prioritized functions called "chains"
@@ -240,7 +251,7 @@ class Game(object):
 
         # Pri 6 is background
         self.update_background() #TODO: Pri unknown
-        if self.msg_runner:
+        if self.msg_runner is not None:
             self.update_msg(keystate) # Pri ?
             keystate &= ~3 # Remove the ability to attack (keystates 1 and 2).
         self.update_players(keystate) # Pri 7
@@ -262,30 +273,37 @@ class Game(object):
         self.frame += 1
 
 
-    def update_background(self):
+    cdef void update_background(self):
         if self.time_stop:
-            return None
+            return
         if self.spellcard_effect is not None:
             self.spellcard_effect.update()
         #TODO: update the actual background here?
 
 
-    def update_enemies(self):
+    cdef void update_enemies(self):
+        cdef Enemy enemy
+
         for enemy in self.enemies:
             enemy.update()
 
 
-    def update_msg(self, keystate):
-        if any((keystate & k and not self.last_keystate & k) for k in (1, 256)):
+    cdef void update_msg(self, long keystate) except *:
+        cdef long k
+
+        if any([(keystate & k and not self.last_keystate & k) for k in (1, 256)]):
             self.msg_runner.skip()
         self.msg_runner.skipping = bool(keystate & 256)
         self.last_keystate = keystate
         self.msg_runner.run_iteration()
 
 
-    def update_players(self, keystate):
+    cdef void update_players(self, long keystate) except *:
+        cdef Bullet bullet
+        cdef Player player
+
         if self.time_stop:
-            return None
+            return
 
         for bullet in self.players_bullets:
             bullet.update()
@@ -299,26 +317,34 @@ class Game(object):
         #TODO: give extra lives to the player
 
 
-    def update_effects(self):
+    cdef void update_effects(self):
+        cdef Element effect
+
         for effect in self.effects:
             effect.update()
 
 
-    def update_hints(self):
+    cdef void update_hints(self):
         for hint in self.hints:
             if hint['Count'] == self.frame and hint['Base'] == 'start':
                 self.new_hint(hint)
 
 
-    def update_faces(self):
+    cdef void update_faces(self):
         for face in self.faces:
             if face:
                 face.update()
 
 
-    def update_bullets(self):
+    cdef void update_bullets(self):
+        cdef Player player
+        cdef Bullet bullet
+        cdef Item item
+        cdef double bhalf_width, bhalf_height
+
         if self.time_stop:
-            return None
+            return
+
         for bullet in self.cancelled_bullets:
             bullet.update()
 
@@ -326,32 +352,34 @@ class Game(object):
             bullet.update()
 
         for laser in self.players_lasers:
-            if laser:
+            if laser is not None:
                 laser.update()
 
         for item in self.items:
             item.update()
 
         for player in self.players:
-            if not player.state.touchable:
+            player_state = player.state
+
+            if not player_state.touchable:
                 continue
 
-            px, py = player.state.x, player.state.y
-            phalf_size = player.sht.hitbox
+            px, py = player_state.x, player_state.y
+            phalf_size = <double>player.sht.hitbox
             px1, px2 = px - phalf_size, px + phalf_size
             py1, py2 = py - phalf_size, py + phalf_size
 
-            ghalf_size = player.sht.graze_hitbox
+            ghalf_size = <double>player.sht.graze_hitbox
             gx1, gx2 = px - ghalf_size, px + ghalf_size
             gy1, gy2 = py - ghalf_size, py + ghalf_size
 
             for laser in self.lasers:
                 if laser.check_collision((px, py)):
-                    if player.state.invulnerable_time == 0:
+                    if player_state.invulnerable_time == 0:
                         player.collide()
                 elif laser.check_grazing((px, py)):
-                    player.state.graze += 1 #TODO
-                    player.state.score += 500 #TODO
+                    player_state.graze += 1 #TODO
+                    player_state.score += 500 #TODO
                     player.play_sound('graze')
                     self.modify_difficulty(+6) #TODO
                     self.new_particle((px, py), 9, 192) #TODO
@@ -368,14 +396,14 @@ class Game(object):
                 if not (bx2 < px1 or bx1 > px2
                         or by2 < py1 or by1 > py2):
                     bullet.collide()
-                    if player.state.invulnerable_time == 0:
+                    if player_state.invulnerable_time == 0:
                         player.collide()
 
                 elif not bullet.grazed and not (bx2 < gx1 or bx1 > gx2
                         or by2 < gy1 or by1 > gy2):
                     bullet.grazed = True
-                    player.state.graze += 1
-                    player.state.score += 500 # found experimentally
+                    player_state.graze += 1
+                    player_state.score += 500 # found experimentally
                     player.play_sound('graze')
                     self.modify_difficulty(+6)
                     self.new_particle((px, py), 9, 192) #TODO: find the real size and range.
@@ -383,10 +411,10 @@ class Game(object):
                     # 12 pixels of the player, in the axis of the “collision”.
 
             #TODO: is it the right place?
-            if py < 128 and player.state.power >= 128: #TODO: check py.
+            if py < 128 and player_state.power >= 128: #TODO: check py.
                 self.autocollect(player)
 
-            ihalf_size = player.sht.item_hitbox
+            ihalf_size = <double>player.sht.item_hitbox
             for item in self.items:
                 bx, by = item.x, item.y
                 bx1, bx2 = bx - ihalf_size, bx + ihalf_size
@@ -397,7 +425,12 @@ class Game(object):
                     item.on_collect(player)
 
 
-    def cleanup(self):
+    cdef void cleanup(self):
+        cdef Enemy enemy
+        cdef Bullet bullet
+        cdef Item item
+        cdef long i
+
         # Filter out non-visible enemies
         for enemy in self.enemies:
             if enemy.is_visible(self.width, self.height):
@@ -406,25 +439,41 @@ class Game(object):
                 # Filter out-of-screen enemy
                 enemy.removed = True
 
-        self.enemies = [enemy for enemy in self.enemies if not enemy.removed]
+        self.enemies = filter_removed(self.enemies)
 
-        # Update cancelled bullets
-        self.cancelled_bullets = [b for b in chain(self.cancelled_bullets,
-                                                   self.bullets,
-                                                   self.players_bullets)
-                                    if b.state == CANCELLED and not b.removed]
         # Filter out-of-scren bullets
-        self.bullets = [bullet for bullet in self.bullets
-                            if not bullet.removed and bullet.state != CANCELLED]
-        self.players_bullets = [bullet for bullet in self.players_bullets
-                            if not bullet.removed and bullet.state != CANCELLED]
-        for i, laser in enumerate(self.players_lasers):
-            if laser and laser.removed:
-                self.players_lasers[i] = None
-        self.effects = [effect for effect in self.effects if not effect.removed]
+        cancelled_bullets = []
+        bullets = []
+        players_bullets = []
+
+        for bullet in self.cancelled_bullets:
+            if bullet.state == CANCELLED and not bullet.removed:
+                cancelled_bullets.append(bullet)
+
+        for bullet in self.bullets:
+            if not bullet.removed:
+                if bullet.state == CANCELLED:
+                    cancelled_bullets.append(bullet)
+                else:
+                    bullets.append(bullet)
+
+        for bullet in self.players_bullets:
+            if not bullet.removed:
+                if bullet.state == CANCELLED:
+                    cancelled_bullets.append(bullet)
+                else:
+                    players_bullets.append(bullet)
+
+        self.cancelled_bullets = cancelled_bullets
+        self.bullets = bullets
+        self.players_bullets = players_bullets
 
         # Filter “timed-out” lasers
-        self.lasers = [laser for laser in self.lasers if not laser.removed]
+        for i, laser in enumerate(self.players_lasers):
+            if laser is not None and laser.removed:
+                self.players_lasers[i] = None
+
+        self.lasers = filter_removed(self.lasers)
 
         # Filter out-of-scren items
         items = []
@@ -435,9 +484,18 @@ class Game(object):
                 self.modify_difficulty(-3)
         self.items = items
 
-        self.labels = [label for label in self.labels if not label.removed]
+        self.effects = filter_removed(self.effects)
+        self.labels = filter_removed(self.labels)
 
         # Disable boss mode if it is dead/it has timeout
         if self.boss and self.boss._enemy.removed:
             self.boss = None
 
+cdef list filter_removed(list elements):
+    cdef Element element
+
+    filtered = []
+    for element in elements:
+        if not element.removed:
+            filtered.append(element)
+    return filtered
--- a/pytouhou/game/item.pxd
+++ b/pytouhou/game/item.pxd
@@ -1,4 +1,5 @@
 from pytouhou.game.element cimport Element
+from pytouhou.game.game cimport Game
 from pytouhou.game.player cimport Player
 from pytouhou.utils.interpolator cimport Interpolator
 
@@ -12,10 +13,10 @@ cdef class Indicator(Element):
 cdef class Item(Element):
     cdef public object _item_type
 
-    cdef object _game
     cdef unsigned long frame
     cdef long _type
     cdef double angle, speed
+    cdef Game _game
     cdef Player player
     cdef Indicator indicator
     cdef Interpolator speed_interpolator, pos_interpolator
--- a/pytouhou/game/item.pyx
+++ b/pytouhou/game/item.pyx
@@ -33,7 +33,7 @@ cdef class Indicator(Element):
 
 
 cdef class Item(Element):
-    def __init__(self, start_pos, long _type, item_type, game, double angle=pi/2, Player player=None, end_pos=None):
+    def __init__(self, start_pos, long _type, item_type, Game game, double angle=pi/2, Player player=None, end_pos=None):
         Element.__init__(self, start_pos)
 
         self._game = game
--- a/pytouhou/game/player.pxd
+++ b/pytouhou/game/player.pxd
@@ -1,4 +1,5 @@
 from pytouhou.game.element cimport Element
+from pytouhou.game.game cimport Game
 
 cdef class PlayerState:
     cdef public double x, y
@@ -8,8 +9,8 @@ cdef class PlayerState:
 
 cdef class Player(Element):
     cdef public PlayerState state
-    cdef public object _game
     cdef public long death_time
+    cdef public Game _game
 
     cdef object anm
     cdef tuple speeds
--- a/pytouhou/game/player.pyx
+++ b/pytouhou/game/player.pyx
@@ -55,7 +55,7 @@ cdef class PlayerState:
 
 
 cdef class Player(Element):
-    def __init__(self, PlayerState state, game, anm):
+    def __init__(self, PlayerState state, Game game, anm):
         Element.__init__(self)
 
         self._game = game
@@ -117,7 +117,7 @@ cdef class Player(Element):
 
         bullets = self._game.players_bullets
         lasers = self._game.players_lasers
-        nb_bullets_max = <long>self._game.nb_bullets_max
+        nb_bullets_max = self._game.nb_bullets_max
 
         if self.fire_time % 5 == 0:
             self.play_sound('plst00')
@@ -231,7 +231,7 @@ cdef class Player(Element):
                 self.fire_time -= 1
 
         if self.death_time:
-            time = <long>self._game.frame - self.death_time
+            time = self._game.frame - self.death_time
             if time == 6: # too late, you are dead :(
                 self.state.touchable = False
                 if self.state.power > 16:
@@ -277,6 +277,10 @@ cdef class Player(Element):
                 self.sprite.fade(26, 96)
                 self.sprite.scale_in(26, 0., 2.5)
 
+            #TODO: the next two branches could be happening at the same frame.
+            elif time == 31:
+                self._game.cancel_bullets()
+
             elif time == 32:
                 self.state.x = float(self._game.width) / 2. #TODO
                 self.state.y = float(self._game.width) #TODO
@@ -296,10 +300,7 @@ cdef class Player(Element):
                 self.sprite.blendfunc = 0
                 self.sprite.changed = True
 
-            if time > 30:
-                self._game.cancel_bullets()
-
-            if time > 90: # start the bullet hell again
+            elif time == 91: # start the bullet hell again
                 self.death_time = 0
 
         self.anmrunner.run_frame()