drop mysticmine BDEP on python2 cython

Convert ai.pyx to ai.py which allows us to drop the build dependency on
python2 cython. This will allow us to make cython python3-only.
This commit is contained in:
daniel
2023-11-26 05:36:36 +00:00
parent 5ea4831be7
commit b4f2917f3a
4 changed files with 480 additions and 6 deletions
+2 -2
View File
@@ -6,7 +6,7 @@ DISTNAME = mysticmine-${MODPY_EGG_VERSION}
GH_ACCOUNT = dewitters
GH_PROJECT = MysticMine
GH_COMMIT = 2fc0a5eaa0ab299c3a23ce17ae1c56a98055a44c
REVISION = 3
REVISION = 4
CATEGORIES = games
@@ -17,7 +17,7 @@ WANTLIB = pthread ${MODPY_WANTLIB}
MODULES = lang/python
MODPY_VERSION = ${MODPY_DEFAULT_VERSION_2}
BUILD_DEPENDS = lang/cython${MODPY_FLAVOR}
RUN_DEPENDS = devel/pygame${MODPY_FLAVOR} \
math/py2-numpy
@@ -0,0 +1,467 @@
convert ai.pyx -> ai.py to remove cython2 dependency
Index: monorail/ai.py
--- monorail/ai.py.orig
+++ monorail/ai.py
@@ -0,0 +1,461 @@
+import copy
+import tiles
+import pickups
+
+
+class AiNode:
+
+ def __init__(self, parent):
+ """Creates a new instance when a parent is known.
+
+ When parent is unknown, use static method 'create'.
+ """
+ self.parent = parent
+
+ def _generate_childeren( self ):
+ """Generate and return the list of childeren nodes of this node
+ """
+ childeren = []
+ trailnodes = self.trailnode.get_out_nodes()
+ for n in trailnodes:
+ node = AiNode( self )
+
+ node.carstate = self.carstate
+ node.playfieldstate = self.playfieldstate
+ node.other_trees = self.other_trees
+
+ node.trailnode = n
+ childeren.append( node )
+
+ return childeren
+
+ def set_playfield( self, playfield ):
+ self.playfieldstate = PlayfieldState( playfield )
+
+ def set_other_trees( self, other_trees ):
+ self.other_trees = other_trees
+
+ def _calc_score( self, distance ):
+ if self.parent is not None:
+ self.playfieldstate = self.parent.playfieldstate
+ self.carstate = self.parent.carstate
+
+ node = self
+
+ if node.playfieldstate is None:
+ print("playfieldstate shouldn't be None in real game")
+ return 0
+
+ score = 0
+
+ if isinstance(node.trailnode.tile, tiles.Enterance):
+ if isinstance( node.carstate.collectible, pickups.Diamond ):
+ node.carstate = copy.copy( node.carstate )
+ node.carstate.collectible = None
+ score = score + 2
+
+ score = score + self._calc_tile_pickups( node )
+ score = score + self._calc_other_cars( node, distance )
+
+ return score
+
+ def _calc_tile_pickups( self, node ):
+ score = 0
+
+ if node.playfieldstate.get_pickup( node.trailnode.tile ) is not None:
+ if isinstance(node.trailnode.tile.pickup, pickups.CopperCoin):
+ score = 1
+ elif isinstance(node.trailnode.tile.pickup, pickups.GoldBlock):
+ if isinstance( node.carstate.collectible, pickups.Axe ):
+ score = 1
+ else:
+ score = 0.1
+ elif isinstance(node.trailnode.tile.pickup, pickups.RockBlock):
+ score = -1
+ elif isinstance(node.trailnode.tile.pickup, pickups.Diamond):
+ if not isinstance(node.carstate.collectible, pickups.Diamond):
+ score = 1
+ elif isinstance(node.trailnode.tile.pickup, pickups.Dynamite):
+ score = -8
+ elif isinstance(node.trailnode.tile.pickup, pickups.Lamp):
+ score = 5
+ elif isinstance(node.trailnode.tile.pickup, pickups.Axe):
+ score = 2
+ elif isinstance(node.trailnode.tile.pickup, pickups.Flag):
+ if self.carstate.goldcar.nr == node.trailnode.tile.pickup.goldcar.nr:
+ score = 1
+ elif isinstance(node.trailnode.tile.pickup, pickups.Leprechaun):
+ score = -2
+ elif isinstance(node.trailnode.tile.pickup, pickups.Torch):
+ score = 0
+ elif isinstance(node.trailnode.tile.pickup, pickups.Key):
+ score = 1
+ elif isinstance(node.trailnode.tile.pickup, pickups.Mirror):
+ score = 1
+ elif isinstance(node.trailnode.tile.pickup, pickups.Oiler):
+ score = 1
+ elif isinstance(node.trailnode.tile.pickup, pickups.Multiplier):
+ score = 1
+ elif isinstance(node.trailnode.tile.pickup, pickups.Balloon):
+ score = 0
+ elif isinstance(node.trailnode.tile.pickup, pickups.Ghost):
+ score = 1
+
+ if isinstance( node.trailnode.tile.pickup, pickups.Collectible ):
+ node.carstate = copy.copy( node.carstate )
+ node.carstate.collectible = node.trailnode.tile.pickup
+ elif isinstance( node.trailnode.tile.pickup, pickups.PowerUp ):
+ node.carstate = copy.copy( node.carstate )
+ node.carstate.modifier = node.trailnode.tile.pickup
+
+ node.playfieldstate = node.playfieldstate.clone()
+ node.playfieldstate.remove_pickup( node.trailnode.tile )
+
+ return score
+
+ def _calc_other_cars( self, node, distance ):
+ score = 0
+
+ for tree in self.other_trees:
+ gen_nodes = tree.get_nodes_of_generation( distance )
+ for ai_node in gen_nodes:
+ othernode = ai_node.smartnode
+ if node.trailnode.tile is othernode.trailnode.tile:
+ if othernode.carstate.goldcar.collectible is not None:
+ if othernode.carstate.goldcar.collectible.is_good():
+ score = score + 5.0 / len( gen_nodes ) / max(distance, 1)
+ elif othernode.carstate.goldcar.collectible.is_bad():
+ score = score - 5.0 / len( gen_nodes ) / max(distance, 1)
+ if node.carstate.goldcar.collectible is not None:
+ if node.carstate.goldcar.collectible.is_good():
+ score = score - 5.0 / len( gen_nodes ) / max(distance, 1)
+ elif node.carstate.goldcar.collectible.is_bad():
+ score = score + 5.0 / len( gen_nodes ) / max(distance, 1)
+
+ # Calc parent node when crossing (can pass by)
+ if node.parent is not None and\
+ node.parent.trailnode.tile is othernode.trailnode.tile and\
+ node.parent.trailnode.in_dir != othernode.trailnode.in_dir:
+ if othernode.carstate.goldcar.collectible is not None:
+ if othernode.carstate.goldcar.collectible.is_good():
+ score = score + 5.0 / len( gen_nodes ) / max(distance, 1)
+ elif othernode.carstate.goldcar.collectible.is_bad():
+ score = score - 5.0 / len( gen_nodes ) / max(distance, 1)
+ if node.parent.carstate.goldcar.collectible is not None:
+ if node.parent.carstate.goldcar.collectible.is_good():
+ score = score - 5.0 / len( gen_nodes ) / max(distance, 1)
+ elif node.parent.carstate.goldcar.collectible.is_bad():
+ score = score + 5.0 / len( gen_nodes ) / max(distance, 1)
+
+ return score
+
+ def equals( self, other ):
+ return self.trailnode.tile is other.trailnode.tile and \
+ self.trailnode.in_dir.__eq__(other.trailnode.in_dir)
+
+ def nequals( self, other ):
+ return not self.equals(other)
+
+
+class Node:
+ """An abstract node used in PredictionTree.
+
+ public members:
+ - generation: the generation of this node
+ - parent: this nodes parent
+ """
+
+ def __init__( self, smartnode, parent=None ):
+ self.smartnode = smartnode
+ self.parent = parent
+ self.childeren = None
+ self._best_score = -999
+
+ if parent is not None:
+ self.generation = parent.generation+1
+ else:
+ self.generation = 0
+
+ def generate_childeren( self ):
+ """Generate and return the list of childeren nodes of this node
+ """
+ childeren = self.smartnode._generate_childeren()
+
+ self.childeren = []
+ for child in childeren:
+ self.childeren.append( Node(child, self) )
+
+ return self.childeren
+
+ def get_generation( self, ancestor_node ):
+ generation = 0
+ node_it = self
+ while node_it is not None and node_it is not ancestor_node:
+ node_it = node_it.parent
+ generation = generation + 1
+
+ if node_it is None:
+ generation = -1
+
+ return generation
+
+ # used externally!!!!
+ def get_childeren( self ):
+ return self.childeren
+
+ def calc_score( self ):
+ """Return the individual score of this node
+ """
+ return self.smartnode._calc_score(self.generation)
+
+ def get_total_score( self ):
+ node = self
+ total_score = 0
+ i = 0
+ while node is not None:
+ i = i + 1
+
+ total_score = total_score + node.score * i
+ node = node.parent
+
+ return total_score / i
+
+ def set_score( self, score ):
+ """Set the score of this node, and possibly update other nodes in its path.
+
+ Other nodes are only updated if this node didn't have best score, or
+ when it's a leaf node.
+ """
+ self.score = score
+
+ # handle best score
+ if self.is_leaf() or self._best_score == -999:
+ self._best_score = self.get_total_score()
+ if self.parent is not None:
+ self.parent._recalc_best_score()
+
+ def _recalc_best_score( self ):
+ old_score = self._best_score
+
+ self._best_score = -999
+ for child in self.childeren:
+ if self._best_score == -999 or \
+ child._best_score > self._best_score:
+ self._best_score = child._best_score
+
+ if self._best_score != old_score and \
+ self.parent is not None:
+ self.parent._recalc_best_score()
+
+ # used externally!!!
+ def get_best_score( self ):
+ """Return the highest score that this node can reach.
+ """
+ return self._best_score
+
+ # used externally!!!
+ def get_score( self ):
+ return self.score
+
+ def get_best_childs( self ):
+
+ if self.childeren is None:
+ return []
+
+ best_childs = []
+ for child in self.childeren:
+ best_child = None
+ if len(best_childs) > 0:
+ best_child = best_childs[0]
+
+ if (len(best_childs) == 0 or child._best_score > best_child._best_score):
+ best_childs = [child]
+ elif child._best_score == best_child._best_score:
+ best_childs.append( child )
+
+ return best_childs
+
+ def is_leaf( self ):
+ return (self.childeren is None) or (len( self.childeren ) == 0)
+
+ CYCLES_PER_UPDATE = 256*4
+
+
+class PredictionTree:
+ """A tree structure that contains all possible future moves with scores.
+
+ Public members:
+ - root_node: the root node of this tree
+ - total_generations: the total generations of this tree
+ """
+
+ def __init__( self, MAX_NODES=256*2, CYCLES_PER_UPDATE=256 ):
+ self.root_node = None
+ self.total_generations = 0
+ self.generations = []
+ self.MAX_NODES = MAX_NODES
+ self.nodes_calc = None
+ self.CYCLES_PER_UPDATE = CYCLES_PER_UPDATE
+ self.node_cnt = 0
+
+ def update( self ):
+ """Update the tree as much as possible, using CYCLES_PER_UPDATE as upper limit.
+ """
+
+ if self.root_node is not None:
+ # We make sure we calculate all in limited time
+ cycles_left = self.CYCLES_PER_UPDATE*2/3
+ cycles_left = self._update_tree( cycles_left )
+
+ cycles_left = cycles_left + self.CYCLES_PER_UPDATE*1/3
+ cycles_left = self._calc_nodes_scores( cycles_left )
+
+ self._update_tree( cycles_left )
+
+ def set_root( self, node ):
+ """Change the root node in an optimized way.
+
+ If the node equals a child of the current root, then the current tree is
+ reused.
+ """
+
+ found = False
+ # first try one of its childeren
+ if self.root_node is not None:
+ for child in self.root_node.childeren:
+ if child.smartnode.equals(node.smartnode):
+ found = True
+ self.root_node = child
+ self.root_node.parent = None
+ self.total_generations = self.total_generations - 1
+ self.nodes_calc = [self.root_node] # Recalculate scores of nodes
+
+ if not found:
+ self.root_node = node
+ self.leafs = [self.root_node]
+ self.total_generations = 0
+ self.nodes_calc = [self.root_node] # Recalculate scores of nodes
+
+ self._update_generations()
+
+ def _update_generations( self ):
+ """Update our generation nodes
+ """
+ assert self.root_node is not None, "Don't call this when root_node is None"
+
+ self.generations = []
+ self.node_cnt = 0
+
+ generation = [self.root_node]
+ while len( generation ) > 0:
+ next_generation = []
+ for node in generation:
+ self.node_cnt = self.node_cnt + 1
+ node.generation = len( self.generations )
+ childeren = node.childeren
+ if childeren is not None:
+ next_generation.extend( childeren )
+
+ self.generations.append( generation )
+ generation = next_generation
+
+ def _update_tree( self, cycles_left ):
+ """Update the tree until the maximum of generations is reached.
+ """
+
+ assert self.root_node is not None, "Don't call this when root_node is None"
+
+ while self.node_cnt < self.MAX_NODES and len(self.leafs) > 0 and cycles_left > 0:
+ cycles_left = cycles_left - 1
+
+ node = self.leafs.pop(0)
+ gen = node.get_generation( self.root_node )
+ if gen != -1: # else it's a leaf of old root_node
+ self.total_generations = gen - 1
+ nodes = node.generate_childeren()
+ self.node_cnt = self.node_cnt + len(nodes)
+ self.leafs.extend( nodes )
+
+ node.generation = gen
+ self.get_nodes_of_generation( gen+1 ).extend( nodes )
+
+ return cycles_left
+
+ def _calc_nodes_scores( self, cycles_left ):
+ """Calculate the score of the nodes that aren't calculated yet.
+ """
+ assert self.root_node is not None, "Don't call this when root_node is None"
+
+ if len( self.nodes_calc ) == 0:
+ self.nodes_calc = [self.root_node]
+
+ while len(self.nodes_calc) > 0 and cycles_left > 0:
+ cycles_left = cycles_left - 1
+
+ node = self.nodes_calc.pop(0)
+ node.set_score( node.calc_score() )
+
+ childeren = node.childeren
+ if childeren is not None:
+ self.nodes_calc.extend( childeren )
+
+ return cycles_left
+
+ def get_nodes_of_generation( self, gen ):
+ """Return all the nodes of the generation.
+ """
+ if gen is not None and gen > -1 and gen < len( self.generations ):
+ return self.generations[ gen ]
+ else:
+ return []
+
+
+class PlayfieldState:
+
+ def __init__( self, playfield ):
+ self.playfield = playfield
+
+ self.pickups = []
+ for tile in self.playfield.level.tiles:
+ if tile.pickup is not None:
+ self.pickups.append( [tile, tile.pickup] )
+
+ def reset( self ):
+ self.pickups = []
+ for tile in self.playfield.level.tiles:
+ if tile.pickup is not None:
+ self.pickups.append( [tile, tile.pickup] )
+
+ def get_pickup( self, tile ):
+ for (t, pickup) in self.pickups:
+ if tile is t:
+ return pickup
+
+ return None
+
+ def remove_pickup( self, tile ):
+ remove_index = None
+ i = 0
+ for (t, pickup) in self.pickups:
+ if tile is t:
+ remove_index = i
+ i = i + 1
+
+ if remove_index is not None:
+ del self.pickups[remove_index]
+
+ def clone( self ):
+ clone = PlayfieldState( self.playfield )
+ clone.pickups = copy.copy( self.pickups )
+ return clone
+
+
+def AiNode_create( goldcarstate, trailnode=None ):
+ self = AiNode( None )
+
+ self.trailnode = trailnode
+ self.carstate = goldcarstate
+ self.playfieldstate = None
+ self.other_trees = None
+
+ return self
+9 -3
View File
@@ -1,15 +1,21 @@
Index: setup.py
--- setup.py.orig
+++ setup.py
@@ -1,6 +1,6 @@
@@ -1,6 +1,5 @@
from distutils.core import Extension, setup
from distutils.command.install import INSTALL_SCHEMES
-from Pyrex.Distutils import build_ext
+from Cython.Distutils import build_ext
import os
# http://stackoverflow.com/questions/1612733/including-non-python-files-with-setup-py
@@ -52,6 +52,5 @@ setup( name='MysticMine',
@@ -45,13 +44,8 @@ setup( name='MysticMine',
('monorail/data/music',music),
('monorail/data/snd',snd),
],
- ext_modules=[
- Extension("monorail.ai", ["monorail/ai.pyx"])
- ],
- cmdclass={'build_ext': build_ext},
requires=[
"pygame",
"numpy",
+2 -1
View File
@@ -3,7 +3,8 @@ lib/python${MODPY_VERSION}/site-packages/MysticMine-${MODPY_EGG_VERSION}-py${MOD
lib/python${MODPY_VERSION}/site-packages/monorail/
lib/python${MODPY_VERSION}/site-packages/monorail/__init__.py
lib/python${MODPY_VERSION}/site-packages/monorail/__init__.pyc
@so lib/python${MODPY_VERSION}/site-packages/monorail/ai.so
lib/python${MODPY_VERSION}/site-packages/monorail/ai.py
lib/python${MODPY_VERSION}/site-packages/monorail/ai.pyc
lib/python${MODPY_VERSION}/site-packages/monorail/control.py
lib/python${MODPY_VERSION}/site-packages/monorail/control.pyc
lib/python${MODPY_VERSION}/site-packages/monorail/controlview.py