Home

Tuesday, December 28, 2021

Python Projects Table of Contents

 Python Projects

-Table of Contents-

Text Role Playing Game ( RPG )

_______________________________________________________________

Wallpaper-scroller

 
Codereview: Code Quality Of Windows-10 Wallpaper-scroller Script With GIF Support
_______________________________________________________________
 _______________________________________________________________

RPG Systems

 
 
 
 _______________________________________________________________

Python Project ABC

something something, something, dark side
_______________________________________________________________ 

Forum Posts: Code Review - Stackexchange 

something something, something, dark side
 
_______________________________________________________________ 

-end-

Python: Wallpaper-scroller - Entry 1 - Rough Gif Support

Python Side Project

-Wallpaper Scroller-


 I am still working on my RPG inventory system. But I've been too tired to think about it. So I was dinking around with a wallpaper scrolling script.


Here is a video of it working.

I show the gif playing function and the base function to set the wallpaper image. Gifs do not play true to duration / fps. Since the OS method I call to set the background appears to have a limit low tolerance for quick changes. A gif I tested at with 79 frames and a duration of 50 milliseconds didn't play nice with the script.


 

_______________________________________________________________

 

Brief:

Overall it is a very simple script. I used Pillow library to parse the gif frames, duration, and save them to a temporary folder so I could have a file path for windows to open. I can't say I like the current code, I might revisit it sometime.For now I'm going to refocus on the Text RPG modules.


Possible improvements:
  • support for higher frame rate gifs
  • altering stretch, tile, fit settings
  • add a tkinter tray interface


Code Snippet:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import os # To create folders and save gif frames
import ctypes # To set the windows background image
import glob # To pull file contents of a directory
from PIL import Image # To parse gif data


def import_images( direct:str, fext: [ str ] ) -> { str : [ str ] }:
    """Takes a directory and a list of files with extension to include. Returns a dictionary with the extension as a key and a array of image names."""

    """Prime the dictionary with the { key(extension) : value(empty array) } """
    img_dict = { }
    for extension in fext:
        img_dict.update( { extension : [] } )
    
    """Use glob() to pull in all files of extension at path, clean the name string and append it to appropriate key : array."""
    for extension in fext:
        for filename in glob.glob( f'{ direct }\\*.{ extension }' ):
            file = filename.replace( f"{ direct }\\", "" )
            img_dict[extension].append( file )
    return img_dict


def set_background( path:str ) -> None:
    """Takes in a absolute path and call the OS to set background image."""
    ctypes.windll.user32.SystemParametersInfoW(
            20, # System wallpaper index
            0, # Buffer
            f"{ path }", # File Absolute Path
            0 # Final Buffer
        )


def unpack_gif( directory:str, image:str, temp:str="temp_frames" ) -> None:
    """Takes in a directory path, an image name.gif, and a new temp folder name. Then parses out the gif's frames into the temp folder at directory. Once a valid path is available set that background to path."""

    image_path = os.path.join(directory, image)
    with Image.open( image_path ) as im: # Open the gif with pillow

        temp_folder = temp # Name of temporary gif frames folder
        folder_path = os.path.join(directory, temp_folder)
        try:
            os.mkdir( folder_path ) # Create the temporary folder to hold the gifs frames
        except: pass
        
        frame_duration = im.info['duration'] # Get gif run length in milliseconds
        img_fps = ( ( frame_duration / im.n_frames ) / 1000 ) # Get the time per frame in milliseconds and convert it to seconds.

        for frame in range(im.n_frames): # Get the number of frames in the gif
            im.seek( frame ) # The current frame in for loop 0 - end_frame
            name = f'temp_gif_{frame}.png' # Create a name for the frame image
            path = os.path.join(folder_path, name)
            
            im.save( path, 'GIF' ) # Save the frame image to the temporary folder
            set_background( path ) # Now we have a path to set the background, call the set function
            time.sleep( img_fps * 10 ) # slow down to not congest the wallpaper assignment

credits and resources:

Code formatted via: <!-- http://hilite.me/ --!><!-- Python, monokai --!>
 
<!-- http://www.craftyfella.com/2010/01/syntax-highlighting-with-blogger-engine.html --!>
<!-- https://stackoverflow.com/questions/679189/formatting-code-snippets-for-blogging-on-blogger --!>
<!--  --!>
<!-- http://www.craftyfella.com/2010/01/syntax-highlighting-with-blogger-engine.html --!>
<!-- https://gogopdf.com/blog/Alex-Gorbatchev-And-gogopdf-Turn-Raw-Javascript-To-A-PDF-Service --!>

-END-

Friday, December 24, 2021

Python: Text RPG - Entry 2.1 - Inventory

Python Text RPG

-Inventory-

 

In today's sprint I was changing gears to let some ideas ruminate about the Maps and organization problem. So I started tackling items and an inventory system. You can see the use example at the bottom of the article.

Sprint method: For my sprints I set a block of time to work and when it expires that's what I have for the day.

_______________________________________________________________

 

class Item():

    # class variable:
    gentype = "consumable"
    subtype = "potion"
    variant_type = "elixir"
    name_ = "elixir of healing"
    desc = "restores () health over () seconds"
    value = 5 #recomended value; value assigned per shop
    stack_size = 1
    quantity = 1
    weight_class = 0#, 1, 2, 3, 4 # none, light, medium, heavy, very heavy
    visable = True

    def __init__( self, name:str, qty:int, stack:int=1, **keywargs ):
        # instance variable:
        # Info
        self.gentype = "general category".lower()
        self.subtype = "local category".lower()
        self.variant_type = "specific category".lower()
        self.name_ = str( name ).lower()
        self.description = "does things".lower()
        # Inventory
        self.stack_size = stack
        self.quantity = qty if qty <= stack else stack
        self.weight_class = 0#, 1, 2, 3, 4 # none, light, medium, heavy, very heavy
        # Shop
        self.value = 5 #recomended value; value assigned per shop
        self.visable = True
    

    def __str__( self ):
        return f"{ self.name().title() }: { self.qty()[0] }/{ self.stack() }"

    def __repr__( self ):
        return f"{ self.name() }: { self.qty()[0] }/{ self.stack() }"
    
    # Public facing calls to set and get object string variables.
    def gen_type( self, new_gentype:str=None ) -> str:
        return self._set_string( "gentype", new_gentype )
    def sub_type( self, new_subtype:str=None ) -> str:
        return self._set_string( "subtype", new_subtype )
    def type( self, new_type:str=None ) -> str:
        return self._set_string( "variant_type", new_type )
    def name( self, new_name:str=None ) -> str:
        return self._set_string( "name_", new_name )
    def desc( self, new_desc:str=None ) -> str:
        return self._set_string( "description", new_desc )
    
    # The utility function for setting string variables
    def _set_string( self, attr, string ):
        if string:
            setattr(self, attr, str( string ).lower() )
        return getattr( self, attr )
    

    # Public facing calls to set and get object boolean variables.
    def show( self, new_state=None ):
        return self._toggle_bool( "visable", new_state )
    
    # The utility function for setting boolean variables
    def _toggle_bool( self, attr, state ):
        if None != state:
            setattr( self, attr, state )
        return getattr( self, attr )
    

    # Public facing calls to set and get object numeric variables.
    def price( self, new_price:int=None ) -> int:
        return self._set_digit( "value", new_price )
    def weight( self, new_weight:int=None ) -> int:
        return self._set_digit( "weight_class", new_weight )
    def stack( self, new_stack:int=None ) -> int:
        return self._set_digit( "stack_size", new_stack )
    def qty( self, new_qty:int=None ) -> ( int, int ):
        remainder = 0
        if not new_qty: pass
        elif 1 < ( new_qty / self.stack() ):
            remainder = new_qty - self.stack()
            new_qty = self.stack()
        return self._set_digit( "quantity", new_qty ), remainder
    
    # The utility function for setting numeric variables
    def _set_digit( self, attr:str, number:int ) -> int:
        if str( number ).isdigit():
            setattr( self, attr, number )
        return getattr( self, attr )

    # Get the current capacity for the item stack
    def capacity( self ) -> int:
        out = self.stack_size - self.quantity
        return out if out <= self.stack_size else 0

_______________________________________________________________


class Container():

    items = []
    size = 3
    # width x height ala diablo

    def __init__( self, size:int ):
        self.items = []
        self.size = size

    def item( self, item:Item=None ) -> Item or [ Item ]:
        """Takes in an item; If successful returns added item. If failed returns held items."""
        out = self.items
        if item:
            self._add_item( item )
            out = item
        return out

    def remove_item( self, item:Item ) -> bool:
        out = False
        if 1 not in self._validate_insert( item ):
            self.items.pop( self.items.index( item ) )
            out = True
        return out

    def _full( self ) -> int:
        return self.size - len( self.items )

    def _contains_obj( self, item:Item ) -> bool:
        return item in self.items
    
    def _contains_name( self, item:Item ) -> bool:
        return item.name() in ''.join( str( self.items ) )
    
    def _validate_insert( self, item:Item ) -> { int, str }:
        """returns a dictionary of { outcome_code : note, } "0, and -4 to -6" are invalid insertions. -2 to -3 denote the insertion is a duplicate. Return value of 1 indicates item is not present."""
        out = { }
        if not item:
            out.update( { 0 : "empty argument" } )
        if Item is not type(item):
            out.update( { -6 : "not an item" } )
        if 0 >= self._full():
            out.update( { -5 : "inventory full" } )
        if 0 >= item.stack():
            out.update( { -4 : "empty item stack" } )
        if self._contains_obj( item ):
            out.update( { -3 : "item duplicate" } )
        if self._contains_name( item ):
            out.update( { -2 : "name duplicate" } )
        else:
            out.update( { 1 : 'valid insert' } )
        return out

    def _add_item( self, item:Item ) -> bool:
        out = False
        if 1 == list( self._validate_insert( item ) )[0]:
            self.items.append( item )
            out = True
        return out

    def _item_to_index( self, item:Item ) -> [ int ] or None:
        out = None
        catch = self._validate_insert( item )
        if -3 in catch:
            # Index of objects matches
            out = self.items.index( i )
        if -2 in catch:
            # Index of Items with name matches
            out = [self.items.index( i ) for i in self.items if item.name() == i.name() ]
        return out
    
    def _index_to_item( self, index:int) -> Item:
        return self.items[ index ]

    def _qty( self, item:Item, new_qty:int=None ) -> Item:   
        out = item
        catch = self._validate_insert( item )
        if -2 in catch:
            # Case: item name exists in list
            index = self._item_to_index( item )[0]
            held_item = self._index_to_item(index)

            # add the current qty to the new qty and return the remainder
            updated_stock = held_item.qty( held_item.qty()[0] + item.qty()[0] )[1]
            catch = item.qty( updated_stock )
            out = item
        return out

_______________________________________________________________

 

# USE EXAMPLE:

# Change the bag size to exclude items below
inv = Container( 6 )
# Try it yourself: Item( name, qty, stack_size )
# 'Pickup' various items until inventory is full

inv.item( Item( "health potion", 1 ) ) # 1
inv.item( Item( "raw steak flank", 1 ) ) # 2
inv.item( Item( "rusty sword", 1 ) ) # 3
inv.item( Item( "mirror shield", 1 ) ) # 4
inv.item( Item( "iron greaves", 1 ) ) # 5
ps = Item( "platinum sword", 1, 4 ) #6
inv.item( ps )
#---- Inventory at capacity
inv.item( Item( "obsidian helm", 1 ) ) #7
inv.item( Item( "golden armor", 1 ) ) #8
print( inv.item( ) )
print()

# Use Scenario: top off an item you already have from one in a locker
world_item = Item( "platinum sword", 4, 4 ) # Item in the locker
# world_item = inv._qty( world_item ) # finding similar item in inventory and stacking to full

print( inv._qty( world_item ) ) # show new qty for locker item
print( inv.item( ) )

 

-END-

Thursday, December 23, 2021

Python: Text RPG - Entry 1.1 - Map Movement Trial

Python Text RPG

-Map Movement- 

 

Was trying out a different solution for controlling the connections and organization of locations in the game. I got some ideas from this trial. But! The solution requires really heavy lifting and is very verbose. Neither of which I'm fond of dealing with.

I'm going for clean and easy to implement.

 

Things that worked:

I did appreciate having more explicit function calls, but also I'm not a fan of having a bunch of them because I'm lazy. But in the long run having a catch all function like 'children' could be bad idea for separation of responsibilities. I also liked the look and readability of the type hinting in the function signature.

I like the idea that the game manager knows about all the locations in the game and they are not nested in multiple layers of location object. But that also presents a name space difficulty. I can't have two northshire abbey's. One in Northshire, and one in Westfall. It also makes pretenses a data grouping difficulty. How does the game_manager know that 'floors_0-4' belong to the lions pride inn in goldshire and not some other structure or map? if I get into explicit naming like expansion_lordaron_goldshire_lions_pride_Inn_floor_1, then it is a pain if I change or move something.


Things that didn't work:

Too explicite for connections I want to take groupings of data for granted.

I didn't think through the granularity and relationships between. They currently only work at the 'zone' category level. Anything 'below' that and the child relationships are broken. A fixable logic error but this day sprint is out of time.


Things I would do differently:

define everything important all at once Location(zone type, name, visible, and lock)

I like the simplicity of everything be a general area 'zone' or a specific facility or place 'site' and nothing else. Elwynn Forest is a zone, goldshire is a site. Deadmine in Westfall would also be a site

 _______________________________________________________________

 

 # Stub
class Container(): pass

# Stub
class Grid(): pass

_______________________________________________________________

 
class Location(): pass # Ugly code for Location parameter signature
class Location():

    # Initialize class variables
    name_ = ""
    notes = ""
    locked = True
    # sublocations = [] # TBD: Should the Game_Manager own this data or the parent Location?
    play_grid = None
    contents = None
    options = []
    intro = ""

    def __init__( self, name, locked, notes="", intro="", options=None, play_grid:Grid=None, contents:Container=None ) -> None:
        # Initialize object's variables
        self.name_ = name
        self.notes = notes
        self.locked = locked
        # self.sublocations = sublocations if sublocations else [] # TBD
        self.play_grid = play_grid
        self.contents = contents
        self.options = options
        self.intro = intro


    def __str__( self ):
        return f"{self.name_}"


    def __repr__( self ):
        # TODO: make a list comprehension that spits out a formatted string of all object variables.
        return f"<{self.name_}: {self.locked}, {self.play_grid}, {self.contents} >"


    def set_intro( self, msg ):
        self.intro = str( msg )


    def fetch_intro( self ):
        return self.intro


    def set_options( self, *options ):
        self.options = [ str( o ) for o in options ]


    def fetch_options( self ):
        return self.options

_______________________________________________________________

 
 

class Game_Manger():

    previous_location = None
    current_location = None
    locations = []
    travel_paths = []
    available = [] # optimization? or iterate through locations.locked.
    location_type = { "zone" : [], "subzone" : [], "site" : [], "section" : [] }
    # TBD this is a heavy lifting solution

    # Create Locations
    def register_location( self, location:Location ) -> Location:
        """Appends the provided location to the games locations and create a travel_paths array"""
        if location not in self.locations and location.name_ not in ''.join( str( self.locations ) ):
            self.locations.append( location )
            self.travel_paths.append( [] )
            self.update_travel_locations()
        return location


    def register_location_to_category( self, location:Location, category:str ) -> None:
        """Appends the provided location to the specified category key for indexing"""
        # Should be more explicit? add_zone, add_subzone?
        if str(category).lower() in self.location_type:
            if location not in self.location_type[ category ] or location.name_ not in ''.join( str( self.location_type[ category ] ) ):
                self.location_type[category].append( location )


    # Create Connections between Locations
    def register_path_to_locations( self, parent:Location, *children:Location ) -> [ Location ]:
        """Returns a list of children added to the parents travel_paths list"""
        catch = []
        index = self.locations.index( parent )
        for child in children:
            if child not in self.travel_paths[ index ] or child.name_ not in ''.join( str( self.travel_paths[ index ] ) ):
                self.travel_paths[index].append( child ) # Todo: Store index numbers rather than object reference?
                catch.append( child )
        return catch


    # Enable travel to a location
    def unlock_location( self, location:Location ) -> bool:
        """Unlock the provided location and add it to the available"""
        self.location.locked = False
        self.available.append( location )
        return location.locked
    

    def lock_location( self, location:Location ) -> bool:
        """Lock the provided location and remove it from the available"""
        self.location.locked = True
        self.available.remove( self.available.index( location ) )
        return location.locked
    
    def fetch_category( self, locale:Location ) -> str:
        for category in self.location_type.keys():
            if locale in self.location_type[category]:
                return category

    def fetch_index_to_location( self, index:int ) -> Location:
        return self.locations[int(index)]


    def fetch_location_to_index( self, locale ) -> int:
        return self.locations.index(locale)


    def update_travel_locations( self ) -> None:
        self.available = [ (idx, locale) for idx, locale in enumerate(self.locations) if locale.locked == False ]


    def travel_locations( self ) -> [ Location ]:
        """Get list of unlocked locations"""
        return self.available


    def travel_neighbors( self, locale:Location ) -> [ Location ]:
        return [ p for p in self.travel_paths[ self.fetch_location_to_index( locale ) ] if p in self.location_type[ self.fetch_category( locale ) ] ]
        

    def travel_children( self, locale:Location ) -> [ Location ]:
        return [ p for p in self.travel_paths[ self.fetch_location_to_index( locale ) ] if p in self.location_type[ 'subzone' ] or p in self.location_type[ 'site' ] or p in self.location_type[ 'section' ] ]


    def fetch_current_location( self ) -> Location:
        return self.current_location


    def travel_to_location( self, new_location:Location ) -> Location:
        """Travel from current location to the provided one"""
        self.previous_location = self.current_location
        self.current_location = new_location
        return self.current_location


    def travel_return_to_previous( self ) -> Location:
        """Travel from the current location to the previous one"""
        self.current_location = self.previous_location
        return self.current_location


_______________________________________________________________
 
 

Use Example:

gm = Game_Manger()

zo1 = gm.register_location( Location( "Stormwind", False, "Human capital" ) )
gm.register_location_to_category( zo1, "zone" )

zo2 = gm.register_location( Location( "Westfall", False, "low level grassland farms" ) )
gm.register_location_to_category( zo2, "zone" )

zo3 = gm.register_location( Location( "Duskwood", False, "mid level dangerous woods" ) )
gm.register_location_to_category( zo3, "zone" )

zo4 = gm.register_location( Location( "Redridge Mountains", False, "mid level mountains" ) )
gm.register_location_to_category( zo4,"zone" )

zo5 = gm.register_location( Location( "Burning Steppes", False, "high level lava flows" ) )
gm.register_location_to_category( zo5, "zone" )

zo6 = gm.register_location( Location( "Elwynn Forest", False, "Lowbie starting region" ) )
gm.register_location_to_category( zo6, "zone" )


a1 = gm.register_location( Location( "Northshire Valley", False, "Welcome and start of new game tutorial" ) )
gm.register_location_to_category( a1, "subzone" )

a2 = gm.register_location( Location( "Goldshire", False, "first lowbie town" ) )
gm.register_location_to_category( a2, "subzone" )

gm.register_path_to_locations( zo6, a1, a2 )
gm.register_path_to_locations( zo6, zo1, zo2, zo3, zo4, zo5 )
print( "CHILDREN:", zo6, gm.travel_children( zo6 ) )
print( "NEIGHBORS:", zo6, gm.travel_neighbors( zo6 ) )
print()


b2 = gm.register_location( Location( "Echo Ridge Mine", False, "The first dungeon" ) )
gm.register_location_to_category( b2, "site" )

c3 = gm.register_location( Location( "Northshire vineyards", False, "A small grape farm" ) )
gm.register_location_to_category( c3, "site" )

d4 = gm.register_location( Location( "Northshire river", False, "a river cutting through the northshire zone" ) )
gm.register_location_to_category( d4, "site" )

b = gm.register_location( Location( "Northshire Abbey", False, "The building" ) )
gm.register_location_to_category( b, "site" )

gm.register_path_to_locations(a1, a2)
gm.register_path_to_locations(a1, b, b2, c3, d4)
print( "CHILDREN:", a1, gm.travel_children(a1) )
print( "NEIGHBORS:", a1, gm.travel_neighbors(a1) )
print()

c = gm.register_location( Location( "Northshire Abbey Exit", False, "General starting spot" ) )
gm.register_location_to_category( c, "section" )
d = gm.register_location( Location( "Main Hall", False, "zone exit > trainers and quest givers" ) )
gm.register_location_to_category( d, "section" )
e = gm.register_location( Location( "Hall of Arms", False, "Get more quests" ) )
gm.register_location_to_category( e, "section" )
f = gm.register_location( Location( "Library Wing", False, "Get training" ) )
gm.register_location_to_category( f, "section" )

gm.register_path_to_locations( a1, c, d, e, f )
print( "CHILDREN:", a1, gm.travel_children(a1) )
print( "NEIGHBORS:", a1, gm.travel_neighbors(a1) )


-END-

Sunday, December 19, 2021

Python: Text RPG - Entry 1 - Map Grid and Movement

Python Text RPG

-Map Grid and Movement-

 
So to get going we need some were for the player to be. A sort of world we can display to the player and keep track of their location. We will need to lay a good bit of ground work for this. But before that Lets go a really simple way as a demo.
 
 
- I'll update / finish this post at my earliest opportunity -
 
Last update: ` 2021/12/20 - 23:47 `  
 
 
Until then I do encourage you to look at and try out the code in your own project.
 
 

SIMPLE DEMO:

_______________________________________________________________

Monday, December 13, 2021

Python: Text RPG - Intro

Python Text RPG

-An Adventure-

 

​This will be an ongoing series for a pet project. Feel free to follow along!

If you have any feedback, suggestions, or requests please leave me a message.


About The Author:

I am not new to Python or game development but I am far from an expert. So I will be spending time researching and making mistakes as I develop this project. If you are an advanced+ python user I doubt you will get much from following the progress of this project. If you are brand new or have dabbled a bit but never built anything then this may be of interest to you.


About The Project:

This will be a text based Python 3.9+ adventure game in the command console. My current approach is to start with a brute force monolith. Meaning a linear all in one file mess of code. Once the base is ready and all the features are established and implemented. I'll start breaking the parts out into their own classes and get more into an object oriented approach.

If this sounds interesting please continue on to part 1!


Documentation Conventions:

At the top of each page I'll have a "updated" string to keep track of things I've touched. As well as a "status" to denote if I plan to work on it any time soon.

Example:

Updated: 2021/12/11 : 01:03
Status: Work In Progress


At the Top of each page will be a forward and back button that will simply navigate you to the next or previous section of the course.

All headers will end with a colon(:), Each Section: will be bold-underlined, Main Groups: will be bold, and sub-headings: will be underline.

I expect the documentation to be a little haphazard as I figure out how I'm going to organize. I do apologize for the mess!


-END-