From b4f2917f3a0ebdf39b8ff71bd2ff073b69068e73 Mon Sep 17 00:00:00 2001 From: daniel Date: Sun, 26 Nov 2023 05:36:36 +0000 Subject: [PATCH] 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. --- games/mysticmine/Makefile | 4 +- games/mysticmine/patches/patch-monorail_ai_py | 467 ++++++++++++++++++ games/mysticmine/patches/patch-setup_py | 12 +- games/mysticmine/pkg/PLIST | 3 +- 4 files changed, 480 insertions(+), 6 deletions(-) create mode 100644 games/mysticmine/patches/patch-monorail_ai_py diff --git a/games/mysticmine/Makefile b/games/mysticmine/Makefile index f9f107f9e3f..42d0d14232d 100644 --- a/games/mysticmine/Makefile +++ b/games/mysticmine/Makefile @@ -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 diff --git a/games/mysticmine/patches/patch-monorail_ai_py b/games/mysticmine/patches/patch-monorail_ai_py new file mode 100644 index 00000000000..afe208e1a45 --- /dev/null +++ b/games/mysticmine/patches/patch-monorail_ai_py @@ -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 diff --git a/games/mysticmine/patches/patch-setup_py b/games/mysticmine/patches/patch-setup_py index b66e2ad8b92..8c4f27f9a66 100644 --- a/games/mysticmine/patches/patch-setup_py +++ b/games/mysticmine/patches/patch-setup_py @@ -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", diff --git a/games/mysticmine/pkg/PLIST b/games/mysticmine/pkg/PLIST index 76be132b536..41e4b745f7f 100644 --- a/games/mysticmine/pkg/PLIST +++ b/games/mysticmine/pkg/PLIST @@ -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