roncli.com blog
The blog of roncli
roncli.com blog
roncli.com
blog
Profile
roncli
Houston, Texas, United States
Labels
Coding
CTG Music
Editorials
Games
Miscellaneous
Music
Servers
Silliness
Software
Sports
Trax in Space Beta
Weather
Recent Posts
WTF did Revival Productions just do?
A Tale of Two Communities
The Final Stretch
A Two Tiered, Untiered OTL
Secretly, you wish you could've done what I did
What have I done since roncli.com v2?
It's Done. It's Finally Done.
The Big Picture is Starting to Wear on Me
A Low Bang to Buck Ratio
win-acme
Archives
February 2005
March 2005
April 2005
May 2005
June 2005
July 2005
August 2005
September 2005
October 2005
November 2005
December 2005
January 2006
February 2006
March 2006
April 2006
May 2006
June 2006
July 2006
August 2006
September 2006
October 2006
November 2006
December 2006
February 2007
March 2007
April 2007
May 2007
June 2007
July 2007
August 2007
September 2007
October 2007
November 2007
December 2007
January 2008
February 2008
March 2008
April 2008
June 2008
July 2008
September 2008
December 2008
February 2009
July 2009
August 2009
September 2009
October 2009
November 2009
February 2010
March 2010
April 2010
June 2010
July 2010
August 2010
September 2010
October 2010
November 2010
December 2010
March 2011
June 2011
July 2011
August 2011
September 2011
October 2011
December 2011
January 2012
February 2012
April 2012
July 2012
November 2012
July 2013
April 2014
July 2014
August 2014
November 2014
December 2014
March 2015
April 2015
May 2015
June 2015
July 2015
September 2015
January 2016
February 2016
May 2016
July 2016
November 2016
March 2017
January 2018
May 2018
June 2018
January 2019
January 2021
February 2021
March 2021
August 2021
October 2021
December 2021
August 2022
November 2022
October 2023
February 2024
April 2024
Monday, February 12, 2024
A Tale of Two Communities
Posted: 3:01:00 AM 0 comments
"If you're a high visibility player, it's on you to move the community in a positive direction." -Jonas Neubauer, 1981-2021, seven-time CTWC champion


This quote by Jonas Neubauer has been engraved on the Classic Tetris World Championship's first and second place trophies since 2021, the year he unexpectedly passed away at the age of 39. I met Jonas and his wife Heather, a Dr. Mario world champion at TwitchCon in 2019. We both happened to cross paths at GDQx 2019. I was doing couch commentary for SpootyBiscuit's Cadence of Hyrule run while Jonas was doing couch commentary for Blink's Tetris 99 run. I got a big hug, a photo I'll cherish forever, and I introduced Jonas and Heather to one of the exhibits on the show floor, which was basically a mind-meld Tetris game. I remember the two of them enjoying themselves and having fun, and their laughter was contageous. Jonas was one of the few people I've met who, online, was pretty much the exact same person he was in real life, one of the most genuine individuals I've ever met. He was graceful in his CTWC defeats, and always worked to build up the community he championed.

Since his last win, there have been 4 other tournament winners. Joseph Saelee won twice. Then, Michael Artiaga (dogplayingtetris), won in back to back years. Eric Tolt (EricICX) defeated Justin Yu (Fractal161), the latter of whom won it a year later. Harry Hong also famously won the tournament back in 2014, interrupting Jonas's seven year streak.

All of these players have made a positive impact on the classic Tetris community in some way. Some have even made contributions to the community, hosting their own events, doing content creation, and genuinely being a good ambassador to the game. I had no idea that one side tournament at SGDQ 2018 would get me involved in a community amazing enough that I now find myself contributing my time to almost every classic Tetris organization. Oh, I suck at the game! But I don't let that stop me from being involved in organizing events, commentating for matches, and playing in the lower tournament levels myself, even if I have taken a bit of a step back from competitive lately to keep my competition anxiety in check.

Sure, Tetris has its bad spots. Players cheat. People get upset at others. People do tasteless things that get them banned. It happens, because people suck. But far and away, the classic Tetris community is wholesome, and I am better for being a part of it.

If only every community was like Tetris.

In the fall of 2021, a player showed up on the Overload Teams League team SpaceCowboys by the name of Zor. He was a pretty decent player. Not quite top 10 in a league of some 60 pilots, but someone seen as dangerous to play against. There was a lot of secrecy around this player. Teammates would often go on mute when talking about Zor's past experience with 6DoF games, or just outright not stream. Someone caught a stream and quickly identified the voice as the Descent pilot - and former OTL pilot - Jediluke. With a DCL record of 5982 wins and 348 losses (1413-117 vs Rethink, 809-33 vs Drakona, and 476-24 vs b2af), Jediluke wasn't just a known quantity, he was the most active competitive player while the DCL was around, between 2013 and 2020. And, as the record indicates, he was pretty good, too: he was ranked first at the end of more DCL seasons (7) than anyone else (Mark392 5, Souljah 3, bahamut 1). What's more, he is unquestionably the most successful D1 pilot on my Twitch series The Observatory, a quarterly tournament open to all Descent 1 pilots where games are broadcast and commentated on, something the community never really had prior to this series, which was founded in 2016. To date, he's won 31 out of 34 seasons, losing only to Mark392 (season 5), Zero (season 12), and Morfod (season 26). We'll talk about Morfod's winning season a bit later. Unfortunately, Jediluke's also got a temper. He gets mad when he loses, and winning your first game over what seems like an impossible opponent usually results in him making you feel like shit, rather than celebrating your momentus victory.

For comparison, here's Jonas's last game at a live CTWC where he was shockingly eliminated in the first round.

Anyway, back to the point, Jediluke decided to compete in the OTL as Zor to "get away" from his Jediluke personality. Thinking it would put less pressure on himself, he adopted the alias and kept his true identity a close secret for a couple weeks. The voice changer was pretty silly, but once the word got out that Zor was Jediluke, he posted something quick in the OTL Discord explaining the name, and barring some critique from some old rivals, that was the end of it. The general vibe was like "yeah, it's weird, but he's got less of an attitude now, so whatever."

Also towards the end of 2021, season 26 of The Observatory occurred. An unknown entrant with the name "Genevieve" (PwnJett in game) showed up for the Finals Tournament as an unqualified player who had to fight from the wildcard anarchy to even get into the knockout bracket. He did so in shocking fashion, taking out Jediluke in the semifinals and Sirius in the finals to win the tournament. And, as the tournament concluded, Morfod revealed that he was in fact Genevieve. It was amusing at the time, and everyone got a good laugh. He had only played under that handle for a couple weeks, enough so that some regulars were aware of the handle. For me, I was kind of kind of amused, but also kind of annoyed that someone would go to such lengths as hide their identity just to gain an advantage. I let it slide, hoping it wouldn't happen again.

Um, remember earlier, when I said that people suck?

Season 35 of The Observatory kicked off this month, and with it Jediluke proclaimed his retirement, supposedly tired of not having a challenge after Mark392 showed up for about a week to play games and then left to become a parent. While Jedi's lack of participation didn't bother me - a more competitive top of the table would benefit the show - a newcomer, Tonka, showed up who had been around since August. Code, a long time Observatory player, did some training with him, and Tonka's skill began to noticeably rise. By the time season 35 began, it was clear that he would be a strong contender, and did in fact win all of his games in the first episode, including against season 34's runner up, terminal.

But something didn't sit right with some pilots, and eventually it was made known to me that he was suspected of cheating. Although there wasn't anything conclusive, and it was later decided that no cheating was involved, it got enough people suspicious that for the second episode, his IP was taken from a game log and compared with some website logs. I noticed that there was a lot of activity with that IP on the Overload game browser that didn't match the player's activity in Overload, which was very low and many months prior. I then decided to compare the IP to logs on the game servers themselves, and that's when the smoking gun was found. That IP was the same IP used by Zor in Overload.

While I had 100% confidence at that point that Tonka was 31-time champion Jediluke, a lot of things didn't add up. This player Tonka showed up in the community about 6 months ago. They were trained on basics like trichording. They had matches with Jediluke. They gave a phone number with a Houston, TX area code. They would chat back and forth with Jediluke, aggressively looking for games. Even now, this whole scheme doesn't make sense to me.

But it was real. Jediluke eventually confessed to the whole thing. And he was totally nonchalant about it. In fact, my current co-commentator, Chillybus, thought it was crazy that he would go through the whole thing. He said on Discord:

"for six months? what do you have to gain against pilots who would take hundreds and hundreds of games to even be competitive with you, let alone take a game? multiboxing discord accounts? faking tracker games? IMO you did competition a disservice and your friends dirty from the sound of it, just chasing shadows of the afternoon someone aliased you to a win. not how a longtime decorated top competitor should compose themselves. it isn't time to go around again, I think it is time to log off. you have shamed yourself here. That's all I'll say in this time and place."


The reply from Jediluke was equally nonchalant: "1 v 1 Chilly?" and "those who don't compete ...commentate".

Is this moving the community in a positive direction? Does he care at all about the community? Is this how a top player is going to treat the community that he represents?

This is literally night and day. In one community, I have players that are leaders, care about the community, and do everything they can to move the community forward. In another, I have players that are selfish, care about themselves, and do everything they can to try to get ahead.

You tell me which community I'm going to put more effort into.

Labels: , , , , , ,

Wednesday, October 11, 2023
The Final Stretch
Posted: 12:40:00 AM 0 comments
After much stalling on my part, the Overload Game Browser has finally migrated away from Windows over to Linux. This now makes 4 out of the 5 websites I wanted to move off the Windows box completed, leaving one last beast: the Overload Teams League.

This means I'm on the final stretch of a project that's gone on for almost two years now. It definitely feels like the light at the end of the tunnel. Now I just need to figure out what that light is.

The first thing I want to do after all this is done is give The Observatory its own website. I haven't updated anything having to do with it on this site in a long time, and the database I was using for it is massively out of date. In addition, I need to recreate the bot that was used for it so that the show runs much smoother. That will take time, however, and I'll be lucky to have it done by Season 35.

After that, who knows? I have a number of ideas on what comes after that, but haven't made any solid plans for anything yet. There's so many possibilities that I don't feel like I can even choose something until I finish up everything else I'm working on. I feel like I have an ocean of possibilities in front of me, but no clear direction to swim in. Yet!

In the meantime, a lot of other things are falling into place for me to resume a regular streaming schedule. More on that to come later, but I have a number of exciting projects in the works for the stream that I will be putting out there soon.

Labels: , , , ,

Tuesday, March 07, 2017
Observing the Swiss
Posted: 2:29:00 PM 0 comments
Running The Observatory is super fun. Being a self-proclaimed 6 degrees of freedom junkie, watching pilots pull off incredible moves just to avoid taking an extra 10 points of damage here or there is fascinating.

The format of The Observatory is largely inspired by CoNDOR, the racing league for the game Crypt of the NecroDancer. Over there, they have their main commentator and several referees who work in the background to ensure that everything goes smoothly. However, on The Observatory, I am afforded no such luxury.

Therefore, I've tried to automate as much as I can to ensure that I can spend time focusing on the action rather than on administering of the tournament. To this end, drawing on my experience from making SixBotGG for Six Gaming, I created FusionBot for The Observatory.

This bot takes care of a number of fun (and not so fun) admin tasks, but the one thing I wanted to highlight was the algorithm I created to create a Swiss pairing system for the qualifier tournaments. In a Swiss tournament, everyone plays a round every game, with the idea being that as the tournament progresses you will play opponents who are closer and closer to your skill range.

A manually-ran Swiss tournament often will have the tournament director throwing players who have the same number of wins in a pile, and randomly determining from that pile who plays who. There are ways to ensure people don't play each other twice, and even ways to govern who gets a bye round.

For The Observatory, I had added constraints that required some custom programming. For instance, players can't play each other if neither of them can host a multiplayer game. But as a general algorithm, here's how I managed to get a working Swiss algorithm in node.js.

1) Get a sorted list of players


This part's pretty easy. You want a list of players sorted by their performance.

var eventPlayers = Object.getOwnPropertyNames(event.players).filter((id) => !event.players[id].withdrawn).map((id) => {
    return {
        id: id,
        eventPlayer: event.players[id],
        ratedPlayer: ratedPlayers.find((p) => p.DiscordID === id) || {
            Name: obsDiscord.members.get(id).displayName,
            DiscordID: id,
            Rating: 1500,
            RatingDeviation: 200,
            Volatility: 0.06
        },
        points: event.matches.filter((m) => !m.cancelled && m.winner === id).length,
        matches: event.matches.filter((m) => !m.cancelled && m.players.indexOf(id) !== -1).length
    };
}).sort((a, b) => b.points - a.points || b.ratedPlayer.Rating - a.ratedPlayer.Rating || b.matches - a.matches || (Math.random() < 0.5 ? 1 : -1));

Here, I get all of the players in the tournament that haven't withdrawn and return an array that includes the player's ID (which for a Discord bot is simply their Discord ID), their player information for the tournament, their ELO, the number of wins they have in the match, and the number of matches they play. We then sort this so that the people with the most points are on top, and break ties on ELO, least number of matches played, and in the case that everything is even we just flip a coin.

2) For each player, match them to a potential opponent


Here, we create a recursive function that attempts to match players based on a set of criteria. In that function, we get the players that have yet to be matched up, and find potential opponents for the first player in that list.

var remainingPlayers = eventPlayers.filter((p) => matches.filter((m) => m.indexOf(p.id) !== -1).length === 0),
    firstPlayer = remainingPlayers[0],
    potentialOpponents = remainingPlayers.filter(
        (p) =>
            p.id !== firstPlayer.id &&
            event.matches.filter((m) => !m.cancelled && m.players.indexOf(p.id) !== -1 && m.players.indexOf(firstPlayer.id) !== -1).length === 0 &&
            (firstPlayer.eventPlayer.canHost || p.eventPlayer.canHost)
    );

You see here that the list of remaining players can't exist in my "matches" array. We get the first player and a list of potential opponents. Opponents cannot be yourself, cannot have been already played, and as is a special case either you or your opponent must be able to host a game. Inside the recursive function, if there is only one player in "remainingPlayers", we check to make sure that the player hasn't had a bye before. That is, if they've played as many matches as there have been rounds in the tournament, the bye is okay. If not, we fail attempt at making a matchup. Failed attempts get interesting, and we'll get to that in a moment.

if (remainingPlayers.length === 1) {
    if (firstPlayer.matches >= event.round) {
        return true;
    } else {
        return false;
    }
}

Now we start a loop through potential opponents. As long as there is a potential opponent available, we will try to assign them the match until we run out of opponents. How we determine who to match an opponent up is where the fun math comes in:

let index = Math.floor(potentialOpponents.length / Math.pow(2, event.round + 1));

With this formula, in an 8-person tournament, the top player will play the 5th seed in round 1, the 3rd seed in round 2, and the 2nd seed every round thereafter (assuming all other conditions for being a potential opponent have been met). This is far better than throwing names into a pile and drawing randomly. Since it uses math, it makes later round matches more meaningful, and allows for earlier matches to be scheduled fairly. Here's the loop in its entirety:

while (potentialOpponents.length > 0) {
    let index = Math.floor(potentialOpponents.length / Math.pow(2, event.round + 1));
    matches.push([firstPlayer.id, potentialOpponents[index].id]);
    if (remainingPlayers.length <= 2) {
        return true;
    }
    if (matchPlayers()) {
        return true;
    }

    matches.pop();
    potentialOpponents.splice(index, 1);
}
return false;

We push the match onto the matches array, but if we don't ever return true because we run out of remaining players or we can't match future players, we basically back the match out of the matches array and try again until we run out of matches to try. The "matchPlayers" function is our recursive function this resides in.

3) Determine who's home


In chess, there's a concept of determining who gets the white pieces. In Descent, it's about whose home level you're playing in. Here in The Observatory, we don't want people playing home or away too many times in a row, so we have a way to make sure that happens as infrequently as possible:

match.sort((a, b) => (
    (event.matches.filter((m) => !m.cancelled && m.home === a).length - event.matches.filter((m) => !m.cancelled && m.home === b).length) ||
    (event.matches.filter((m) => !m.cancelled && m.players.indexOf(b) !== -1 && m.home !== b).length - event.matches.filter((m) => !m.cancelled && m.players.indexOf(a) !== -1 && m.home !== a).length) ||
    (Math.random() < 0.5 ? 1 : -1)
));
eventMatch.home = match[0];

The "match" is the array of players, and we sort the array in a way that makes it so that the player who has played home the least gets it next. In the case they've both played home and away identically, we flip a coin.

Summary


Putting together a Swiss algorithm for pairings wasn't too hard, and in fact was more about getting all of the nuances of a Swiss tournament just right. A lot of trial and error was had, and the first two seasons of the Observatory resulted in some pretty spectacular failures. But the code is pretty solid now, and you can find the whole function under "generateround" in fusion.js. I would like to break this algorithm out of the bot and into its own NPM module at some point, perhaps that's something for a future CodeShare episode!

Labels: , , , ,

Thursday, February 11, 2016
The Observatory
Posted: 11:41:00 AM 0 comments


I'm starting a new Twitch show tomorrow, February 12th at 7 PM Eastern, called "The Observatory". This will feature the original Descent game, with 1v1 DCL matches being played. I will be observing the game using a brand new observer mode, and will be commentating the matches along with co-casters Entropy and Morfod.

Not sure what to expect? Check this out for a visual of observer mode, along with the neat custom HUD that will be used for the event.




Want to learn more? Check out my information page on The Observatory.

Labels: ,