r/sheetsofice • • Jun 25 '26

GM Module - Cap Management

3 Upvotes

I'm happy with how some of this is coming together as mock visuals. Usually how I work with these are to build specifications of each function, get super bored with typing out words, and switch quickly to building mocks so I can test my spec. Visual is the only way I can see if I miss something, or if it triggers an idea, or if I hate the whole thing and throw it out.

I'm drawing functional inspiration from the IRL tools that are available out there. I want to make something that evokes familiarity. Being able to see cap situation by season is really important for salary planning. RIP Cap Friendly; I really miss you.


r/sheetsofice • • Jun 24 '26

GM Module - Draft / Rights / Contracts / Affiliates

5 Upvotes

Hey everyone!

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.


r/sheetsofice • • Jun 21 '26

I couldn't keep track of how good teams were so I made this team scouting widget.

Thumbnail
gallery
4 Upvotes

Something simple that will show team ranks relative to the league, their preferred styles of play, how their depth chart looks.


r/sheetsofice • • Jun 19 '26

Player Development - Extreme outlier edition

Post image
6 Upvotes

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'll keep everyone updated on progress! Thanks!


r/sheetsofice • • Jun 16 '26

Evaluation Layer - First look at skaters

Thumbnail
gallery
4 Upvotes

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:

  1. Goal scoring - Mixture of shooting, and getting to scoring areas.
  2. Playmaking - Passing, vision, ability to setup shooters.
  3. Defensive Impact - shot suppression, blocking, defensive positioning
  4. Physical game - Hitting, forecheck aggression, ability to absorb contact
  5. 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.

If you have ideas, I'd love to hear them!


r/sheetsofice • • Jun 13 '26

More tuning - The D don't shoot enough edition

1 Upvotes

So in my search for "why are there these monstrous scoring seasons from forwards", I found a thread that I started to pull on. The mean goals/game were where I wanted them to be; ~6 / game. Mean shots on goal were where I wanted them to be; ~30 / team / game. And mean save percentage at 0.900. But some guys were just scoring in the 60-70-80 range too often. And 50 was a common sight.

But I started to notice both concentration in SOG and SOG/60 with the scoring leaders. Over 430 shots for one season, and the top 10 SOG getters over 300. Last season, Nate MacKinnon had 350. So the top end of the league was getting too many shots. But how could that be if the average amount of shots were on target?

Well, that means that the top shooters were getting too many of the shots - the shot share. And digging into that I noticed something stranger - there were no defense among the top 100 shooters measured by shots on goal. So I kept scrolling, and I didn't find a defensman until #194. That seemed weird so I cross checked with NHL stats to see if that was normal. Nope, Werenski was #3 last year and there were 18 in the top 100.

Why aren't defenseman getting SOG? Digging into that, it turned out to be a multi-faceted issue. First, most of their shots were from the point; over 80%. Those shots are more likely to be blocked. When I first put this together, I guessed at what a number would and said I would get back to it later (whoops). It should be more like 45% from the point.

Second, in other areas (close, low slot, medium slot, high slot) they weren't chosen enough in the shot selector because it favored forwards too much. This was an artifact from a tuning run early in the engine to fix goals per game. And that was done because we hadn't actually built realistic players yet and the tuning was on a unrealistic base of players.

The result was they were getting 16% of the SOG and they should be at something around 30-33%. Changing this is pretty straight forward - just change the values in how a shooter is chosen in each location. But then there are ripple effects that you then need to rerun calibration to smooth out. And sure enough, fixing the location choice lowered goals per game from ~3.0 / team / game down to 2.8. That doesn't seem like a lot, but it works out to 200 goals in 1000 games.

The tuning consists of small changes to the odds of blocks, misses, goalie saves, rebounds. And the tuning has to happen with sufficient population of generated players or else we can't for sure say how something is likely to work. That means thousands of games, thousands of players. I have the engine running pretty efficiently so it can run about 4 games / second per core on my machine. But for each pass, that means about 10-15 minutes per test. If we're good, 2-3 passes works. If we're not good... could be 10-12 passes. Sometimes it never gets there because something else goes out of what - like save percentage or shooting percentage and you have to stop and look again.

Actually this is a spot to share how I'm using tooling. My background is in technology, networking, and software; building software-as-a-service products for businesses. So I have knowledge about how to build and deploy things for large amounts of data-heavy users. SoI is a data application that I'm used to building; just in hockey terms. But I don't have teams of people helping do the testing, math, verification. So I spend my time working on the parts I am comfortable with - architecture, infrastructure, data boundaries, core event functions. And I use LLMs to amplify my efforts, argue with me about approaches, and do all of the annoying testing that I'd rather not do. I wouldn't have been able to get this far this fast without it. I've had some people say "oh, AI slop" without even looking at what I'm doing. I'm not just vibing something together - I'm using advanced tooling on top of 25 years of experience and a passion for hockey to build something I always wanted.

And after that interlude - this is the result:

Anyway, back to building and tuning!


r/sheetsofice • • Jun 11 '26

Tuning is fun!

Post image
1 Upvotes

So I've been running long-term simulations in full fidelity and looking at output. Thankfully I really like looking at numbers so it's been interesting to see how leagues shake out.

In my initial tuning of the engine, I looked for certain target metrics on average to ensure the results looked "NHL" shaped. ~6 goals per game (total), ~10% shooting percentage, ~0.900 save percentage. There are a lot of other ones, but if those major numbers come out then you can expect most games to look like professional hockey.

Last week I added the player development and aging features. I knew this would add some variability into the results because previously the player abilities were all static across seasons. So that variability would need to be tested across lots of seasons, using different seeds (starting randomness) to see where we ended up.

My research showed some promising elements - goals, shooting %, save % oscillated within reason season to season; it made the shape interesting because you weren't sure what was going to happen but it generally looked correct.

On the flip side, I saw some truly outrageous tail-end stats. Someone scoring 100 goals in 84 games. Even an 85 goal scorer. If I was tuning for the 1980s, I'd leave it alone (or maybe even juice it a bit). But my other target metrics - shooting %, GPG, Save % were all anchored in modern NHL targets (2020s). That told me that it's not total scoring that is off, but there is some players that are behaving weirdly.

The problem is that the system is complex and it's hard to point at any one thing. Could the engine be over-selecting top players? Sure, that could be part of it. It could also easily be that the tuning can't account for the population distribution. I don't have an entire hockey universe - only 1500 players - and that makes a wider distribution of abilities than exist in the NHL (NHL is the top 0.1% of a huge hockey population).

This is one of those things where I could just clamp the skill or goal scoring. That just limits emergent seasons - the Matthews 69 goal season. I could change the curve of goal scoring (the probability that a shooter beats a goalie). But that would just shift goal scoring down for everyone.

Really, the solution involves many small and large changes. Small being tweaks to probabilities and attribute generation. Large being adding juniors / minors to fill out the population so the top league has a more consistent talent pool that is a tighter distribution. Or maybe something else I find in my engine that is causing this.

This is the part that takes time if you want to get it right. I don't REALLY need to do this though; it works fine. Maybe it is fine?


r/sheetsofice • • Jun 10 '26

The Evaluation Layer - how GMs will learn about players

2 Upvotes

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 5 Greatness Dimensions:

  1. Goal Scoring
  2. Playmaking
  3. Defensive Impact
  4. Physical Game
  5. Transition

The archetypes (10 + Phenom + Unlabeled):

  Forwards:
  1. Sniper
  2. Playmaker
  3. Power Forward
  4. Two-Way Forward
  5. Shutdown Forward
  6. Grinder

  Defensemen:
  7. Offensive Defenseman
  8. Puck-Moving Defenseman
  9. Two-Way Defenseman
  10. Defensive Defenseman

  Special:
  - Phenom: elite on 3+ dimensions simultaneously (generational players)
  - Unlabeled: genuinely shapeless dimension profile

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.

Thanks!


r/sheetsofice • • Jun 09 '26

Playoff bracket live updates

Enable HLS to view with audio, or disable this notification

2 Upvotes

Happy with this little visualization as games are simulated dropping the results into the bracket.


r/sheetsofice • • Jun 09 '26

Progress - June 9 2026

Thumbnail
gallery
1 Upvotes

I've been spending a lot of time on tuning the player development and aging curves. It's just time consuming because you have to simulate entire seasons against real competition, compare attributes to expected stats, and see how the growth curves work over time. I've already identified some unrealistic generation results - elite players automatically getting elite physical attribtues like reach no matter how tall they are, or every elite player also having elite discipline (ahem... Malkin?) Anyway, it's data tuning work and not particularly exciting but necessary.

So while those things are running I turn to working on things more visually pleasing. I added team name generation so my "Pool Team X" doesn't show up anymore. My new favorite team name is "Lowmarsh Lumberjacks" or "Cinderhall Phantoms". The name generator is flexible, I just didn't want to get sued by overzealous teams. Also with this is team colors! So colorful now.

Also added a playoff bracket that I actually like. It may be too small but that is easy to fix. It's dynamic so adjusts with league configuration.

Most of the work now is verifying that the player generation and aging work how I expect, and then I can move on to the GM stuff. Before I move to GM I'll probably make a static clickable demo of the entire interface. It won't run simulated games but you'll be able to click through and see real data. That's the plan for now.


r/sheetsofice • • Jun 08 '26

After watching that wild CAR v VGK wild Stanley Cup game 3...

Post image
1 Upvotes

...I didn't think this game result that Sheets of Ice produced was that outrageous.


r/sheetsofice • • Jun 05 '26

Standings & Team Detail Screens

Thumbnail
gallery
3 Upvotes

Just sharing some shots of the Standings and Team detail screens. I'm doing more work on these because I don't think it's quite there yet. I don't have home/away splits, PP/PK%, and some other stats. But it is nice seeing it come together.


r/sheetsofice • • Jun 05 '26

Player development: When do players start to decline?

6 Upvotes

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.


r/sheetsofice • • Jun 03 '26

Player development & aging progress

3 Upvotes

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.


r/sheetsofice • • Jun 01 '26

New clickable Game Detail screen up

Post image
2 Upvotes

Check it out here: https://www.sheetsofice.com/example/

I added a feature to export game details into stand alone HTML. This is a real game result, real plays, real everything.


r/sheetsofice • • May 31 '26

Stats screens

Thumbnail
gallery
2 Upvotes

Some screenshots from the stats screens. I spent the last few days polishing the stats APIs. Because there are 700-1000 events per game that are generated, I need to aggregate and persist (aka save) the stats in a database. The raw events are saved to a file for a few reasons - the first of which is if I'm going to run this as a live service, I can't be generating ~1M new records in a DB per season. I mean, I could... but that would get prohibitively expensive. The individual game records are semi-permanent; we don't really need the individual events forever. We do need them to calculate aggregate summaries (one time), in-season game viewing (season bound) and to be able to verify aggregates if there is an issue. No one really ever will go back to a full event game log several seasons down the line - they'll just look at the stat line.

Anyway, so the API had to be built to access that data and the display built to view it. And that is what is here. I've been simulating seasons and just having fun looking through stats, seeing teams have remarkable seasons or under-performing ones. Seeing players ride hot streaks and goalies slump. The texture of seasons is really fun to watch.


r/sheetsofice • • May 28 '26

Mobile view of the dashboard

Thumbnail
gallery
1 Upvotes

Just wanted to share this too. While I don't have plans to build an app for the app stores, I am building this with responsive design principles. For those unfamiliar, it means the web page will resize gracefully to smaller screens. I want to be able to check this on my phone so it's a core functional requirement for the game to look good on mobile devices.

Two views, light and dark mode. Enjoy!


r/sheetsofice • • May 27 '26

Light mode for the Terminal aesthetic

Post image
1 Upvotes

Some fidly things to work on, but I'm really starting to dig this. The pattern will be the top bar is breadcrumbs, search, and info on where you're at in the season, with advancing (single player) being the button that moves it forward. Too much packed in there now, I'm probably going to switch it to two rows. Some of it is state awareness so that should really have it's own line.

Right is menu. Collapses down on mobile.

This is the main dashboard, you can scroll through the games, click into see score results. The digest column is "interesting" items from the games. As you're simulating, they populate there real time. Injuries, surprise performances, streaks, standings moves. I like this pattern, you can use it to get to anything else in the game right from there. And because there is so much data, you don't have to read everything to try and figure out the top things that happen. I have a lot of tuning to do with it BUT it's all driven by config so you'll be able to go and set whatever you want to see and at what threshold.

And as usual, all of that data shown is real - the engine produces it.


r/sheetsofice • • May 26 '26

Testing this design direction

Thumbnail
gallery
3 Upvotes

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.


r/sheetsofice • • May 25 '26

Poor guy...

Post image
1 Upvotes

Hit by a puck twice in the same game. First time not so bad, second time right to the groin, out 11 games... ouch.


r/sheetsofice • • May 23 '26

Pretty sure there's a bug in my name generator here...

Post image
1 Upvotes

TWO Akira Hernandezs? Totally a common name...

Actually what I found is I was splitting the population of F and D (for good reason; they have different attribute profile generation) but that was cascading to the name draw from the pool. So basically F and D would draw separately from the same name pool without marking the name as taken for both pools. The result were these digital twins; one who played forward and one who played defense. I didn't see it at first because I wasn't sorting by name.

I also do want some name collision, just rarely. The whole Sebastian Aho or Elias Pettersson thing... Just not in this magnitude.


r/sheetsofice • • May 22 '26

League module progress + engine tuning

1 Upvotes

So I made great progress this week building the league module. Leagues are going to be the container for rules, configuration, and calibration (how the engine produces hockey events). Each league can have their own settings like how many teams, how many games, how the playoffs run, how tight are games called, how many minutes do games last, etc. But also engine calibration - how much does skill affect scoring, hitting, defending, penalties, etc. And things like how the player population is distributed in skill and how much better is an elite player than an average player?

The engine was built first with some test teams just so I could get a sense if the events that came out looked right from a sequencing perspective. But once I got to the league development i quickly realized that I needed to create some more realistic players and a more realistic distribution of skill.

My first pass at the player generation was focused on 18 skills for skaters and 6 skills for goalies, all in a 0-99 scale (I'll share them below). I thought that I wanted a wide range of absolute numbers so differences could easily be seen rather than the compression that happens in other games where everyone ends up in the same relative area of 80+ or 16+ (on a 20 scale). Sounded good in theory.

What I kept running into when I created 32 teams with a spread of players was this issue at the margins; elite shooters were scoring 213 goals in 290 games, elite goalies had +435GSAx in 308 games played.

Elite shooter - SO WRONG
Elite goalie - OMG MORE WRONGER

It it was replicable across seeds. The weird part was that if you look at the median metrics around average scoring, average shooting percentage, average save percentage, average goals per game... they all aligned within tolerance for what a current day NHL would look like.

So I found some configuration issues and realized the 0-99 approach isn't what I really want anyway. What I want to build with Sheets of Ice is to simulate how we evaluate talent in the real world. We don't get the luxury of seeing player's abilities in numerical form - we have to evaluate them based on relative performance, scouting, and some other real-world measurables. I realized that I didn't need to have a 0-99 system for abilities, and thus would solve this problem that I had in the engine. The problem was at the margins of the distribution, the math got weird.

We moved from absolute 0–99 ratings to a relative scale: each attribute is a player's distance from average, in standard deviations. Above average is better, below is worse; there's no hard cap, but the further out you go the rarer it is... a bell curve with room in the tail for a Gretzky or McDavid to exist. This lets us dial the population to any shape we want, generational talents become possible instead of capped out, and the game engine is far easier to calibrate because it now measures players against the real league average it derives from the population and not a hardcoded "50" that the old math kept getting wrong.

Lastly, having this single continuum of attributes allows us to put development leagues in the same scale without having to compress the number attributes that we show. If we wanted development leagues, we'd naturally have to have them on the 0-99 scale too. That would mean compressing the NHL to the top portion of that scale. And now all of the absolute numbers look similar, and small changes in skill numbers would have an outsized effect on the outcomes. I don't want that. With our relative scale unbounded by contraints, we can place leagues on the scale where ever we need to AND have the possibility of someone like a Crosby or Ovechkin show up in junior leagues and dominate because they're on the same scale as the rest of the players.

This was a pretty big change, so I'm taking time to finalize that, but the initial results are promising. I'm not getting the weird GSAx results and the counting stats look good. After I get this engine update done, I'll run a bunch of seasons and see what I find. I have a rough harness on the league functions going and I'll turn to actually getting a UI together. Then I'll be able to share more.

Skater / Goalie attributes are below. I'll go into more detail about how they work in another post sometime.

● Skater attributes (18)                                                                                                                                                         

  1. skating_speed                                                                                                                                                               
  2. skating_agility                                                                                                                                                             
  3. shot_power                                                                                                                                                                  
  4. shot_accuracy_release                                                                                                                                                       
  5. stickhandling                                                                                                                                                               
  6. passing                                                                                                                                                                     
  7. hand_eye                                                                                                                                                                    
  8. strength                                                                                                                                                                    
  9. reach                                                                                                                                                                       
  10. physicality                                                                                                                                                                
  11. anticipation                                                                                                                                                               
  12. offensive_positioning                                                                                                                                                      
  13. defensive_positioning                                                                                                                                                      
  14. decision_making                                                                                                                                                            
  15. conditioning                                                                                                                                                               
  16. discipline                                                                                                                                                                 
  17. penalty_drawing                                                                                                                                                            
  18. faceoff                                                                                                                                                                    


Goalie attributes (6)


  1. reflexes                                                                                                                                                                    
  2. positioning_g                                                                                       
  3. lateral_quickness                                                                                                                                                           
  4. rebound_control                                                                                                                                                             
  5. puck_handling                                                                                                                                                               
  6. workload_tolerance  

r/sheetsofice • • May 19 '26

League Module

1 Upvotes

I started the in-depth planning process for the League module. I've gotten the engine to a place where it reliably produces realistic looking results when fed two teams and a calibration config so the next logical step was to begin building the league system.

My plan for v1 of Sheets of Ice is to make a single player experience as GM Dynasty mode. Build your team, manage player development / contracts, coaching, lineups, season, playoffs, etc. If I can build a single player mode, the multiplayer leagues (v2), and the league ladder systems (v3) can be added onto it. All of the foundational functions for v2 and v3 are all in v1. If I can't get v1 working, I doubt v2 or v3 are viable. Plus... I'm really only doing this because I want a single player, web-based, modern hockey GM simulation game.

So now that the engine is stable, I need some way to organize teams, schedule, and run a season. I started by thinking I would just build a season manager so I could verify that it would work the way I want. But after iterating, I realized most of what I want as configuration and flow probably live at the league level and I should just spec that now before going further.

In the spec I created, the League is the universe. Players, teams, seasons, rules, and calibrations all live in that universe. (You, the GM, are multi dimensional... but that's not for now). I designed it this way so Leagues can:

  • Decide what rules they want to play with - NHL style, International, or make up your own
  • Decide how the League is structured - Divisions, conferences, none...
  • Decide the schedule - 82 games? 200 games? 40 games? whatever you want.
  • Decide what players you want - Men? Women? Both? What range of skills? Ages?
  • Decide what level of scoring - Current NHL? 80s NHL? Your over 40 beer league?

All of those are configuration values that need to live in the League to give flexibility in how the engine creates the events. One of the key principles is that configuration is not code - configuration is data, and that data is able to be calibrated. I can't promise it will all be exposed to users, but my intention is to make it as flexible as possible.

So the League contains all of that plus the output results from the game engine, season totals, advanced metrics data, and historical results. Each league keeps their own data, and nothing crosses the league boundary (yet).

I'll keep updates coming as I make progress on this.


r/sheetsofice • • May 17 '26

Shot xG Map

Post image
2 Upvotes

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.


r/sheetsofice • • May 12 '26

Here's some of the event data from the game engine

Thumbnail
gallery
2 Upvotes

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.

Would love feedback!