Front Office Football Central

Front Office Football Central (https://forums.operationsports.com/fofc//index.php)
-   Dynasty Reports (https://forums.operationsports.com/fofc//forumdisplay.php?f=8)
-   -   Groundhog learns Python - Road to a Basketball Text Sim (https://forums.operationsports.com/fofc//showthread.php?t=89021)

muns 08-22-2015 07:48 AM

Ya, still looking good!

Groundhog 08-22-2015 10:12 PM

Thanks guys! :)

CraigSca 09-06-2015 06:49 PM

LOVE!

Incredible job!

Groundhog 09-08-2015 08:04 PM

Quote:

Originally Posted by CraigSca (Post 3052136)
LOVE!

Incredible job!


Thanks, appreciate it! :)

And now for a boring post that might be of interest to anyone else that tries something like this...

When I started this project a little over a year ago, I felt the hardest thing would be coming up with a way to accurately simulate a game of basketball. Looking at it now, that was probably the easiest part. After 25 or so years of playing and watching the sport, I know how basketball works. Once I found out how Python syntax 'works' and learnt how to control the flow of my game loop it was just really a matter of filling in the gaps and coming up with ways to calculate different results.

Since I started working on a GUI it's been a whole new ballpark, and I've had to dig deep into documentation and other people's code to come up with solutions to a lot of difficult problems that pop up, usually relating to concepts that are completely new to me.

A good example is threading. The isn't an entirely new concept to me because it's something I'm familiar with at a high-level through my day job, but I did not expect that, for something as seemingly simple as pausing play-by-play text between results, I would need to read through a ton of documentation and spend a good part of the last two weeks trouble shooting and re-writing large sections of code.

On the plus side, it's made me a better programmer, and it's definitely improved the structure of the sim!

SirBlurton 09-09-2015 03:15 PM

Totally agree with all the compliments, it does look amazing.

I've been struggling through the first couple of chapter of the PyQt book you referenced earlier in the thread, so I fully appreciate the amount of work it took you to get this far!

I haven't run into the threading issue yet, but my goal project is somewhat similar to yours - wondering if you could give any more detail on what resources you used to figure out what you needed to re-write?

Groundhog 09-09-2015 06:56 PM

The PyQT book is super dry, I agree, mostly due to it's age. It's good starting point, but in general I've seen people do the same things in better ways based on posts to stackoverflow, and have opted to follow those. I'm not using PyQt, I opted for PySide for licensing reasons, but they are almost identical so it hasn't been an issue.

Keep in mind that I'm far from an expert on any of this, but I'll just detail my experience for you in case it helps - I managed to solve my issues, but there are probably better ways to do it. :)

The problem I encountered was just a problem with controlling how data is passed between classes... basically, OOP. My program's structure is one .py file with the main UI logic - navigation, configuring widgets, displaying data, etc. and then many separate .py files containing classes for different aspects of the game - one for handling the saving/loading of data, one for controlling the scoreboard, generating players, the actual simulator engine, etc.

The problem you quickly encounter is blocking - I actually ran into this first working on a BBCF->FOF utility that scrapes thousands of web pages to build player profiles. Your Qt UI is one big loop and any process that blocks the loop from updating the display makes your UI unresponsive, as if it's hung. The best way around this is to take these processes outside of your UI loop and have them run in another thread. Qt has it's QThread class which is exactly for this purpose. You inherit the QThread class*, put all your blocking code in it's run() method, then kick it off via the start() method:

Code:

def run_blocking_process(self)
    athread = MyInheritedQThreadClass()
    athread.start()


This worked fine for nearly everything. The ticker at the top of the simulator screen for example - it's a 'While True:' loop that displays text, sleeps, displays text, sleeps, etc. until the sim finishes. It uses signals and slots to connect to the simulator engine and retrieve stats, then connect to the main UI class and update the widget with a signal, because you should never update a widget directly from another class, even a QThread, as it will cause blocking.

Pausing the play-by-play of the simulator class would also block the UI, so obviously I needed to convert the (huge) class to a QThread and follow this same logic. I changed the simulator class to inherit from QThread, and put all the 'sim possession' logic under the QThread's run() method, to prevent the time.sleep() function after each line of PbP from blocking the UI. This was no small task but was also a good opportunity to clean up my 8 or so months worth of simulation code (that's still ongoing). The PbP text for the GUI as well as scoreboard data etc. was all sent via signals rather than by directly updating widgets, etc.

To my frustration, this did not work. The UI still blocked and I had no idea why. I made a stripped down test class, and it did work, so what was the problem.....

The problem was that my simulator class's __init__ method was loading all the player and team information from my main program class, which was making it a child of this process, rather than a true threaded process. The simulator class also directly accessed some of the variables loaded into memory by the main class - some player/team attributes, etc. - which led me to read a little bit about the difference between 'stack' and 'heap'.

So the solution was to build the simulator class, THEN load ALL the values for teams/players/etc. into that class so it doesn't reference back to the main program class - this is the OOP part I mentioned above.

This is a simplified example of what I ended up with:

Code:

def run_game(self, home_team_id, away_team_id):
    simulator = Simulator()
    values = self.data_handler.build_simulator_values(home_team_id, away_team_id) # Load everything needed by the simulator into a dictionary.
    simulator.load_values(**values) # Unpack these values into the simulator class.
    simulator.start() # Magic happens!


Hopefully some of this makes sense... Obviously feel free to pm me if you have any Qs. I can't guarantee I'll know the answers, but it's likely you'll run into the same problems along the way that I have!

*One small note about inheriting the QThread class... If you google QThreads, it seems to be popular opinion that this is not the recommended way to do this. There's another way to use QThreads, but I found this way easier to work and it seems to work fine for me.

SirBlurton 09-10-2015 09:55 AM

Wow, thank you so much for the detailed write up - I've saved it for future reference. I was just starting to bump into some blocking issues but couldn't figure out what was happening....you've saved me a great deal of time.

I really appreciate you taking the time to give this advice and share your experience! Also wanted to say that there've been a few times where I've wondered if I should even bother continuing down the Python road because I couldn't find similar projects - your project has helped me move past that internal debate so wanted to thank you for that too!

Also appreciate the offer around pm's...will likely take you up on that - like you I've spent time on my core simulation loops and now am trying to figure out how to tie all my spaghetti together to make a cohesive program.

Groundhog 09-10-2015 06:33 PM

Quote:

Originally Posted by SirBlurton (Post 3052822)
Also wanted to say that there've been a few times where I've wondered if I should even bother continuing down the Python road because I couldn't find similar projects - your project has helped me move past that internal debate so wanted to thank you for that too!


To be honest with you, this is something I still think about. After getting a little better with Qt I knew Python could do what I wanted it do, functionally and graphically.

The biggest 'problem for another day' I have though is if I want to actually sell this simulator at some point. Compiling/obfuscating Python code is problematic, and just simply not the goal of the language.... There are options like Nuitka, Cython, and learning a little C or C++ to build extensions, but I haven't toyed with any of them as yet. I suspect this is the major reason why there aren't more projects like these out there...

The trade off of course is that I don't think there's another language where I could have gone from zero to where I'm at now in the time it's taken. If I'd tried learning C or C++ I'd probably still be trying to get my head around if/else statements. :)

SirBlurton 09-11-2015 10:12 AM

I totally hear you on the if/else statements...I've dabbled with C# for Unity and I find it takes me so much more time to accomplish the same things because I'm tripping over syntax. I just like Python so much more...

I've only done a little bit of reading about compiling but you're right that it doesn't seem to be the focus...hopefully a clearer path will emerge when you need it! I have a bookmark saved for cx_Freeze as something to look into but I haven't explored it in any depth at all.

Groundhog 09-13-2015 06:13 PM

cx_Freeze and Py2exe both work as far as freezing the python code to an .exe and including all the different modules, but they are just as easy to unfreeze. With Py2exe you can use PyInstaller to retrieve the .py files (or .pyc - I can't recall which) and the same is also true with cx_Freeze, so it works as far as packaging your program as a standalone exe, but does not protect the source code.

Groundhog 09-22-2015 08:52 PM

I've done some work with the UI around player stats, which I'll show some screens of once I'm happy with it. Some more boring stuff now...

After spending time planning how to handle the saving/loading of data - mainly related to database queries and converting the output of these queries into the format I use within Python - dictionaries - I had an idea that seems stupidly simple in hindsight... why am I even using databases? I mean, I understand the benefits of relational databases, but why spend the time converting data between SQL and my Python dictionaries when I can just serialize the data using Python's pickle module or something similar?

There are some drawbacks to going with this method, but if I maintain my own 'foreign keys' correctly, I think the pros outweigh the cons. It also makes putting together a pure-Python editor for data very, very simple.

Groundhog 10-14-2015 11:23 PM

Quote:

Originally Posted by Groundhog (Post 3053575)
cx_Freeze and Py2exe both work as far as freezing the python code to an .exe and including all the different modules, but they are just as easy to unfreeze. With Py2exe you can use PyInstaller to retrieve the .py files (or .pyc - I can't recall which) and the same is also true with cx_Freeze, so it works as far as packaging your program as a standalone exe, but does not protect the source code.


A little more on this - it sounds like Nuitka is the solution. Nuitka freezes to .exe (and some .dll files) and is not nearly as simple to decompile back to the python source. I'm not positive on how well (if at all) it compiles PySide/PyQt though.

SirBlurton 10-15-2015 09:31 AM

Thanks for posting about Nuitka, will definitely check it out!

Groundhog 11-01-2015 02:37 AM

Stats... I figured this would be easy, but it took me a good 3 weeks to come up with a good way to display the stats (all of which are just test data right now), calculate career totals, and restrict by leagues and regular/post season on the fly...

Added hover text to the league acronym to give a summary of what nations take part in the league, because I think this will come in handy when scouting guys for foreign leagues.


SirBlurton 11-02-2015 10:09 AM

This looks awesome...I love seeing this thread every time it's updated.

Understand if it's a trade secret - but would love to know how you got it to update on the fly...

Groundhog 11-02-2015 04:51 PM

Definitely not a trade secret! This will be a lot of info, hopefully it's helpful.

First I came up with a structure to store the stats. It's a dictionary structured like:

Code:

Player_ID > Year > League_ID > Team_ID > 'season'/'postseason' > Stats

The league selection combobox is filled each time a player is opened, the program loops through all the leagues stored in the player's stats similar to the below:

Code:

# Initialize a blank list.
leagues_played = []

for year in PLAYERSTATS[player_id]:
    for league in PLAYERSTATS[year]:
        leagues_played.append(league)


the 'league' variable here is the league_id value, a unique number that ties it to a key in another dictionary that stores all the leagues. I then create a 'set' structure of this list (one of those things you never think you'll need when you learn it!) of leagues to eliminate duplicates, and use it to populate the combobox options, including an 'All' option as well if the player has played in more than one league.

I have another dictionary that looks similar to this:
Code:

stats_settings = {'league': id, 'season_or_post': 'season', 'format': 'totals'}

Changing the league selections through any of the comboboxes or radiobuttons changes these settings here, and then refreshes the stats. So if the league was set to a particular league_id with 'season' stats I would query all the player's stats like:

Code:

raw_stats = []

for year in PLAYERSTATS[player_id]:
    for league in PLAYERSTATS[year]:
        if league == league_id_ive_selected:
            for team in PLAYERSTATS[player_id][year][league]:
                raw_stats.append(PLAYERSTATS[player_id][year][league]['season'])


This gives me a list of all the raw stats for a player as per the criteria set by the different selection widgets. The stats object is a list that contains the dictionary of season stats, as well as the other info needed for the row of stats that looks like [year, league_id, team_id, {'G': 82, 'GS': 82, 'FGA': 2, etc.}].

The next step is what I call 'pretty stats', which is to format the stats correctly - totals, averages, per36, advanced (not done yet). Each stat object is passed to the 'pretty stats' function to generate a list that is formatted for display. So for example if 'averages' has been selected, the pretty stats function will return something like:

Code:

[year, league_id, team_id, 82, 82, 1.5, 3.2, .401, etc.]

This is appended to a new list, and is ready to add to my datamodel for display via the QTableView widget. Last step is to come up with the totals for the selected league. This was the hardest part. A copy was made of the original raw_stats list (before any other changes were made), and I'm going to post my solution for this in full, changing a few function/variable names so it makes more sense.

I'm not necessarily happy with this solution, but it works. It's a bit messy and I'm sure there's a more efficient way to do it.
Code:

    def stats_career_totals(self, raw_stats, format_):
        # Take all the stats in the raw_stats object, and determine the career average or total based on format_
        # attribute. Exclude the attributes we don't want to calculate first.
        # TODO: Not happy with this method at all. Find a better way to do it.
        calc_fields = []
        return_list = []

        for i in raw_stats:
            i = i[4] # i[4] is the dictionary of stats - i[0]-i[3] is the info columns for each row (year/league/etc)
 
            # x variable is a 'throw-away' list that makes up all the elements to be added together to determine the
            # total.
            x = []
 
            # A list of the different categories - G/GS/FGM/etc. This is used to make sure the stats are ordered in the correct order - and ordered list -
            # so I can use the index of the list to determine which stat is which.
            for cat in self.STAT_CATEGORIES:
                if cat not in ['FG%', 'FT%', '3P%']:
                    x.append(i[cat])
 
            calc_fields.append(x)
 
        # Add all individual elements in the lists together to get the total.
        # This 'magic' line of code was the part that took me (with help from Google) a long time to come up with.
        TOTALS = [sum(i) for i in zip(*calc_fields)]
 
        # Iterate through the TOTALS, using the index to identify the FG%/3P%/FT% fields.
        for index, stat in enumerate(TOTALS):
 
            if index in [0, 1, 5, 7, 9]:
                # GP and GS
                if index in [0, 1]:
                    return_list.append(stat)
                # FG%
                elif index == 5:
                    return_list.append(self.player_profile_stats_format_shot_percentage(TOTALS[3], TOTALS[4]))
                # 3P%
                elif index == 7:
                    return_list.append(self.player_profile_stats_format_shot_percentage(TOTALS[5], TOTALS[6]))
                # FTs
                elif index == 9:
                    return_list.append(self.player_profile_stats_format_shot_percentage(TOTALS[7], TOTALS[8]))
 
            # Don't want to calculate anything for the 'G' or 'GS' stats - just use the raw figures.
            if index not in [0, 1]:
                if format_ == 'average':
                    # Fetch the average of all games played / tallied stats.
                    return_list.append(self.player_profile_stats_format_average(TOTALS[0], stat)) # TOTALS[0] = 'G' stat.
                elif format_ == 'per36':
                    # Fetch the per36 average of the tallied stats.
                    return_list.append(self.player_profile_stats_format_per36(stat, TOTALS[2])) # TOTALS[2] = 'MINS' stat.
                elif format_ == 'totals':
                    # Just return the totals.
                    return_list.append(stat) # Append the raw stat.
 
        return return_list


The total row is appended to the other 'pretty stats' and ready for display. These pretty stats and a list containing the headers are then built into a QAbstractTableModel, which is then assigned to the QTableView widget to be displayed. I make this part sound trivial, but the 'model view' style of programming with qtableviews and item models is in itself a few weeks of work to get a semi-decent grasp on... Definitely worth the time though, this guy's series of tutorials was invaluable:

PyQt4 Model View Tutorial Part 01_0 - YouTube

SirBlurton 11-03-2015 02:16 PM

That's awesome - thanks for sharing!
Love the 'magic line'...list comprehensions were definitely an "ah-ha" moment for me.

Did you end up going with pickle for storing your stats?

I've been playing with JSON for my project.

I started by using .csv files to read and write my data but it was taking a full minute in some cases (which could have been due to extremely poor implementation on my part). I decided to try JSON, thinking it wouldn't make much difference, but it went from a minute to a second to complete the operation without changing anything else about the approach.

Thanks for the model view link! And for being so open about your process in general, learning a lot! Always been shy about posting but considering posting some status updates of my project as a way to keep myself honest.

Radii 11-03-2015 04:07 PM

Couple thoughts if you are interested (and if you are not, let me know, what you're doing and sharing here is awesome, I don't want to potentially bog you down over anything that you're satisifed with)

If you are using the raw_stats dictionary in a lot of places othern than this one display screen, did you consider creating a class for it instead of using the string dictionary? Being able to refer to raw_stats.GP instead of remebering that games played is in index[0] could make the data a lot esier to work with.

Alternatively, if the dictionary is the best way to go, what about an enumeration so that you don't have to refer to the index numbers in hard coded fashion every time you touch the stats. Something that sets Stats.GP = 0, Stats.GS = 1, Stats.MINS = 2, etc. If you decide you want to change the layout of things down the road, or change the order, or alter the data based on the addition of some new advanced stats calculation, then you only have to change it in one place instead of many.

I should point out I don't use Python, just generic programming thoughts. May or may not be relevant to the exact way you're doing things :D


Keep it up, this is an awesome read and its great to see the progress you've made.

Groundhog 11-03-2015 04:23 PM

Quote:

Originally Posted by SirBlurton (Post 3063293)
That's awesome - thanks for sharing!
Love the 'magic line'...list comprehensions were definitely an "ah-ha" moment for me.

Did you end up going with pickle for storing your stats?

I've been playing with JSON for my project.

I started by using .csv files to read and write my data but it was taking a full minute in some cases (which could have been due to extremely poor implementation on my part). I decided to try JSON, thinking it wouldn't make much difference, but it went from a minute to a second to complete the operation without changing anything else about the approach.

Thanks for the model view link! And for being so open about your process in general, learning a lot! Always been shy about posting but considering posting some status updates of my project as a way to keep myself honest.


Please do post, I would definitely be interested in reading your progress too!

I'm using pickle now, but I'm not sure how well it's going to scale up. A pickled object of historical stats for example could end up being quite a big object and take awhile to load when needed, so I've made it easy to change this down the line if I feel I need to go a different way down the line.

I use JSON for storing all my 'settings' data. Originally I used it for everything but having to convert the type of loaded JSON from strings to other types was a bit of a pain... There are ways to handle this, but for now I've opted for pickle. I see the advantages to SQL databases and did spend a bit of time reading up on them - managing my own 'foreign keys' to link dictionaries together in particular feels like it might get out of hand pretty easily.

Groundhog 11-03-2015 05:21 PM

Quote:

Originally Posted by Radii (Post 3063324)
Couple thoughts if you are interested (and if you are not, let me know, what you're doing and sharing here is awesome, I don't want to potentially bog you down over anything that you're satisifed with)

If you are using the raw_stats dictionary in a lot of places othern than this one display screen, did you consider creating a class for it instead of using the string dictionary? Being able to refer to raw_stats.GP instead of remebering that games played is in index[0] could make the data a lot esier to work with.

Alternatively, if the dictionary is the best way to go, what about an enumeration so that you don't have to refer to the index numbers in hard coded fashion every time you touch the stats. Something that sets Stats.GP = 0, Stats.GS = 1, Stats.MINS = 2, etc. If you decide you want to change the layout of things down the road, or change the order, or alter the data based on the addition of some new advanced stats calculation, then you only have to change it in one place instead of many.

I should point out I don't use Python, just generic programming thoughts. May or may not be relevant to the exact way you're doing things :D


Keep it up, this is an awesome read and its great to see the progress you've made.


Appreciate the tip, and I agree that that's a really good idea!

My current coding process is:

1) Try to think how something could work.
2) type away madly.
3) test through trial-and-error until I get it to work.
4) Completely rewrite/refine it from scratch to match the structure of everything else.

Right now I'm on step #4 with stats, and I'm going to do exactly what you propose. :)

Radii 11-03-2015 07:54 PM

Quote:

Originally Posted by Groundhog (Post 3063338)
1) Try to think how something could work.
2) type away madly.
3) test through trial-and-error until I get it to work.
4) Completely rewrite/refine it from scratch to match the structure of everything else.


Yup! The more experience you get doing steps 2-4, the better you'll get at step 1. I am not a great designer, its not uncommon for me to start planning out class structures and to reach a point where I get stuck and can't visualize everything as well as I wish I could and just start writing something. You could easily go look at a project I finished and pick out the parts that I slammed together vs the parts that were well thought out. Always something to learn I suppose :D

Groundhog 11-04-2015 09:03 PM

I ended up creating two new classes, one specific for working with the individual rawstats - it takes the dictionary entry of a specific season's stats in the dictionary format that it's stored, and returns it in a format ready for display - and a class that takes a player's whole statistical history and will return it (using the rawstats class per entry) depending on the restriction applied - all stats for all leagues, all stats for one league, all stats for one league in one year, all post-season stats for one league etc. as well as generate the 'career totals' row on the bottom.

The end result is much the same as what I had before these two classes, but the amount of code needed was much, much less and is much cleaner. Thanks Radii. :)

Radii 11-05-2015 01:56 AM

Quote:

Originally Posted by Groundhog (Post 3063567)
The end result is much the same as what I had before these two classes, but the amount of code needed was much, much less and is much cleaner.


Awesome stuff man! Can't wait to see what's next!

Young Drachma 11-05-2015 02:25 PM

Glad you're still trucking!

Groundhog 11-08-2015 06:04 PM

I put some work into cleaning up the code around styling the Qt forms, as this was some of the first coding I did and it lacks consistency, especially the first forms I put together. To be less polite, it works, but it's a real mess. :)

Qt Designer is one of the best reasons to use the Qt framework, but I really only use it to lay out my forms, and do all the actual styling from within Python. I chose to go this route (you could do it through Qt - you can set StyleSheets from within the Designer and see the results instantly), but if you want to be flexible and allow custom colour schemes you will be using the .setStyleSheet method for your widgets, and using the string formatted stylesheets, which are similar to .css in html, with variables.

As an example, a simple QPushButton widget's style sheet will look like:

Code:

standard_button = '''QPushButton {{
                                      color: rgb{};
                                      background-color: rgb{};
                                      border-bottom-left-radius: {}px;
                                      border-bottom-right-radius: {}px;
                                      border-top-left-radius: {}px;
                                      border-top-right-radius: {}px; }}
                    QPushButton:hover {{
                                            background-color: rgb{};
                                      }}
                    QPushButton:disabled {{
                                                background-color: rgb{};
                                                color: rgb{};
                                          }}'''


One of the downsides to Qt is that changing the original style is not enough to then go on and update all the widgets with it assigned - it's obviously just a stored string, so no signals are emitted to let all the widgets know to reapply it. I figured there would be a quick and easy way to do this, but after some google searches it seems not. To allow a user to change the colour of a push button, you will need to update the above string with the new rgb values, and then re-apply the stylesheet to all the widgets.

To handle this, I created a new class. I won't post the whole thing as it's huge, but this is a quick snippet of how it is structured for the settings screen:

Code:

class Style:

    def __init__(self, app):
        # The 'app' variable is my application itself, which gives the Style class access to directly modify all the widgets.
        # So when I create an instance of this class from within my application, I create it as self.style = Style(self).
        self.app = app
        # Class attributes - this will store all the stylesheets strings.
        self.attrs = {'css': {}}
        self.store_ss_strings()
        self.setup_shortcuts()
        self.init_all_widgets()

    def setup_shortcuts(self):
        # The quick command to retrieve the stylesheet and current variables stored in settings.
        self.mainwindow_panel = self.attrs['css']['mainwindow_panel'].format(self.retrieve_styling('mainwindow_panel'))

    def store_ss_strings(self):
        # Define all the stylesheets strings and store them in the self.attrs dictionary.
        self.attrs['css']['mainwindow_panel'] = mainwindow_panel = '''QLabel {{
                                                                                color: rgb{};
                                                                                background-color: rgba{};
                                                                                border-bottom-left-radius: {}px;
                                                                                border-bottom-right-radius: {}px;
                                                                                border-top-left-radius: {}px;
                                                                                border-top-right-radius: {}px;
                                                                          }}'''
     
    def init_all_widgets(self):
        # This method is called upon creation of the class instance and applies all stylesheets to the different widgets,
        # via the individual method per Qt form. It uses the colours and other settings related to styling in the main
        # application's 'settings' class, so if changes are made to the colour scheme, this method is run again to re-style
        # everything.
        self.init_settings_form()

    def retrieve_styling(pointer):
        # Fetch the colours from settings class.
        if pointer == 'mainwindow_panel':
            r, g, b, a = self.app.settings.panel_background_color[0], self.app.settings.panel_background_color[1], \
                        self.app.settings.panel_background_color[2], self.app.settings.panel_transparency[0]

        # This dictionary supplies all the different settings needed by the stylesheet string.
        lookups = {'mainwindow_panel': (self.app.settings.panel_text_color, (r, g, b, a), self.app.settings.corner_radius,
                                      self.app.settings.corner_radius, self.app.settings.corner_radius,
                                      self.app.settings.corner_radius)}

        return lookups[pointer]

    def init_settings_form(self):
        # Apply the style sheets to all widgets.
        self.app.ui.settingsPanel.setStyleSheet(self.mainwindow_panel)


Groundhog 11-28-2015 08:30 PM



Initial work on the nation screens. I want to make it real easy to add/remove nations, so it took me some time to work out how to best generate menus and screens 'on the fly' based on nation information stored in a csv file. The sim will read the file, populate the menus, and colour the scheme based on the foreground/background colours defined for the nation.

Leagues work in a similar way, with the menu system grouping by Region > Nation > League(s).

SirBlurton 11-30-2015 11:56 AM

Sooooooooo pretty....Guessing it's too early to tell you to shut up and take my money?

LastWhiteSoxFanStanding 12-01-2015 05:26 AM

Looking great. Hope you can include Singapore too :D

Groundhog 12-05-2015 02:48 AM



Ask and you shall receive. :D

...although still need to put the first names/last names and city files together.

LastWhiteSoxFanStanding 12-05-2015 05:03 AM

Talk about great customer service! :)

Can't wait to see the next update.

Groundhog 01-21-2016 09:49 PM

No updates in a while, but it hasn’t been for lack of progress (relatively speaking). This post will more be for those of you who are interested in the programming side of things… ie. lots of text, no pics. ;)

After 100,000+ lines of code, much more if you count deleted/restarted lines of code, it was time to take a step back and look at the structure of my program. Things were getting a bit messy and, it seemed to me, overly complicated. I had been doing some reading lately on software design patterns (half because of this project, half because I think it’s something I might be interested in doing for money one day) and while reading through the different patterns, I was interested to see that without really knowing it I had drifted towards what is called the Model-View-Controller pattern. I won’t add to many details about what an MVC pattern is (there’s lots of info on the web), other than to say that it became pretty clear to me why things were getting messy, and what I needed to do to fix it. It requires re-writing a bit of code, but not excessively so.

The other major thing I’d been looking at was the interface. As monitor resolutions grow, the fixed-window style of GDS and Wolverine Studios sims began to bug me. Early on I realised why they opt to go this way, and why it's not as straight forward to make a program that will 'stretch' to fit your selected resolution, like with any graphical video game you play.

I spent some time the last couple of weeks thinking of ways around it - Qt (the UI framework I'm using) has layouts which are made for exactly the purpose of resizing components to fit a screen. The problem is they don't really suit a program like this IMO, and objects on the screen stretch and shrink and look ugly. For general purpose desktop apps I think it works great, but not for this. So I've decided to stick with what I've got so far, which is a a fixed-window, though in the standard 'widescreen' size of 1280x720 pixels. I'm going to push on with this with the idea that if I want to improve on this later, all of my presentation logic can be replaced without touching anything else (this relates directly back to the MVC stuff above).

As far as the actual simulator goes, I haven't done a great deal of work for a while because I know how it's going to work. There's no problem to solve (unlike quite a few other areas), so I haven't been focusing on it as much. I have been looking at schedules, trying to make it as flexible as I can. Initially there will be two types of schedules - round-robin, and your NBA-styled 82 game schedules. I have a good idea of how to generate both, just ironing out a few wrinkles right now.

CraigSca 01-22-2016 10:40 PM

Always good to hear updates, Groundhog - been missing these!

Groundhog 01-27-2016 01:32 AM

Until this point I've imported all of my data - teams, coaches, leagues, nations, etc. - in a database (sqlite) that I manually populated from csvs full of info I've gathered over the last couple of years, so I spent some time the past couple of days putting together the 'new game' process - main menu/league selections, etc.

Here's a couple of screens:

Title Screen
A quick (5 minute) title screen just to give the standard new/continue/load options.


New Game Menu
This one took a lot longer - the fully functional league selection screen. All the data here is taken from csv files that can be easily modified to add/subtract/modify the leagues included - currently 44 fields per league (some repetitive, like 7 fields for each day of the week that games will be scheduled). Leagues where I've manually created teams (also done via csvs) default to 'import' teams, other leagues will be randomly generated. Active leagues will be bolded (not yet done), and I'll ditch the '#' in the Teams column header.

The Olympics/Tournaments options on the left will be changed to a table like the regional leagues, because it gives me more flexibility with how many I can include without running out of room - there's international tournaments in the Americas, Asia and Africa that I don't plan on looking at now but would probably like to look at down the track:



Nav Bar
Lastly, the top-of-screen navigation bar - not completely functional yet, because I don't have all the screens completed. This image is at 100% actual width, to give you an idea of how wide the UI will be:

SirBlurton 02-01-2016 04:29 PM

Super, super awesome....if you get to the point you want any testing help, I'd love to volunteer! No worries if not though.

Really impressed with your GUI skills, I just got to the point of making my first (very remedial) GUI for one of my sims the other day. Your posts are a reminder that I still have a long way to go!

Groundhog 02-01-2016 11:51 PM

Thanks for the kind words! When I get to the point of testing, this would be the first place I would come. That point is still some ways away unfortunately... inching closer, but lots of work that needs to take place before that point. No idea of timeframes.

UI design is as foreign to me as coding was at the beginning. I just took a look at some of the programs I find I really like, tried to figure out what it was about them that I liked, and then try to figure out how replicate it, or tweak it to fit what I'm doing. Although I didn't go with a web-based program, Flask and the Bootstrap css stuff was a big influence on what I've tried to put together so far, too.

Groundhog 02-21-2016 07:51 PM

I'm revisiting the whole player generation part of the sim now, as the rest of the logic is now all in place to create a new universe.

When I started to learn Python one of the first scripts I came up with was a random name generator, to add more flavour to the foreign recruits in my FBCB game. When putting together the player generation code it was also one of the first things I coded. I set out to make it smarter over the weekend, and allow for different settings for each nation - things like hyphenated names, no surnames, middle names, 'I, II, III, IV, Jnr' names, etc. Although names are purely cosmetic, in the world of text sims I personally find little details like this are important because it gives a bit more uniqueness to individual players which for me adds another important layer of non-technical abstraction between the user and the random number generator that's at the core of all simulators.

Each nation has 2 or 3 name files built mostly by text scraping national soccer league team pages on wiki where possible (and a much more manual process where not). In addition there is a name rules csv file in the same folder that determines % of players with no surnames (Portuguese styled sportsmen - Nene, etc.), % of players with ', II' appended at the end or hyphenated names etc., most of which I determined by data scraping every Div I college basketballer and coming up with %s. You can also specify substitute nations and a % chance of the name being chosen from there. For example, I didn't bother scraping Canada names, instead assigning USA as 100% to be chosen instead. For the USA itself there's a slight chance of a 'Puerto Rican' name, etc.

When I run the name generator now, I specify a nation, and it will produce a name, taking into account the different rules, and spit out a first name and a last name.

Player hometowns work in a similar fashion, except are weighted selections, with more populated cities in each nation a better chance of being selected. USA is slightly different as I included state distribution as well. Again using Div 1 basketball rosters I calculated the % of players from different states, and USA hometowns will select the state first, and then a weighted selection of cities from that state.

I also put together a list of US colleges, breaking them down into 5 levels of talent from top tier schools down to division 2. When a US player is generated, there is a greater chance that higher ability players will have attended higher tier schools.

I'm trying to think of a good way to handle non-US players with college experience. Some nations (like Australia) send a lot of kids to US colleges, and the pro league is largely national players who attended US colleges. This is not the norm internationally though, as most European countries in particular have stronger youth systems and there is not as much drive to play in the US college system when they can earn decent money as teens.

Young Drachma 02-27-2016 07:33 PM

This is looking great!

Groundhog 03-23-2016 05:52 PM

I mentioned that I was trying to make the influx of new talent each year as realistic as possible, and I've spent a couple of nights this week designing a system called 'Youth Settings'. These are a series of rules that determine the age ranges and probability of each age for incoming rookies, as well as the chance of a rookie having attended a USA college. If a generated rookie is determined to have attended college, a calculation is made based on his potential ('scouted' as opposed to 'actual') of how many years he attended college (weighted towards higher potential means less years), which will be used to determine his age instead of the range of ages mentioned earlier - so a higher rated player from the US is more likely to have spent 1 or 2 years at college and enter the league at a younger age than, say, a 3-star type player, who might be 22 or 23 as a rookie after graduation. As mentioned above, the quality of the college selected will be based on the player's scouted ability, too.

Right now I have setup 10 different categories of youth settings to cover the NBA (99.9% chance of college, to allow for the Mudiay/Jennings exceptions in theory - ie. they wouldn't actually play overseas for a year at this point, and would just be generated at 17 or 18 with 'None' for college), high level European nations (younger rookies, less chance of having attended college), mid-tier European nations (slightly higher chance of college), Latin America (above-average chance of college), etc. These are mapped out in a csv file with a reference number (or 'primary key'). I added an additional column to the csv containing all the countries information to point to the different youth settings.

It's working as expected, but further down the line I want to add more logic to it - ie. a one-star Nigerian rookie with 4 years college experience is probably not going to want to play professional basketball in Nigeria for peanuts. One way to handle this would be outside of the Youth Settings, making anyone with a college other than 'None' a lot less willing to sign in low salary leagues, or rather than just having a single % to determine how many rookies have college experience, weight it towards only the stronger players generated from that nation. I'm tabling it for now, but it's something I'll have to come back to when it comes time to think about free agency.

tarcone 04-10-2016 12:04 PM

I have been watching from afar. I really like reading this and watching your progress. Very impressive.

Keep up the good work and I am looking forward to watching this project come to its full potential.

Groundhog 04-11-2016 07:41 AM

Thanks!

It's been a slow couple of weeks - really struggling to come up with a player progression system that I'm happy with. Physical progression is in - players will develop in terms of height and weight (and speed/strength/jumping ability). Players will have growth spurts between 16-18, and potentially up through 20. Strength develops mostly around the 20-24 years.

What I'm struggling with now is 'basketball' attribute progression - whether to generate 'potential' per rating (which would be hidden), or have players develop semi-randomly.

My initial stance was semi-random, but I don't like how chaotic it makes the player progression system. Now I'm leaning the other way - players will be generated with a 1-5 potential rating in each attribute to indicate their ceiling, and an overall 'potential' rating that is partially masked until a certain age. I continue to tweak it.

Once I'm happy with this, I'll be working on the player generation logic per nation/league, and then initial work on the financial side of things, which is a little daunting to even think about to be honest!

Groundhog 07-08-2016 01:52 AM

Been awhile since an update. The previous post outlines the biggest reason for that, and I basically had to shelve it for now and just push on with other aspects of the sim. Hopefully I'll come up with a solution I'm happy with further down the line.

I put the finishing touches on the play-by-play "engine" today. It's completely customisable, with all text and the chance of that text appearing being stored in an .ini file as per the following quick example:

Code:

[introduction]
option1 = "Welcome to $home_team_arena, $home_team_display_city, where the $home_team_name are up against the visiting $away_team_name.|100"

[pre_opening_tip]
option1 = "$home_team_jumper and $away_team_jumper are in the middle for the opening tip, and we're ready to go.|50"
option2 = The referees blow their whistles, $home_team_jumper and $away_team_jumper are lined up for the opening tip, and we're ready to get things underway.|50"


Every bit of text in the PbP is in the ini file (outside of misc. stat reports, information re: the scores/time left, etc.), so it allows for some added flavour to the pbp, especially for things like dunks, passes, etc.

SirBlurton 07-08-2016 10:10 AM

Hah, I'd thought twice this week about bumping the thread for an update but tried to stay patient!

How do you end up processing the text file? That's something I haven't seen, I've been using massive dictionaries....what are the $ tags and |100 things? Codes for replacing when processed?

Groundhog 07-08-2016 08:03 PM

It's not Python at all, just tried to come up with something that is easy to edit by someone who doesn't know Python.

The '|integer' thing is the chance of that line appearing, and the $ tags are words that will be replaced by the Python equivalent, and then I use simple string operations (split on "|" to get the chance, and then a string replace method to swap out the $ tag).

The $ tags seemed like a good idea at the time but I might just replace them with the Python syntax - "{home_team_jumper}" instead of "$home_team_jumper".

I read this in with the configparser module rather than just importing the strings, but I do then build a dictionary that puts them in an easier format to work with.

nilodor 07-20-2016 07:01 PM

This is really awesome, I've been working on something similar, although I'm at about midway down the first page in figuring out how to get it done. I'm very inspired!

Groundhog 07-21-2016 12:35 AM

Quote:

Originally Posted by nilodor (Post 3110590)
This is really awesome, I've been working on something similar, although I'm at about midway down the first page in figuring out how to get it done. I'm very inspired!


Thanks! What language are you working in?

GoldenCrest Games 07-22-2016 10:19 AM

Just wanted to join the rest in saying this is an awesome dynasty topic!

Love all the little details you put in. I hope you keep it going!

Balldog 07-23-2016 06:16 AM

I've started learning Python due to this thread. What you have so far looks amazing.


Sent from my iPhone using Tapatalk

nilodor 07-27-2016 12:57 PM

Quote:

Originally Posted by Groundhog (Post 3110658)
Thanks! What language are you working in?


I had started off in unity, then went to Java and now am in Python. You are way ahead of where I am but you've certainly inspired me to keep going.

GoldenCrest Games 08-08-2016 08:35 AM

It just dawned on me that this has been a 2 year project! That's impressive.

Kudos for grinding away at it. I have such a hard time staying with projects for that long!

Groundhog 08-10-2016 01:57 AM

Yikes, thanks, I didn't even realize. Thought it was about 18 months. Most interesting thing has probably been how much I've adopted Python at work off the back of trying to make this thing - some of which has made my team's lives a lot easier (or at least, a lot less monotonous).

Only update I have lately is I've revisited the player generation/progression logic that I put aside because I thought of a way to do it. During player generation, I determine a 'ceiling' for each player, as well as their age, athletic profile and 'personality' attributes. The players are then "grown" to their current age with all progression being applied as it would during a normal offseason.

Currently the plan is to have players only progress in the offseason, but I like the idea of having a chance of 'bonuses' applied to player's attributes over the course of a season based on court time. The bonuses would take into account the player's potential in each rating, and be lost in the offseason, but playing time would also be a factor in offseason progression. I think it kind of models what happens in reality to some players, but I'm not sure if it will make its way in initially.

GoldenCrest Games 08-10-2016 07:52 AM

Quote:

Originally Posted by Groundhog (Post 3113645)
Most interesting thing has probably been how much I've adopted Python at work off the back of trying to make this thing - some of which has made my team's lives a lot easier (or at least, a lot less monotonous).


I've found the same thing. Since I started writing sports sims, I've been transferring the new programming skills to my job, writing various data crunching tools. (which has ended up helping career growth)

Funny how that works out!

nilodor 08-19-2016 03:05 PM

Silly noob question:

I'm trying to generate random numbers so I've done
Code:

from _random import Random

A = []
for i in range(0,8):
    A.append(4+Random().random())


But when I print A, I get the same random number the entire time. I'm kind of confused as to why as I thought if I don't specify a seed I should get a different random number based upon the system clock. Or is it just happening too fast, so the clock is the same?

nilodor 08-19-2016 03:22 PM

Quote:

Originally Posted by nilodor (Post 3115054)
Silly noob question:

I'm trying to generate random numbers so I've done
Code:

from _random import Random

A = []
for i in range(0,8):
    A.append(4+Random().random())


But when I print A, I get the same random number the entire time. I'm kind of confused as to why as I thought if I don't specify a seed I should get a different random number based upon the system clock. Or is it just happening too fast, so the clock is the same?


Apparently _random is different than random, which has fixed my issue!

Groundhog 08-19-2016 09:27 PM

It is - I actually have no idea what _random is, I know there's a few libraries with the underscores.

Another quick tip - you don't need the (0,8) with range, you can just:
Code:

for i in range(8):
  A.append(4 + random.random())


SirBlurton 08-22-2016 11:50 AM

You could also use a list comprehension to skip the step of appending to the created list and just create the list.

Code:

A=[(4+random.random()) for i in range(8)]

List comprehensions were one of the best things I've learned about. I had one block of about 12 overly complicated lines in an early project get replaced by one list comprehension....and that simplification flowed through everything else.

Groundhog 12-12-2016 10:16 PM

Yes, list comprehensions are the bomb once you get a handle on them. I remember reading through one of my first Python books and my eyes just glazed over when covering that topic (similar to what used to happen to me back in school in maths, actually... I wonder if I have dyscalculia...). It was only when I saw a real simple example in someone else's code that was clearly commented that it clicked with me.

Groundhog 12-12-2016 11:43 PM

It’s been months, so here’s a quick update:

Life and holidays got in the way for the most part since my last updates, but over the past three weeks I’ve picked this up again and got quite a bit done. One of the big changes I’ve made is just to focus entirely on the code component, and so I’ve sliced the GUI (PySide) right out at the moment. The main advantage to putting the UI aside is that it was really slowing me down, and my plan is to try and get the core functionality coded before looking at a UI again. If I’m happy with what I’ve coded I may just pay someone else to do it.

I think PySide is great, but I don’t know that I would ever be able to package it in a way that I’d like, and I also think that if I ever wanted to make money from this at some point, it makes sense to focus on the online/browser based component. This is contrary to how I myself like to play, but hey, I have massive CD and DVD collections too. I'm no design expert or anything, but I liked the overall style I came up with with my PySide GUI, and I'd probably want something very similar no matter which way I go. The code as I've been writing it will make it pretty easy to slot any UI framework over the top, so I can worry about this decision later, once I have a functioning simulator!

Groundhog 12-13-2016 04:12 AM

Testing out the 'universe generator' code I setup ages ago to make sure it all still works after cutting my code to pieces. Right now it uses the fictional default league info I've saved into a csv and generates fictional teams for all leagues based on that info, selecting random cities (weighted towards the most populous cities) and nicknames (if enabled). There is a ton of loading of csv files and calculations to make this info, which is what impresses me about Python - it's done in less than a second (excluding players - they take about 1.5 seconds at the moment).

I want to quickly be able to spin up entire random universes with teams/coaches/players/arenas etc. quickly to help with testing down the road.

For fun, here's the most recent USA 'NBA':

Quote:

San Jose Seagulls (SAJ): City: San Jose, CA, Popularity: 3, Arena: 1078, FG_Colour: #AF906A BG_Colour: #990000
Nashville Tigers (NAS): City: Nashville, TN, Popularity: 4, Arena: 1079, FG_Colour: #FFC700 BG_Colour: #00386B
Albuquerque Bruins (ALB): City: Albuquerque, NM, Popularity: 1, Arena: 1080, FG_Colour: #243760 BG_Colour: #F07422
Minneapolis Jaguars (MIN): City: Minneapolis, MN, Popularity: 2, Arena: 1081, FG_Colour: #00457C BG_Colour: #FF2222
Mesa Vikings (MES): City: Mesa, AZ, Popularity: 3, Arena: 1082, FG_Colour: #FFFFFF BG_Colour: #005D28
San Antonio Leopards (SAN): City: San Antonio, TX, Popularity: 5, Arena: 1083, FG_Colour: #003399 BG_Colour: #FF9900
San Diego Bucks (SAD): City: San Diego, CA, Popularity: 2, Arena: 1084, FG_Colour: #F8B800 BG_Colour: #000000
Philadelphia Thunderbirds (PHI): City: Philadelphia, PA, Popularity: 4, Arena: 1085, FG_Colour: #B31F29 BG_Colour: #11351A
Washington Stallions (WAS): City: Washington, DC, Popularity: 2, Arena: 1086, FG_Colour: #C41E3A BG_Colour: #321414
Virginia Beach Hornets (VIR): City: Virginia Beach, VA, Popularity: 3, Arena: 1087, FG_Colour: #BDB76B BG_Colour: #000000
Colorado Springs Ospreys (COS): City: Colorado Springs, CO, Popularity: 3, Arena: 1088, FG_Colour: #000000 BG_Colour: #FFC94B
Santa Ana Hawks (SAN): City: Santa Ana, CA, Popularity: 2, Arena: 1089, FG_Colour: #FFFFFF BG_Colour: #D2DDFC
Honolulu Salukis (HON): City: Honolulu, HI, Popularity: 3, Arena: 1090, FG_Colour: #FFFFFF BG_Colour: #990000
Arlington Eagles (ARL): City: Arlington, TX, Popularity: 3, Arena: 1091, FG_Colour: #000000 BG_Colour: #DC143C
Austin Barons (AUS): City: Austin, TX, Popularity: 3, Arena: 1092, FG_Colour: #374EA1 BG_Colour: #FFF200
Raleigh Miners (RAL): City: Raleigh, NC, Popularity: 4, Arena: 1093, FG_Colour: #000066 BG_Colour: #FFFFFF
Chicago Pirates (CHI): City: Chicago, IL, Popularity: 2, Arena: 1094, FG_Colour: #000000 BG_Colour: #CC0000
Denver Bees (DEN): City: Denver, CO, Popularity: 4, Arena: 1095, FG_Colour: #50C878 BG_Colour: #0047AB
Phoenix Nighthawks (PHX): City: Phoenix, AZ, Popularity: 3, Arena: 1096, FG_Colour: #FFFFFF BG_Colour: #0000FF
Tucson Flames (TUC): City: Tucson, AZ, Popularity: 2, Arena: 1097, FG_Colour: #000000 BG_Colour: #E00122
Saint Paul Centaurs (SAP): City: Saint Paul, MN, Popularity: 3, Arena: 1098, FG_Colour: #EF6E22 BG_Colour: #216EB7
Dallas Comets (DAL): City: Dallas, TX, Popularity: 1, Arena: 1099, FG_Colour: #FFFFFF BG_Colour: #000070
Riverside Kings (RIV): City: Riverside, CA, Popularity: 5, Arena: 1100, FG_Colour: #FFFFFF BG_Colour: #5858FA
Los Angeles Hawks (LAS): City: Los Angeles, CA, Popularity: 3, Arena: 1101, FG_Colour: #FFFDD0 BG_Colour: #960018
St. Petersburg Riverhawks (STP): City: St. Petersburg, FL, Popularity: 5, Arena: 1102, FG_Colour: #C5B358 BG_Colour: #8B0000
Houston Defenders (HOU): City: Houston, TX, Popularity: 1, Arena: 1103, FG_Colour: #613005 BG_Colour: #FF7300
Louisville Yellow Jackets (LOI): City: Louisville, KY, Popularity: 4, Arena: 1104, FG_Colour: #FF9900 BG_Colour: #000000
Milwaukee Wolves (MIL): City: Milwaukee, WI, Popularity: 3, Arena: 1105, FG_Colour: #CAB388 BG_Colour: #00285C
New York Cavaliers (NYK): City: New York, NY, Popularity: 4, Arena: 1106, FG_Colour: #990000 BG_Colour: #000066
Jersey City Falcons (JER): City: Jersey City, NJ, Popularity: 3, Arena: 1107, FG_Colour: #FFD700 BG_Colour: #000000

Usually it's very Texas heavy as I used a list of the 80ish most populated cities in the USA.

And the same for Spain:

Quote:

Alicante (ALI): City: Alicante, Popularity: 1, Arena: 941, FG_Colour: #FF652B BG_Colour: #4C230E
Pamplona (PAM): City: Pamplona, Popularity: 4, Arena: 942, FG_Colour: #FFFFFF BG_Colour: #006000
Valencia (VAL): City: Valencia, Popularity: 1, Arena: 943, FG_Colour: #C41E3A BG_Colour: #321414
Vitoria-Gasteiz (VIT): City: Vitoria-Gasteiz, Popularity: 2, Arena: 944, FG_Colour: #FFFFFF BG_Colour: #00386D
Madrid (MAD): City: Madrid, Popularity: 4, Arena: 945, FG_Colour: #ED1C24 BG_Colour: #F5EE30
Valladolid (VAL): City: Valladolid, Popularity: 3, Arena: 946, FG_Colour: #CFB53B BG_Colour: #006633
A Coruña (ACO): City: A Coruña, Popularity: 4, Arena: 947, FG_Colour: #FFFFFF BG_Colour: #461D7C
Getafe (GET): City: Getafe, Popularity: 4, Arena: 948, FG_Colour: #FFAC2C BG_Colour: #240A67
Bilbao (BIL): City: Bilbao, Popularity: 3, Arena: 949, FG_Colour: #662D91 BG_Colour: #F7BE19
Oviedo (OVI): City: Oviedo, Popularity: 2, Arena: 950, FG_Colour: #EFEFEF BG_Colour: #002F30
Córdoba (COR): City: Córdoba, Popularity: 5, Arena: 951, FG_Colour: #FFFFFF BG_Colour: #00A000
Sabadell (SAB): City: Sabadell, Popularity: 2, Arena: 952, FG_Colour: #FFC61E BG_Colour: #000C03
Málaga (MAL): City: Málaga, Popularity: 3, Arena: 953, FG_Colour: #FAE051 BG_Colour: #002469
Castellón de la Plana (CAS): City: Castellón de la Plana, Popularity: 3, Arena: 954, FG_Colour: #0021A5 BG_Colour: #FF4A00
Santander (SAN): City: Santander, Popularity: 3, Arena: 955, FG_Colour: #000000 BG_Colour: #F47B20
Badajoz (BAZ): City: Badajoz, Popularity: 3, Arena: 956, FG_Colour: #818285 BG_Colour: #105D95
Lleida (LLE): City: Lleida, Popularity: 2, Arena: 957, FG_Colour: #FF652B BG_Colour: #002E4D
Vigo (VIG): City: Vigo, Popularity: 5, Arena: 958, FG_Colour: #FFCC33 BG_Colour: #000000


SirBlurton 12-14-2016 04:25 PM

Very cool! It's interesting to watch your decision making process as you work through the project! Probably a wise move to put GUI aside, from screenshots it looks like you've done enough to know you can do that if that's the direction you want to go!

If there are ever any tasks you think you could use a hand with let me know...I'm still very much a beginner but I've been working away at building little bits and pieces like name generators and cartoon face generators. Just starting to learn about databases and flask right now.

I know that would likely be too complicated to manage and it's also "your baby" - but just wanted to put it out there if there's ever a piece you could use help on.

Continuing to watch for your updates! :)

Groundhog 12-14-2016 09:26 PM

Yeah, and as a plus I've learnt a good deal about PySide/PyQt/Qt, and have developed UIs for a bunch of other small projects at home and in the office, and created my first "proper" python module to manage stylesheets for widgets, the creatively named qt_widgetstyler, so it has not been time wasted!

While I do think of this as "my baby" to some extent, I'm more than open to help. By no means feel obligated to tackle this, but one thing I was playing with months and months ago and couldn't get it to work quite right is a round-robin schedule creator. When I get a chance I'll post what I put together, and if you can get the dang thing to work, fantastic! It's harder than it sounds, because it needs to take into account a few factors, such as valid days of the week to schedule games, etc. :)

SirBlurton 12-15-2016 01:53 PM

I have played with scheduling a bit and I definitely wouldn't go in thinking it's easy! But that would be cool to take a look at it...like you said, if I can't do it, it'll be something you can look at again in the future but I'd be interested in taking a crack at it!

CraigSca 12-15-2016 03:05 PM

I don't want to steal sirBlurton's thunder, but I wouldn't mind lending a hand with things, either. I've been programming my own college football sim but it's coming in drips and drabs and wouldn't mind increasing my skill set.

Groundhog 12-15-2016 09:15 PM

Hey, the more the merrier as far as I'm concerned :D

Here's a snippet of what I put together actually nearly 12 months ago, checking the time stamp of the file. I've made a few small changes just now, but I haven't tested anything yet, so there may be typos/errors if you try run the below currently (just working out of notepad at the moment...) :)

The part that was frustrating me was the 'get_valid_game_days' method. There's nothing in the code below for this, but I spent an hour or so playing around on the python command line trying to solve this - basically, need to take a date range, and calculate all the valid game days within this range, grouped by week. The timedate library has some cool functionality that should make this possible, but I'll be darned if I could crack it - this was that "math-level" stuff I mentioned above that I struggle with.

For example, if Friday and Saturday were the only valid days, and the date range was for 4 weeks starting this week, the output of this method should be:

[[16/12/2016, 17/12/2016], [23/12/2016, 24/12/2016], [30/12/2016, 31/12/2016], [6/1/2017, 7/1/2017]]

As a rule I generally work with all dates as ordinal numbers - datetime objects have a .toordinal() method. I just trust it more, especially when writing to databases.

Code:

class ScheduleGen:


    def __init__(self, current_year=2016, season_start_month=9, season_start_day=30, season_end_month=3, season_end_day=28):
        self.current_year = current_year
        self.season_start_month = season_start_month
 

    def create_round_robin_schedule(self, team_ids=None, play_each_team=3, valid_days={'monday': False, 'tuesday': False,                                                                                        'wednesday': False, 'thursday': False,
                                                                                        'friday': True, 'saturday': True,
                                                                                        'sunday': True}):

        matchups = self.generate_round_robin_matchups(teams=team_ids, play_each_team=play_each_team)
        valid_game_days = self.get_valid_game_days(valid_game_days=valid_days)
   

    def get_valid_game_days(self, valid_game_days):
        '''
        Parameters:

        :valid_game_days: (dict) A dictionary of each day of the week and a True/False value.

        Returns:

        A list of all possible game days, in ordinal format.
        '''
        # This is where I had trouble. What is needed here is to use the current_year/season_start_month/season_start_day,
        # the current_year+1/season_end_month/season_end_date, and the valid_game_days dictionary to compile a list of
        # all available dates, grouped in weeks (to ensure teams don't always play multiple games in the same week),
        # with the dates stored as ordinals (whole numbers - a feature of the timedate module), similar to this:
        # valid_dates = [[734228, 734229],[734235, 734236],[734241. 734242]] etc.
        pass

    @staticmethod
    def generate_round_robin_matchups(teams=None, play_each_team=1):
        '''
        Note: Bulk of this code taken from: https://gist.github.com/Makistos/7192777

        Parameters:

        :teams: (list) league team IDs.
        :play_each_team: (int) how many times all teams play each other.

        Returns:

        A list of all valid matchups.
        '''
        matchups = []

        if len(teams) % 2 == 1:
            #teams = teams + ['BYE']
            teams.append(False)

        for i in range((len(teams) - 1) * play_each_team):

            mid = round(len(teams) / 2)
            l1 = teams[:mid]
            l2 = teams[mid:]
            l2.reverse()       
           
            # Switch sides after each round
            if(i % 2 == 1):
                matchups = matchups + [ zip(l1, l2) ]
            else:
                matchups = matchups + [ zip(l2, l1) ]

            teams.insert(1, teams.pop())

        return matchups


Groundhog 12-15-2016 09:21 PM

dola

The above was written in Python 2 as well, where as I've moved to 3 now. I think the only impact this has on the above is that the 'generate_round_robin_matchups' method will returns a generator object rather than a list of lists, but this is minor.

edit: fixed some typos in the above.

CraigSca 12-18-2016 10:39 AM

Is the valid_game_day list only for a single week or for all weeks? Meaning, do I want to state that Mondays and Tuesdays are invalid game dates for ALL weeks, or do I just want an potentially long list showing which days are invalid for the entire year?

Groundhog 12-18-2016 05:14 PM

For all weeks - so a long list of dates, grouped by weeks, between a start date and end date. What I'm trying to achieve here is a list of all valid game days between two dates. The next step would be assigning all the matchups generated by the 'generate_round_robin_matchups' method to these dates.

Most of the "round robin" leagues around the world have set days that they usually play their games - for European leagues it's generally Sat/Sun (which makes the Euroleague/Eurocup scheduling easier), in Australia it's everyday except Tue/Weds - so that's the behaviour I'm looking for here. The leagues all have the 'valid days' attribute that defines which of these days of the week to schedule games on. This class (will eventually) take that info as a dictionary, generate a list of all the matchups, generate a list of valid game dates, and then assign those matchups to those dates.

CraigSca 12-19-2016 09:16 AM

Quote:

Originally Posted by Groundhog (Post 3136147)
For all weeks - so a long list of dates, grouped by weeks, between a start date and end date. What I'm trying to achieve here is a list of all valid game days between two dates. The next step would be assigning all the matchups generated by the 'generate_round_robin_matchups' method to these dates.

Most of the "round robin" leagues around the world have set days that they usually play their games - for European leagues it's generally Sat/Sun (which makes the Euroleague/Eurocup scheduling easier), in Australia it's everyday except Tue/Weds - so that's the behaviour I'm looking for here. The leagues all have the 'valid days' attribute that defines which of these days of the week to schedule games on. This class (will eventually) take that info as a dictionary, generate a list of all the matchups, generate a list of valid game dates, and then assign those matchups to those dates.


Stupid question - and now I see why working in teams can sometimes be hard - when you generate the list of invalid game days (the "falses" in the list) can't you just generate the "trues" as a list for the entire year, keep it in memory and just access it when needed?

Groundhog 12-19-2016 06:23 PM

No, not a stupid question at all!

Ignore the invalid dates, because we only need those to determine which days are valid, and they will be skipped over. The False/True flags just tell the method that it needs to compile a list of all True days for each week between a range of dates. So if we feed the class a starting date of June 30 2016 up until January 31 2017, the method would first check which days are valid - let's say Saturday and Sunday - and then split up the the period between those dates into units of weeks, and append all the dates of Saturday and Sundays to a list. When we assign the matchups to days, it would be a random.choice() type of operation to randomly assign the individual matchups to these valid dates.

This could all be stored in memory, but this calculation will need to be run for all 70ish leagues once per offseason, as they all have their own unique True/False flags for valid game days, and obviously the dates will also change from year to year.

The final output of this would be a list of all games in a season, with the date, league id, home team id, and road team id, which would then be written to the database.

An instance of this class would be used inside another method somewhere along the lines of (and this is probably full of syntax errors, but just to give you a quick idea):

Code:

def generate_all_schedules(dict_of_all_leagues, dict_of_all_teams, current_year=2016, season_start_month=9, season_start_day=30, season_end_month=3,
                          season_end_day=28):
    # dict_of_all leagues: A dictionary with league_id as the key, all league attributes as values in another dict.
    # dict_of_all_teams: A dictionary with the league_id as the key, and all team ids in a list as value.

    season_schedule = []

    sg = ScheduleGen(current_year, season_start_month, season_start_day, season_end_month, season_end_day)

    for league in dict_of_all_leagues:
        # Create a 'shortcut' reference directly to the league to be processed
        l = dict_of_all_leagues[league]
        # Create a 'shortcut' reference to list of teams in league
        t = dict_of_all_teams[l['id']]

        league_sched = sg.create_round_robin_schedule(team_ids=dict_of_all_teams[l['id']], play_each_team=l['play_each_team'], valid_game_days=l['valid_game_days'])
       
        season_schedule.append(league_sched)

    return season_schedule


Groundhog 12-19-2016 08:08 PM

dola

Had another thought. Maybe the way to approach it is to just create a dictionary once of every week and day of the week in that date range, and then use that info to build all the schedules.

CraigSca 12-20-2016 10:11 AM

Quote:

Originally Posted by Groundhog (Post 3136339)
No, not a stupid question at all!

Ignore the invalid dates, because we only need those to determine which days are valid, and they will be skipped over. The False/True flags just tell the method that it needs to compile a list of all True days for each week between a range of dates. So if we feed the class a starting date of June 30 2016 up until January 31 2017, the method would first check which days are valid - let's say Saturday and Sunday - and then split up the the period between those dates into units of weeks, and append all the dates of Saturday and Sundays to a list. When we assign the matchups to days, it would be a random.choice() type of operation to randomly assign the individual matchups to these valid dates.

This could all be stored in memory, but this calculation will need to be run for all 70ish leagues once per offseason, as they all have their own unique True/False flags for valid game days, and obviously the dates will also change from year to year.

The final output of this would be a list of all games in a season, with the date, league id, home team id, and road team id, which would then be written to the database.

An instance of this class would be used inside another method somewhere along the lines of (and this is probably full of syntax errors, but just to give you a quick idea):

Code:

def generate_all_schedules(dict_of_all_leagues, dict_of_all_teams, current_year=2016, season_start_month=9, season_start_day=30, season_end_month=3,
                          season_end_day=28):
    # dict_of_all leagues: A dictionary with league_id as the key, all league attributes as values in another dict.
    # dict_of_all_teams: A dictionary with the league_id as the key, and all team ids in a list as value.

    season_schedule = []

    sg = ScheduleGen(current_year, season_start_month, season_start_day, season_end_month, season_end_day)

    for league in dict_of_all_leagues:
        # Create a 'shortcut' reference directly to the league to be processed
        l = dict_of_all_leagues[league]
        # Create a 'shortcut' reference to list of teams in league
        t = dict_of_all_teams[l['id']]

        league_sched = sg.create_round_robin_schedule(team_ids=dict_of_all_teams[l['id']], play_each_team=l['play_each_team'], valid_game_days=l['valid_game_days'])
       
        season_schedule.append(league_sched)

    return season_schedule



Yeah - getting back to my question - does that mean the list of true/false flags are ONLY for days of the week, or could you have something like false for all Saturdays and also Christmas day?

Groundhog 12-20-2016 05:27 PM

Quote:

Originally Posted by CraigSca (Post 3136446)
Yeah - getting back to my question - does that mean the list of true/false flags are ONLY for days of the week, or could you have something like false for all Saturdays and also Christmas day?


Currently, only for days of the week. I did think about things like holidays, but at the same time its a lot of work tracking down holidays/special days for 68 different nations, to have things like no Christmas day games in Australia, or no Sunday games in Israel, etc.

Young Drachma 01-31-2017 04:49 AM

Spam aside, glad you're still at this.

GoldenCrest Games 03-05-2017 05:27 PM

Nice to see you're still chugging along!

nilodor 07-07-2017 10:24 AM

Quote:

Originally Posted by Groundhog (Post 3080333)
Until this point I've imported all of my data - teams, coaches, leagues, nations, etc. - in a database (sqlite) that I manually populated from csvs full of info I've gathered over the last couple of years, so I spent some time the past couple of days putting together the 'new game' process - main menu/league selections, etc.

Here's a couple of screens:

Title Screen
A quick (5 minute) title screen just to give the standard new/continue/load options.


New Game Menu
This one took a lot longer - the fully functional league selection screen. All the data here is taken from csv files that can be easily modified to add/subtract/modify the leagues included - currently 44 fields per league (some repetitive, like 7 fields for each day of the week that games will be scheduled). Leagues where I've manually created teams (also done via csvs) default to 'import' teams, other leagues will be randomly generated. Active leagues will be bolded (not yet done), and I'll ditch the '#' in the Teams column header.

The Olympics/Tournaments options on the left will be changed to a table like the regional leagues, because it gives me more flexibility with how many I can include without running out of room - there's international tournaments in the Americas, Asia and Africa that I don't plan on looking at now but would probably like to look at down the track:



Nav Bar
Lastly, the top-of-screen navigation bar - not completely functional yet, because I don't have all the screens completed. This image is at 100% actual width, to give you an idea of how wide the UI will be:


What did you use to make these screens? They look great, much better than what I'm capable of in PyQt

Groundhog 07-09-2017 06:44 AM

Quote:

Originally Posted by nilodor (Post 3166003)
What did you use to make these screens? They look great, much better than what I'm capable of in PyQt


Just a combination of the Designer app to layout all the widgets, and then applying different stylesheets to each widget within Python. Most of the "magic" is just labels with rounded corners and partially transparent background colours (rgba with an alpha channel that acts as the transparency %), so the background image shows through.

nilodor 07-19-2017 03:15 PM

Quote:

Originally Posted by Groundhog (Post 3166123)
Just a combination of the Designer app to layout all the widgets, and then applying different stylesheets to each widget within Python. Most of the "magic" is just labels with rounded corners and partially transparent background colours (rgba with an alpha channel that acts as the transparency %), so the background image shows through.


To get the rounded push buttons did you use something like:

Code:

self.pushButton.setPalette(palette)
self.pushButton.setStyleSheet("background-color: rgb(170, 255, 255); border-style:solid; border-color:black; border-radius:20px;border-width:2px;")
self.pushButton.setObjectName("pushButton")


I can't seem to get rounded edges this way

Groundhog 07-19-2017 10:10 PM

Code:

standard_button = '''QPushButton {{
                                  color: rgb{};
                                  background-color: rgb{};
                                  border-bottom-left-radius: {}px;
                                  border-bottom-right-radius: {}px;
                                  border-top-left-radius: {}px;
                                  border-top-right-radius: {}px;
                                }}
                    QPushButton:hover {{
                                        background-color: rgb{};
                                      }}
                    QPushButton:checked {{
                                          background-color: rgb{};
                                          color: rgb{};
                                        }}
                    QPushButton:disabled {{
                                            color: rgb{};
                                            background-color: rgb{};
                                            border: none;
                                          }}'''


I can't remember if there's a good reason why I used individual corner radius %s vs just a general border-radius setting. Swap out the {} for values and that's straight out of my code.

I didn't use the setPalette or .setObjectName methods for any of my widgets - again, can't remember if that was for a reason or if I just never saw those methods. I created this library to let me group widgets and make it easier to manage colour schemes on different team pages, etc. It's super amateur and I'm sure Qt has a way of doing the same thing (possibly .setObjetcName), but it was an easier "python" way for me to handle it.

nilodor 07-20-2017 09:15 AM

Quote:

Originally Posted by Groundhog (Post 3167032)
Code:

standard_button = '''QPushButton {{
                                  color: rgb{};
                                  background-color: rgb{};
                                  border-bottom-left-radius: {}px;
                                  border-bottom-right-radius: {}px;
                                  border-top-left-radius: {}px;
                                  border-top-right-radius: {}px;
                                }}
                    QPushButton:hover {{
                                        background-color: rgb{};
                                      }}
                    QPushButton:checked {{
                                          background-color: rgb{};
                                          color: rgb{};
                                        }}
                    QPushButton:disabled {{
                                            color: rgb{};
                                            background-color: rgb{};
                                            border: none;
                                          }}'''


I can't remember if there's a good reason why I used individual corner radius %s vs just a general border-radius setting. Swap out the {} for values and that's straight out of my code.

I didn't use the setPalette or .setObjectName methods for any of my widgets - again, can't remember if that was for a reason or if I just never saw those methods. I created this library to let me group widgets and make it easier to manage colour schemes on different team pages, etc. It's super amateur and I'm sure Qt has a way of doing the same thing (possibly .setObjetcName), but it was an easier "python" way for me to handle it.


Weird, doing each corner separately seems to work. I think that you have to use the palette command if you're using PyQt5. Like how the QMainWindow moved from PyGui to QtWidgets.

One thing I'm going to have to work on is the ability to batch import. I've got it set up that I can read in constants from a text file for athlete generation, but I'm not sure how to do it to create a generic style sheet based on the country the player is using.

Maybe I'll actually post in my thread to discuss it better. :)

Groundhog 07-20-2017 10:02 PM

Happy to show you how I do it, PM me if you want a quick example.

CrescentMoonie 07-20-2017 10:21 PM

Enjoyed reading through this. I'm at the very beginning of my journey learning languages, starting with a couple of web developer courses through Udemy (nearly everything on the site was $10 around July 4). After that I'm looking at Ruby on Rails, I already bought an intro course on it, and maybe Python after that. This dynasty will definitely help in my sections on Python in the two developer courses.

Groundhog 06-25-2019 06:43 PM

Just an update on this, because I get the odd PM here and there, especially from folks who find this thread via google. The two questions I get are:

Did I ever finish this? No. Real life, in particular a promotion at work and having a kid, made this begin to feel like work to be completely honest. I was spending 2-3 hours a night coding in bed, and I was beginning to have dreams involving Python functions, which was a bit strange. I decided to take a break from it and never got the urge to pick it up again. I'm sure I will down the line however, and I've still kept up with Python and use it from time-to-time at work.

Is it possible to program something like this in Python? Absolutely yes, and probably a lot better than how I was doing it. Even the GUI I think looked OK, but as I touched on a few times in this thread I would ignore this component it if I did this again and look at a web GUI (and probably paying someone to build one rather than learning it myself).


All times are GMT -5. The time now is 06:15 AM.

Powered by vBulletin Version 3.6.0
Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.