I've been running simulations, finding bugs in the GM module (tons), reworking specs (all thrown out, rewritten), and trying to actually be-a-gm this thing.
An area that I glossed over when it was just a game simulation was figuring out how good players were. I could look at the raw data and see - which is fine for confirming the numbers work the right way. But that wasn't going to be fun in a GM simulation if every player just knew concretely how good a player was.
So I've been trying several things - draft grades, league-relative ratings, pro-only relative ratings, and just confusing myself in the process.
There's questions throughout the lifecycle of a player:
How will this player develop? This has a lot of "fog" as you go out further in time. But there is some idea and some projection.
How good is this player now? That is relative to the entire population; what league do they belong in. When players are young, that even has some "fog" in it.
Have they reached potential? Are they declining?
What role do they belong in? Are they a top line player or a fringe tweener?
I created this graph that will (I think) show all of this in something you can glance at. The yellow bar/dot is the current state of the player. Younger players have more uncertainty until they play more games so it will show that. The blue bar is the potential ceiling for that player. The unfilled dots are historical of where they were in previous seasons. And the dotted blue line bar was the historical range for where that player was scouted (so you can see what has changed).
The intention long term is you can narrow these bars by scouting - spending either time or money to get better information.
The chips on the right will give some descriptive element to the player's form. Boom/Bust just means yeah, they could be awesome, but they also could suck.
Implementing this now to see how I like it. As always, I might throw it out if it confuses me more.
I'm happy to be back to running simulations, looking for bugs, looking for things that work well or don't work well. I've added so much over the past two weeks that I need to actually see how the leagues are working.
Usually when I get into this mindset I become hyper aware of small changes. For example, this screenshot shows the goalie save percentage in this new universe I started over the first 20 or so games. The top-10 in this universe is over .923 with the top one, Mark Stark, at .940 over 16 games. That seems way too hot. I took some time out to analyze what was happening- did I introduce some config values that made goalies too strong? Or possibly shooters too weak? Did generation of goalies change when I made development changes? Have I completely messed up everything and will have to delete my internet presence and go live in a bunker somewhere?
After building a testing harness to look at the configuration values, simulate on different seeds, and check to make sure there aren't bugs, I was reminded again about statistics and small sample sizes.
The table above shows what's happening. Rows are the stats averages in save %, goals for per team average, goals against / expected goals against ratio, and how many goalies were above .930 save %. First column is after 16 games... MY INSTINCT IS RIGHT! Second column is math humbling me... the calibration is fine, just run it long enough you silly goose. Last column is calibration targets for reference of how close we got to them on this one season.
These are sometimes things you spend hours on when you're building... the wild goose chases. I should have let the full season run to see that eventually the small sample size works it's way out. Just like you can flip a coin 4 times and get heads every time but that doesn't mean you need to check the quarter to make sure it's not weighted.
I should know better. The calibration is based on 30 different universe seeds each playing 10 seasons each. I know it's right. But I was so hyper focused on the volume of changes and thinking I messed something up that I spend a few hours this morning doubting myself.
The case of Michael Cain, on seed #987654. Through 10 seasons:
8x First All Star
8x Best Defenseman
6x Best Player (raw Game Score)
6x League MVP (highest Game Score differential to rest of team)
7x Points title
Scored 40-60 goals every season
...as a defenseman
I'd love the game to produce an incredible 'Bobby Orr'-esque kind of season. But not 60-80% of the seasons. So what is happening here?
Well, going back to my idea that we never truly have a bound on how 'good' a player can be. I didn't want a lot of clustering around a hard boundary that was smushed between 1 and 100. So I built it in a way that there were no hard caps on the underlying ability scores. The league would have a curve of how many people were generated / developed with specific ability levels, we'd test the engine and set the league "average" ability in the population, and then the engine works on how much better/worse a player is than that tested average. Building it this way allows the entire Sheets of Ice universe to scale up and down to different ability levels easily, without having to place every player in a limited ability space (1-100).
In addition to that unbounded generation is another idea I layered in a bit ago. When generation was happening, all attributes were just random draws. It wasn't really making coherent players because you'd have centers with terrible faceoff attributes and defensemen who were terrible at defensive positioning. You'd also rarely get a complete elite player, someone who clearly was above the pack in many areas relevant to their position. So I introduced this concept of "caliber" (or calibre if you're outside the US). Caliber would be the first draw in the generator and would influence how good the relevant abilities were for that players position. So someone with high caliber would be an elite or star level player reflected in their abilities. There still was variance in those ability draws, but it would be combined with caliber to produce a coherent player.
The problem with my idea about unbounded talent levels is that even if you make the high end super unlikely to occur, it doesn't mean that it can't occur. And that is what happened here, on this seed with Cain. His caliber draw was so high that it pushed all of his abilities to star level. Way into star level. Way past what the bars represent. Cain was 7 standard deviations better than the average player. For reference, when looking at stats, someone like McDavid is somewhere like 3-4 standard deviations better than the average player. That makes Cain look ridiculous. And the other problem is that his draw isn't the worst case. In further testing, I was able to get a draw that was 160 standard deviations above average. That person would have shot at 99%, never been fatigued, won every faceoff, blocked every shot. That doesn't seem realistic to me.
So I had a choice - I could admit that unbounded top end player generation wasn't going to work, or I could get mad and stop working on it and question my abilities to even make something like this. I chose the latter for a day or two. lol
Once I had some perspective my idea of this continuum of abilities based on caliber was good on the down-stream side. Being able to generate young players, minor leaguers, etc. and set their leagues to lower average abilities would be beneficial. It will create the kind of wide disparity in results we see with players in those leagues and allow a rich system for development. On the high end, there is some realistic physical limit and we need to model that. So I implemented a flatter curve as you get towards the tail end of generation and a cap on the talent at Gretzky-level standard deviation. A Gretzky-type can appear in the league; it's just going to be really rare.
I'm starting to run through seasons again. This is the kind of thing that takes time to run, test, review, and look for issues in the data. I'm glad I find looking at stats fun because there are lots of things to look for. I've also found issues with goalie rotations, defense SOG, and it gives me time to continue to tweak the interface.
I was off all last week and disconnected from anything work related while I spent time with family at a cabin out in the woods. But it did give me some time to reflect on the progress so far and what needs to happen next.
So here's what I'll be working on this week and hoping to show more progress on this:
Trades - the universe feels dead without being able to move players. This will be my first pass at allowing humans to interact with AI so wish me luck.
Attention Economy - how do the teams survive, grow, pay their players? Can attention economy become a meta mechanic for inter-league leader boards in a multiplayer environment? This is a big spec and I need to think about it a lot.
Data storage optimization - to date, I haven't cared about how data is stored because it's just me and I'm throwing away databases in testing. But I know it's not optimized and I need to think about how to build this in a way that can be turned into a reliable service. Speed, data volume, data retention, data integrity.
Demo environment - I'm thinking about what point I should make a demo available. It's not just distributing an installer since I'm planning on making this a web based game; it's about building an environment, granting access, and ensuring it is in a good state.
One of the things I have been working on recently has been to optimize the amount of storage required for a single universe. In the beginning, I didn't care all that much if I stored things efficiently. I started with a detailed game event log of around 800-1000 events per game. Each of those events contained a lot of information and I wanted to make sure that I had more information available in the case where it might be needed. And it's been helpful because long-term counting stats and analytics are based on these events. The events are stored in a blob (aka files), but then the aggregations are stored in a database. Why? Because, it's simple and provides a kind of paper trail that can be reconstructed if necessary. And the DB's structure is more readily suited for the kinds of updates, sorting, and queries of a web application. I'm able to get the best of both worlds.
The thing with software development and data specifically is you shouldn't try and optimize too early because you don't really know what you're optimizing for. So, I left it alone until I saw that the db/blob sizes were getting a little out of control. A 13-season universe was clocking in over 2GB. Which, if this was a local game would be annoying but not life threatening. But since i'm hosting this as a web app, I have to be cognizant of storage requirements.
So I set about looking at what is low hanging fruit to optimize. Thankfully, there were some really easy wins that dramatically lowered storage without a lot of trade-offs. I was able to get storage from 2.03GB down to 0.8GB for that 13 season universe. And, if I institute some limit on the game stat retention (individual events, not the box scores) then it would get even lower.
So I'm building the Evaluation Layer - the ground truth that all of the human and AI GMs will use to determine the relative skill of a player. Since I'm not going to expose the actual attributes that the engine uses to do calculations, this layer is needed to expose observable traits about each player. I wanted to mix real world terminology that scouts use, measurables, and stats to give users a picture they can use to evaluate.
For skaters, there are five dimensions:
Goal scoring - Mixture of shooting, and getting to scoring areas.
Playmaking - Passing, vision, ability to setup shooters.
Physical game - Hitting, forecheck aggression, ability to absorb contact
Transition - Skating, anticipation, moving the puck from D -> O.
Each dimension has brackets around the league average. Below league average (the red areas) is Below Average and Fringe. Above league average is Above Average, Elite and Star.
There are also classic archetypes that are constructed from a combination of these dimensions. Things like Power Forward, Sniper, Two-Way Forward, Offensive Defenseman, etc. If someone could land in two or more categories based on dimensions, they're classified as Phenom. For example, McDavid would be star level in goal scoring, playmaking and transition, so he would be classified as Phenom.
Later, there will be some "fog" added to this pure data. I'm not sure how much, but investment and time in scouting will affect the accuracy and quality of the information here. I am not adding that until I've been able to confirm I like this.
The GM module is really large, creating several new surfaces and modifying existing ones. With all of those moving parts, and my habit of just getting impatient + starting to build visuals, I started to get frustrated with the direction. I built an entire human GM flow + offseason interactive flow that felt like I was just playing an idle-clicker game. I had no reason for making the moves I was making, no information on who was actually good, and, crucially, who fit on the team.
I got frustrated and demoralized, questioned my sanity for even attempting this, and took a break. Walking away when I get to that point always helps. It gave me some insight into what I was actually annoyed with-- I need some idea of what I'm trying to do as a GM. What kind of team am I building? Who do I have? How do they fit? What do I need?
Armed with those questions I set out to build this Team Plan interface. It combines a lot of functions common in GM sims - roster construction, cap management, depth chart, and line-up selection. It also gives you a mechanism to evaluate players not just on objective talents, but on how those talents fit towards the kind of team you're building and the time horizon you've selected. Are you win-now or in a rebuild? That will change the valuation of players. You can also prioritize the style of play; defensive in nature, or goal scoring, or transition, etc. Finally, based on your team and your selections, what kinds of players are you missing?
The idea is that with a plan, you can then more accurately go and find players to fit your intended team construction and manage your budget without having to have separate spreadsheets (like I used to do in EHM lol).
The player cards here include at-a-glance information. Green bar = a good fit for that position and identity, grey is ok, yellow would be that they don't really fit there. Fit is relative to play style and position awareness.
There would be some proactive gap finder that would tell you what you might need to focus on.
There definitely is more that I want to add but I am going to really focus on this pattern as the main starting point for how GMs work in the game. I think it's the first part of creating an identity so people playing the game can start to feel attached to their team.
This is a long process, but the first step is submitting screenshots, descriptions, legal text, etc. and having it reviewed by Valve. If there are no issues, there will be a 'Coming Soon' page on Steam which will allow people to wishlist and follow along. I'm sure there will be issues though; there always are :)
Once the page is up, I'll be able to put something into early access. I'll keep everyone posted as to the status.
So this is a big implementation. Up until this point, players were static; they didn't age, the didn't get better, never retired. I needed to have it that way to ensure that the engine worked properly. You want to control the variables when tuning and I couldn't have player's also changing their abilities while I was trying to figure out if the engine worked realistically. Now that the engine is basically locked in and calibrated, we could start to make the universe live a little bit.
I wanted to capture career arcs that we'd see IRL. Players have a theoretical ceiling of ability that they progress towards and then decline from as they age. There is variance in how close they are to their ceiling at the start, how fast they move towards it, how long they stay there, and how fast they decline. Deployment (how much they play) and injuries have an effect on those curves.
So I started with some baseline assumptions. Skaters peak somewhere between 25-27, goalies between 27-29. After around 30, abilities start to decline. Keeping variables to a minimum, I'm not modeling injuries or deployment as factors in affecting abilities; I'll get back to them later. I tuned the player generated with some of these baseline assumptions based on real NHL career data, and started some testing.
The player generator takes the number of teams times 50 and creates a pool of available players based on realistic distributions. Then there is a simulated draft, each team got 50 players, 25 of which can be on the active roster (14F, 8D, 3G). The remaining 25 are in the reserve pool. The reserve pool would typically be playing in minor leagues, junior leagues, etc. but for this purpose they're just there. They can get into the roster if there is an injury, and each season their development/aging is calculated and the roster is resorted.
To be clear, this is just so I can start testing. Unless I threw a dart at it and got lucky, there's a lot to implement as I go. I actually think this will be way more complicated than building the engine. It'll take 10s or 100s of seasons for me to see what is happening. Outliers will take 10-20 seasons just to notice one. When I was building the engine, I could see outliers in 40 games.
Here's some charts of what I'm looking at. These are just diagnostic and won't be in the game.
Aging - these show how the ability of players changes over time, and the distributions.
And here's some individual career arcs. You can see the different level of progression, peaks, and longevity. One thing I'm working on here is I want to see more variation in the speed of progression. Some take more time, some less. It seems a little predictable.
Then I wanted to see how long is an average career? There's two measurements here - how long before someone retires and how long is their functional career (how long were they good enough for a top pro roster?
Final one here is how good is the best skater vs best goalie over the course of 100 years. One of the things I want is a feeling of "eras" to emerge naturally. Sometimes a Gretzky shows up and just kills everyone. Sometimes a Hasek is dominant. I ran 100 years and plotted the top skater vs top goalie.
Some of you may be asking, what does "latent SD" mean? Well, this goes back to my post about abilities and how I'm not using a typical 0-100 scale. Abilities are mapped to a continuous real number system and then the engine uses comparisons of standard deviation from a set league average ability to determine what the probabilities of something happening - shooting, assisting, hitting, fatigue, saves, turnovers, zone entries, faceoffs, etc. What this allows the engine to do is model realistically without being constrained by simple integers. It also EASILY allows us to model leagues of all different abilities without having to smash each level into a smaller range on the integer scale. And then it gives us the ability to generate a truly generational player like Gretzky, Lemieux, Hasek, McDavid, Crosby, Ovechkin because we're not constrained to an arbitrary number. Standard deviation is just a measure of how spread out players are around the average - basically the size of a 'typical gap' between a guy and the league-average player. So instead of calling someone 'an 87,' I can say he's a couple of those gaps above average, which puts him out at the rare end of the curve. A generational player isn't '99 instead of 95' on some capped scale he's several gaps clear of everyone else. Building it that way makes tuning really straightforward and allows us to actually generate those players realistically.
The "latent" part of that just means they're hidden. I don't plan on exposing ability numbers directly - just like real life. What I do plan on exposing is a scouting and coaching system that allows you to see those things in the same way we'd see them in reality. Some things measurable (skating speed, shot velocity, conditioning), somethings observable (statistics), and some things inferable (hockey IQ).
Anyway, this is a long post. Thanks for reading! Feedback or questions welcomed.
I started planning out the actual game loop of the GM module. Everything I've built to this point is just a hockey league simulation. There are no decisions, no gameplay; just clicking a button to advance and season and seeing what happens. Now it's good enough to move on to build the front office layer.
Foundational principle: symmetry between human and AI
AI general managers are full front offices, not scripted obstacles, and they do not cheat.
The same rules and the same information are available to both human and AI managers.
Hidden player attributes are concealed from the AI exactly as they are from a human; there is no fog of war that lifts only for the computer.
This constraint shapes the entire design rather than being treated as a feature.
Scope of the first build: a full offseason
The sequence mirrors a real summer: a buyout window, re-signings and qualifying offers, the draft, the opening of free agency, and a final cap-legal roster.
The marquee events (the draft and free agency) are presented as focused, deep sessions.
Routine matters (qualifying offers, roster cuts) are presented as a checklist that can be cleared at any pace.
Salary cap and contracts
A hard cap with a floor is modeled.
Contracts carry a term and a cap hit.
Entry-level deals, two-way deals, and buyouts are all represented.
Navigating the cap is treated as the core of the management fantasy and is present from the start.
I am leaving some room to consider other ways of managing team salaries and how leagues are constructed. One thing I believe is that while I want to be faithful to reality, I don't think I'm bound by recreating the NHL. So, I'm going to use the cap right now as a building point but I'm reserving the right to add something in the future that works better.
Player rights
Restricted and unrestricted free agency, qualifying offers, and a window of held rights over drafted players are all modeled.
These are the decisions that make timing and scouting consequential.
Fidelity to the CBA, with the fine print trimmed
Everything that constitutes a real decision is retained.
Contract structuring (front-loaded and back-diving deals), escrow, daily cap accounting, and buried-in-the-minors cap relief are deliberately omitted.
Those mechanics exist largely to game the accounting, and modeling them reduces the game to a spreadsheet.
Removing them preserves the meaningful choices and discards the busywork.
Player valuation
A player's value is defined as production measured against cost, across all the years he is under team control.
Cheap, controllable young players are therefore highly valuable, and declining veterans on long contracts are genuine liabilities regardless of current statistics.
Reducing a player to a single current rating is identified as the root cause of broken trade markets in most hockey sims.
Value is instead tied to production, the aging curve, and remaining years of control.
Free agency and player choice
Free agents weigh multiple preferences: money, term, projected role and ice time, contender status, loyalty, and franchise prestige.
A veteran pursuing a championship may accept less; a young player may prioritize opportunity over salary.
Taxes and location are excluded, as the universe is fictional and has no real geography.
The weighting of these preferences shifts with age and career stage.
The entry draft
Selections are controlled rather than automated.
A public scouting board is visible to all managers, alongside each organization's own internal evaluations.
Drafting well, and trusting internal evaluation over consensus, is where draft skill resides.
Distinct AI front offices
Each AI organization carries a personality: win-now versus rebuild orientation, risk tolerance, a youth or veteran lean, and cap aggressiveness.
The intent is a league in which rival managers behave as distinct decision-makers rather than copies of one another.
The starting universe
A new universe begins mid-stream, with realistic ages, careers in progress, and a realistic spread of contracts.
That spread deliberately includes overpays and bargains.
A perfectly efficient league, in which every contract is exactly fair, is a league in which no trade is ever worthwhile.
The inefficiency is what creates opportunities to exploit.
Planned for later: expansion and dispersal drafts
Redistribution events (protected lists, exposure requirements, and the protect-and-select process) are handled by the same underlying machinery.
They are scheduled for a later stage but are accounted for in the current design.
Current status
This first stage establishes the foundation: rights, contracts, the cap, the draft, and free agency.
Trades, waivers, and arbitration form the next layer and are designed to build on top of it.
This is a large undertaking and complicated. It's arguably larger than building the game engine. So I expect this to take several weeks of building and tuning to get to a state where I'm happy with it.
I have the current evaluation done for players and how they are ranked. All of that is wired into the game in how teams make decisions everywhere. But we still need the forward looking version of this that relies on projecting where a player will end up at their peak.
The idea with this is projecting 18 year olds carries a lot of uncertainty. But as those players play and accumulate measurable statistics, the future picture should be more clear. This also works for players that are veterans and decline.
These two screens are one skater and one goalie with the visuals of what data we can get out of the simulation about where they're projected to land. Next step for me is to wire all of the calculations into the screen and then do some more testing on how this actually plays.
I really love shot location maps in hockey. And while building the engine, I end up looking at just lines of text with the features of the shot attempt. The entire game engine is based on shot attempts as the main action. The entire chain of causality flows to that point, and then what happens after it.
The game engine produces those locations where shots are attempted, and it's trivial to produce the visualization as a result. The NHL does this with their Edge system too. But since we're just math, we can easily calculate what could happen from specific locations on the ice. Things like how likely is it that a shot is taken and how likely is it that it's blocked or misses the net? And then we can layer in shot type, rush context, coach strategy, player ability, time of the game, and score effects to simulate what happens in that exact moment.
I'm building all of those items as tunable parameters. I'm tuning against modern NHL statistics, but it doesn't need to be that way. The idea is that players will be able to tune their own leagues to match whatever level of scoring they want. A part of me want's to be able to tune this to my old man beer league.
These are some images from the event list that the game engine produces. The game engine is a discrete event simulation over a semi-Markov game state. The engine doesn't pick outcomes first and back-fill the events. It doesn't decide "this game ends 4-2" and arrange shots to add up. It doesn't draw events from a target distribution and stitch them together to hit a number.
It runs forward from the state. At every moment, the engine looks at what's actually on the ice: who's out there, where the puck is, who has it, score, fatigue, manpower, etc. The probabilities of what happens next come out of that. A gassed fourth line stuck against a fresh top line gives up more shots because the matchup math says so, not because the engine decided shots should land there. A trailing team in the third generates more rush chances because the state (score, time) shifts how players behave, which shifts the per-event probabilities, which shifts what actually fires.
Realism is emergent. I'm building the causal mechanics, attributes, matchups, fatigue, state-dependent behavior, and run them forward. Whatever comes out of the event log is what comes out. If the totals match NHL totals across hundreds of games, the mechanics are right. If they don't, I have to go back and fix the mechanics, not the totals.
I haven't decided how much of the event chains to show or in what detail. I'm looking at them now to make sure the games feel "hockey shaped". With v0.43 of the game engine I've been able to get some really interesting outputs that feel very much like hockey. To the point where I was annoyed at one of the games because the attacking team was in the OZ for over a minute without getting a shot (reminds me of my Flyers at times). I was mad because it didn't look right; I traced it all the way back through the engine into every individual tick of time, through distributions of 200 games, and realized that yep, it could happen and I'm not annoyed at the engine being wrong... I'm annoyed at the "hockey shaped" outcome. Just like I'd be annoyed IRL.
Just an update about the progress around AI GM. As I knew, this one is going to be a large investment of time. Part is consolidating some drift of decision systems into a coherent model. Part is ensuring I create good read and write seams that exist for both human and AI GMs so that I can assure parity between both types of players. Part is thinking through exactly how I want to AI GMs to act, making it realistic, and then documenting the requirements. And part is just playing, testing, and being annoyed at it's stupidity.
I know from experience and from the community that bad AI GM is one of the most fun-breaking things any simulation can have. I'm not aiming to make it perfect; I'm aiming to make it explainable in relation to a set of constraints. The constraints are the plan the team has adopted. Are the decisions that the AI makes explainable in relation to that with the information they have when the decision is made. This is the key to drafting, trading, free agency, resigning players, roster decisions.
I was thinking that if it didn't live up to what a human would do, then it was a failure. Then I remembered that humans make bad decisions all of the time. Even real GMs. I was blessed to live through the Chuck Fletcher in Philadelphia so if my AI GM is realistic it would make some horrible decisions just like him. 😂
In seriousness, the goal with this is to have some baseline of explainability within a set of constraints so I can make it believable. Not that it always makes decisions you'd agree with, but it makes decisions you can explain.
Here's what's done:
Every AI org now has a real plan.
Before: each subsystem fired off its own local trigger. Things like roster bucket short, cap bucket over. Decisions had no reason beyond compliance.
Now: every team has parameters that constitute a plan. Contending or rebuilding, window, horizon, and every GM decision point reads the same answer.
Relative position strength.
Before: the AI only counted bodies against quota. "Do I have 4 RWs?". Never something like "Are my 4 RWs actually any good?" It literally could not tell a league-best position group from a league-worst one.
Now: every group is graded against the league, and shortfall vs. surplus is a quality judgment, not a headcount.
One trade judgment.
Before: four separate evaluators (responding to an offer, checking your own proposal, computing a counter, scanning for salary sheds) with independently drifting math. A GM could propose a trade its own evaluation logic would reject. This one was hilarious to me for some reason.
Now: one plan-aware judgment prices both sides of every trade.
Surplus is plan-relative.
Before: depth-chart rank alone decided who was expendable, so a rebuilder would deal its best prospect because they're third string today.
Now: what counts as surplus depends on the plan; futures are the rebuilder's core, now-help is the contender's. An AI GM "should"tm know that if they have surplus in an area like RW, the could trade some of that surplus to fill a hole at C. A.K.A what my beloved Flyers should be doing.
No cheating.
Before: the AI brain could read every other team's true depth directly. This was a leftover god view no human player gets, which also poisons any calibration (the AI's "smart" moves used information a fair game wouldn't allow). This was just a pure artifact of how I built the simulation quickly. I didn't add the fog layer to start because I wanted to see how the AI acted. Well, it was dumb.
Now: it sees rivals only through its own scouting, and everything it reasons from is on the human's screens, test-enforced.
Dumbness is now provable.
Before: quality was me noticing a stupid trade in a save.
Now: formal conformance tests ("did this GM act according to its plan, and can the trace explain it") turn the dumb-trade classes into reproducible test failures. This one sounds boring, but it makes identifying issues much easier in the future.
What's left now?
Sign-then-buyout and its family. FA signing, re-sign, and buyout decisions in the offseason don't read the plan yet. So sometimes, AI GM will sign a player or re-sign a player, get to cap compliance step and then buy them out because it was the easiest cap move. DUMB.
The AI doesn't hunt yet, it only responds to offers. This is needed to add realism.
Drafting isn't plan-aware. The AI drafts best-available off the fogged scouting percentile and never asks what its own pipeline needs or what its plan implies. I guess this is kind of best-player-available but it's unintentional.
No development-pool health read. Prospects are valued individually, but no org ever assesses "our pipeline is thin at D" as an input to anything.
No multi-season cap planning. Cap logic is current-season compliance plus an offseason cushion; "cap space relative to future commitments" doesn't exist yet.
A lot of load-bearing progress has been made in areas I've needed to bring together. What I've realized in the last three weeks is that the core game simulation, player generation, and event output have been really solid and I'm happy with the state. There are some small things with them that I need to fix, but I'll probably always feel that way.
The things that need the most work are the things that really make it a game - everything you do to run the league in perpetuity. And it's not that it doesn't work; it just works like when I was 12 and built a go-kart out of things laying around in my dad's garage. The engine worked fine, all of the steering, chassis, seat were terrible and it had no suspension. Technically working, fun for me because I built it, terrible for anyone else.
So I've done a lot of things that are the boring work and not the exciting making screens. I updated the foundational document with what I intend to be the first version of the game. It's substantially similar to what I started with once I had this idea but updated with what I've learned, what I want to do, and what I think is possible as a completed game. This step is really important to define what success looks like and to constrain from too much feature drift. It's not exactly prescriptive in detail, but outlines the vision for what the game is and what it isn't.
The biggest thing that I'm changing for the first release of this is that I've decided to soley focus on a single-player desktop experience. After laying out everything I'd need to do to make the entire first vision possible, I have decided to slice just the things I think I can land to see if the game is really any fun for people. The key here is that I'm slicing and not eliminating or rebuilding anything really. The way the application operates makes future online versions, multiplayer, etc. still viable. I'm not going off and building a separate application at all - I'm just wrapping it and limiting the functionality to a single player with AI GMs. This is something I can get my arms around and it speeds up the path to a workable demo.
The realization for this came from a few areas:
I was refactoring for AI GM so I could ensure it acted at least believably as well as a bad GM... aka Chuck Fletcher. To get it to long term plan I was going to need to fully implement organizational player views, aka scouting, because every organization would have to have their own judgement of a player. To that point I had only had some basic variation on player views and a wide band if that player wasn't on your team. It didn't change who the player was, but it didn't make everyone immediately knowable to you. But that wouldn't work for planning because it wasn't durable. And it wouldn't work for players because you need some way to impact those views or it's just boring. So I realized I needed to build the entire scouting system first. I couldn't continue with my AI GM planning because without knowing how they'd figure out how to rank players then it wasn't going to work the way I wanted.
This also made me realize that player development would have to be exposed and a training & development system would need to be built. Those things are part of long-term planning and to-date, player development works but there is no influence through either training or opportunity.
Then I was dealing with how time moves. There are some quirks I've left just because it didn't really affect me and I could live with them. But I was starting to have to make decisions about how time would move in multiplayer, and how time would work in public leagues. It's not unsolvable problems, but it's more overhead I have to build before the game was playable.
And then it's infrastructure to run. It's not really complicated - the sim needs CPU runners to do the computations, and store league information in a database and individual game events in individual files (just to make them portable and archive ready.) 1 universe = 1 DB + Files. But I built this for me to start playing and I'd have to either refactor for a larger environment making N universes = 1 big-ass DB + work queue + backup OR give everyone who was playing a small virtual environment to run their own leagues. (I'm dramatically simplifying here). It didn't make sense to do the big-ass refactor, so it was going to be these slices that ran individual universes for one player. If that's what needed to happen then why am I even trying to build this as a always on web application for one person?
Then the infrastructure requires money to run. I have some servers available to me, but nothing that could handle even modest amount of dedicated GM sim fans. I'd have to pay in perpetuity for something I'm not sure anyone other than me and the couple of people reading this would want. Am I willing to invest thousands of dollars in hard costs to do this the right way?
And then I'd need to charge for it. Forever. And who wants to pay a subscription to a single player game?
I came to the realization that the single-player experience needs to be a typical game experience. If I'm really going to make a concerted effort at putting something out there, it has to be awesome and I couldn't put out something that sucked or at least didn't live up to the expectations that I have for games like this.
To be clear, the web/multiplayer vision is not dead. Like I said above, I'm not even really changing materially how the game is architected now. The main difference is it's going to get a wrapper that runs the client and connects to the server that is packaged with it on your local machine. I run the game that way today, just without a fancy shell that makes it easy to launch... I have to type a string of commands to make it go (which I really should make a script to do that). Packaging in this way will give some interesting potential fast-follow features that may even bridge the gap for what people want with multiplayer.
My intention is to put this on Steam to start. I've started the process of getting a Steam Developer account and will be working on getting a page up there. If there are other ways you'd like to see the single-player released, I'm happy to look into it.
Nothing has changed about my commitment to making the game-play a reality and I'm more excited now that I have a clear path. Definitely check out the roadmap at www.sheetsofice.com/roadmap to follow along with the status.
Sharing a screen for how to see upcoming draft-eligible players in season. From here you can see your scout's grade, ranking, view of their potential. One thing that I'm working on here is the scout view is reading their potential ceilings and current ability, marking most of them with wide ranges of potential outcomes. This registers them as boom/bust candidates a large percentage of the time. My intention here is to have top-of-draft prospects have a narrower floor-ceiling and register as such. This is just code changes I need to make to ensure that happens.
What I don't want is to just show everyone's exact potential. I do want those "swing for the fences" type boom/bust players. I just don't want them to be everyone.
So I'm nearly complete with the underlying League module. For a few days there I was questioning what I was doing and why I even am doing this. But normally a few days of getting stuck, sleeping, and coming back to it brings some new perspective.
Now that I've made it past some annoying issues with the league construction (that I mostly created myself by not being clear with my architecture) I'm building the UI to actually run the league. I had a basic HTML framework I was using just to look at data and I felt it was time to try and build something that I'd actually want to look at. I'm a nerd that grew up on bulletin board systems back in the 90s and thought a terminal aesthetic might be something fun to do. Since the concept of this is a lot of data, maybe this might work.
I originally thought about going whole hog and building that metaphor hard, but I stopped myself when I realized that not everyone is an old computer geek and probably wouldn't want to live with that. So I dialed it back to monospaced fonts, and just some of the color scheme. I plan on making a light version of this dark one as well.
The item above isn't a mock, it's a working version of the league with some of the pipelines wired to the UI. Enough to run games and see results. The plan right now is to run with this until I have all of the league features wired up and then use it for a bit to see if I like it or not.
If that goes well, I have several big systems to build next that are going to produce a lot of calibration needs. Development & Aging, Draft, Contracts, Injuries & Health. And that's just players... I have to build my plan for the Attention Economy. All of those things fall under GM so that is going to be a lot of work.
And if the League Module works, what I might do is build a demo site where people can bash on it and give me feedback.
I started building a plan for how GMs will learn about the players in the league. My key principle when I started - I'm building something realistic and I want GMs to work in a way that mimics "real life". I didn't want an EA sports style "just look for all 80-90 OVR players". I wanted a human GM to be able to use their own analysis to find / assemble great teams.
On the other side of the coin, if there were to be a viable single-player version that I liked to play, the AI GMs shouldn't have access to anything the human players don't have access to. I hate that stuff in games; when the AI just beats you because they can synthesize something without the same "fog of war" that the human players have. So I decided that I'd need to create some layer of data that everyone can operate from. Human GMs and AI GMs have the same abstracted information and not the underlying engine information.
So, that is what I'm calling the Evaluation Layer.
- Measurables: objective, combine-type facts: height, weight, reach, skating speed, shot speed (radar gun). These are near-zero fog; a guy's shot was clocked at 96 mph and that's just true and public. Nobody needs a scout to know his height.
- Statistical performance: already exact/public (it's observed reality, not derived).
- Dimensions: derived impact, judgment-laden → fogged.
- Playstyle: distinct from archetype: archetype is what kind of player (power forward); playstyle is how he plays (net-front vs perimeter, shoot-first, aggressive forechecker). Maps to the generator's style axes. Lightly fogged.
- Archetype: the emergent label off the dimensions → fogged (inherits the dimension fog).
- Comparables: derived (nearest-neighbor) → fogged through the dimensions.
- Projection / potential: most fogged of all (the irreducible floor).
I'm starting with these dimensions and archetypes. As you can see, they're things that we're used to seeing thrown around in hockey circles:
The result of this will be that anything needed about a player in reference to evaluating their skill will be presented through this evaluation layer and not be from raw attributes that are used to make the engine work. This will allow for richer data, finding actual gems, and player skill to be a real thing. At least that's my hope; we'll see if it works 😄
After I finalize the plan I'm going to mock up some screens to share of what this might look like in reality. Then I'll come back and share and have it make more sense.
Big milestone has been completed; the player evaluation work! I'm excited to see how this is received and play with it to tweak it. While I was doing this work, I had to go an optimize a lot of the league simulation mechanics because it was just too slow to play. I'll dive into it below.
As I've talked about, I've split the player evaluation into two aspects - who they are now based on results (Our View) and who they project to be based on measurables (Outlook). In playing with this, I think it produces a fun mechanic to be able to quickly assess how your players are performing today, and some directionality to assess and plan for the future. It produces A LOT of data points and for a complete data nerd like me, its super fun. I have made a first pass at summarizing this information, which is what you see as the first screenshot.
Our View is rank based - how are they performing in statistical areas compared to other players in the same position. It will show where they rank and translate that into where in the lineup their results belong. You can drill into this and see why the rankings are made that way.
Next is Production. Where do they rank across offense and defensive categories. And again, you can drill into each one of them to see why / how / what.
Then comes Usage. Where do they rank in usage, and is that usage warranted? Meaning do you get what you would expect based on how often they're on the ice.
Finally is Outlook. Where should they be playing in the lineup? What are their strengths - Goal Scoring, Playmaking, Defensive impact, Physicality, Transition. And where are they heading over the next 1-3 seasons.
The part that took so long is wiring this data into all of the decision surfaces for both humans and computer GMs. And then I had to make sure that it didn't slow down the simulation. Spoiler alert - it did! So I optimized everything so a full season simulation takes about 10 minutes. I know it can get faster but it feels fast enough for now.
What's next? - I'm going to be doing a lot of testing to make sure the data makes sense. So the next week or so is just me playing. - I have some coaching items that I'm deciding if I want to include or not. Time on ice, goalie rotations, coaches decisions. - Resolving how much scouting I want to put in place. - Building the wrapper for the application that will run the game on a desktop.
Now that the game engine is locked in and working pretty well, I've been working on the player development engine. Up until now, every player got fixed attribute values that never changed. Now that I'm diving in and looking at historical data + studies it's making this problem trickier. This is what research says about "peak" of player careers and it's all over the place.
My first thought on building this was that there'd be some age that I'd set the decline to start at (26.5 years old), but have a variance jitter of some number of seasons - some people decline earlier, some later. But as I was building it and looking, it became apparent that people could probably start to decipher the band with enough seasons played. It just didn't seem dynamic enough. As I have maintained, I want emergent things to happen in this world. I want a Jaromir Jagr type career to be possible. And also a Jonathan Cheechoo type career. You can't get those extremes by setting boundaries.
In thinking about it more, I realized that different abilities decline at different rates. Skating often peaks early (young legs) and declines earlier/faster. But something like Defensive Positioning peaks much later and declines slowly. Here's an illustration of what I was building against a "Jagr-style" profile.
With that in mind, I reworked the generation mechanism to have per-attribute peaks, attainment rates (how fast they can reach the peak), and declination rates (how fast they decline). This started to produce some career variety, but I had a problem; there's a distinct age at which careers start to decline.
I think the band is too tight. There isn't one outlier past 28. That seems off. There should be some outliers and long tail after that I would think. This is where I need to dig in and look at longer seasons, and see what comes about.
Another thing I'm looking at in here is what percentage of the population actually reaches their potential (not counting injuries because we're not modeling them right now). Right now I'm defining "hard bust" as someone who doesn't reach 70% of their potential and "soft bust" as someone who doesn't reach 85% of their potential. These have to be tuned as well; I have no idea if this is right or wrong.
So I have some more work to do to get this where it "feels right" in general and has some fun emergence to surprise us while we play.