41

A few days ago, I solicited help on finding ways to prevent (or at least make it harder) for folks to cheat at Hat Dash. I would like to give a shout out to all users who have helped thus far - you can see them in the new White Hat Hall of Fame which is at the bottom of the Hat Dash Leaderboard. Each of these users exposed some weakness in the game protections that has subsequently been addressed (on client, server, or both). All of these users will also be receiving a bounty reward as well, as well as a special new hat (Defender of the Unicorn).

Defender of the Unicorn hat image

Since the post was made, I released a new version of the anti-cheating controls and heuristics for Hat Dash, featuring changes on both the client and server. Things are definitely not 100% secure. Knowing how it is built, I could definitely devise ways to get fake scores in (and I am sure that some of you will try to do so). However, as there is now a a pretty aggressive user auto-ban mechanism in place, it will be pretty easy to get banned if you start fooling around with things, so: caveat emptor. You can now also check on your ban-status.

That said, if you are able to cheat with the new system (cheat = manage to get an illegitimate score into the Leaderboard, and are able to report on how you did this; illegitimate = playing the game or accessing the API through any means other than using space/up-arrow/tap to play the game) and want to earn the hat/bounty/hof, feel free to post about it below or on the original post (only answers posted through the end of Dec 30 will be considered, awards are at my discretion).

And if you have already been banned, and still want to see if you can beat the system, Hat Dash will now print in your console after each game a line with [Date] | Is Game Suspect | (true/false) to let you know if there was something caught in the game (Note: not all suspect games will lead to auto-bans).

With the upgrade of the anti-cheat system, the Overall Stats listing has been deprecated. It is now located at the bottom of the Leaderboard. In its place at the top of the leaderboard is a new Overall Stats (from 2020-12-22) section (if you can think of a better name, let me know), which will only include scores from today and on.

May the odds be ever in your favor & happy jumping.

(Oh, and if you want to earn the new secret hat but don't want to be a white hat cheater, stay tuned…)

Update: The final White Hat Hall of Fame lineup is now set, thanks all for participating!

19
  • 2
    That's the hat I asked about. Awesome! :) Commented Dec 22, 2020 at 11:03
  • 1
    How can I know if I am banned?
    – Tugay
    Commented Dec 22, 2020 at 11:15
  • Yaakov, is there a hat for the first person to report this and who (shortly thereafter) proposed a quick heuristic in the chat.
    – Rob
    Commented Dec 22, 2020 at 11:55
  • 3
    @Tuqay check on your status at https://winterbash2020.stackexchange.com/hat-dash/status Commented Dec 22, 2020 at 13:08
  • 1
    Already looks like there's at least a couple ways around it - "double-beep" and "Indrajith Ekanayake" both seem to have circumvented the new system. Double-beep I believe is doing so for testing purposes, as they wrote an answer in meta.stackexchange.com/questions/358104/… but I have not yet seen anything of the same sort from the other guy.
    – CollinB
    Commented Dec 22, 2020 at 13:41
  • 1
    @Yaakov was this hat part of the secret hats list from the beginning, or did you add it only after doing the "penetration test" to Hat Dash? Commented Dec 22, 2020 at 14:11
  • @ShadowtheHatterWizard Hat 46, the Defender of the Unicorn, was added in cdn.sstatic.net/WinterBash/js/hakovaim.js?v=7 and was not part of cdn.sstatic.net/WinterBash/js/hakovaim.js?v=6
    – Bergi
    Commented Dec 22, 2020 at 14:51
  • @CollinB I achieved this score while I was banned. When I was unbanned, it got reinstated. It has now been removed. Commented Dec 22, 2020 at 15:30
  • @double-beep Thanks for clarifying!
    – CollinB
    Commented Dec 22, 2020 at 15:33
  • @Bergi nice find! :) Commented Dec 22, 2020 at 15:57
  • 9
    I made the hat myself this morning (using a retired hat from an old winterbash as a template). No more design time available for this year. Waddayathink? Commented Dec 22, 2020 at 15:58
  • 2
    @YaakovEllis I think you have a good backup plan in case you'll get bored of plain programming. ;) Commented Dec 22, 2020 at 16:22
  • @YaakovEllis Really catchy hat! :) Was the template Flip Flop (WB2015)?
    – Panda
    Commented Dec 22, 2020 at 16:54
  • 3
    @Zoe the new hat wont show up there without a JS refresh for the winterbash JS, but we are on a holiday build-freeze right now for non-emergencies, so the chat WB js wont refresh until Jan 4. Commented Dec 23, 2020 at 9:40
  • 1
    The actual js served from the WB site is up to date. But to force chat users to get it (and not use the version that they already have cached) requires a new build. Which will hopefully happen sometime on Jan 4. If you clear your js cache on chat right now, you should be able to see the hat Commented Dec 25, 2020 at 12:35

11 Answers 11

11
+50

In the chat, I told that, I won't cheat anymore, but I couldn't keep myself from trying this method and it worked.

setInterval(function() {
    Runner.instance_.horizon.obstacles[0].collisionBoxes = [];
},200);

I am emptying collision boxes every 200ms and our unicorn passes through them. But I thought that you check jump counts, so I jumped whenever I saw an obstacle even though there was no need.

enter image description here

P.S. After my doings, you are free to ban me once and for all

2
  • Yet you didn't get the highest score 🤔
    – No Sssweat
    Commented Dec 24, 2020 at 15:53
  • 3
    I could've gotten, but I didn't want
    – Tugay
    Commented Dec 24, 2020 at 15:53
5

In its place at the top of the leaderboard is a new Overall Stats (from 2020-12-22) section (if you can think of a better name, let me know)

I was thinking how they called it in the game halls, and I think

All Time High Scores

and

Daily High Scores

would be nice

5
+50

I've found a way to beat the system and get a high score without playing by the rules. I'm banned from the leaderboard so I couldn't get a score there, but the browser console message confirms that the game wasn't suspected.

I'm not really familiar with javascript so probably mine isn't the best method to do what I'm doing, but since it works, I won't waste time figuring out the correct way to do this.

Basically, you have to start the game and go hit an obstacle. When the game ends, enter

Runner.instance_.horizon.obstacles[0].typeConfig.yPos = 1337

into the browser console. Then start again, hit another obstacle and enter the same code into the console. Repeat until you stop getting any more obstacles (if things are working fine you should have to do this three times, and the third time the obstacle comes after a while, so you have to jump as if you're avoiding real obstacles to avoid tripping the anti-cheat system). Now you can play the game obstacle-free, but to fool the anti-cheat system you have to keep jumping and ducking as if you were actually avoiding obstacles. Keep going until you get tired and then switch to another tab to end the game.

Under the hood: There are three different types of obstacles, CACTUS_SMALL,CACTUS_LARGE and PTERODACTYL (they're defined in an array s.types). When you hit an obstacle, that particular obstacle type's reference will be in stored in Runner.instance_.horizon.obstacles[0] so you change that particular obstacle type's yPos to something huge. Obstacles of this type will now be positioned outside the canvas so you don't see them and more importantly, can't hit them. As I said, I'm not super familiar with javascript so I couldn't figure out how to edit s.types directly from the console (in which case you can change the yPos of all three types in one go) but if you can, feel free to edit this post or comment down below.

PoC Here I've scored 2557 and the console shows Is Game Suspect = false

4
+50

It appears that the game can still be auto-played through JavaScript.

First, to be clear, I did not create this script in its entirety. (original source) I modified it for the unicorn to dodge and tweaked the height of its jump.

It consistently netted me a score of around 1500 to 2000+ (before the unicorn trips over inadvertently).

I accidentally got myself banned (likely not due to the use of the script below) while trying other ways to cheat. I have theories on why I got banned, possibly relating to totalJumps.

Here's my one of my high scores that I got using the script below before I got banned.

Image

const autoPlayLoop = function() {
    const JUMP_SPEED = 750;
    const DISTANCE_BEFORE_JUMP = 112;

    const instance = window.Runner.instance_;
    const tRex = instance.tRex;

    if (tRex.jumping) {
        requestAnimationFrame(autoPlayLoop);
        return;
    }

    const tRexPos = tRex.xPos;
    const obstacles = instance.horizon.obstacles;

    const nextObstacle = obstacles.find(o => o.xPos > tRexPos);

        if (nextObstacle && (nextObstacle.xPos - tRexPos) <= DISTANCE_BEFORE_JUMP) {
        if (nextObstacle.yPos < 80) {
            tRex.setDuck(true);
        } else {
            tRex.startJump(JUMP_SPEED)
        }
    }
    requestAnimationFrame(autoPlayLoop);
}
requestAnimationFrame(autoPlayLoop);
4
  • The URLs are /hat-dash/start and /hat-dash/end respectively and don't start with /run-with-the-hats. Such URL doesn't exist. Commented Dec 22, 2020 at 15:14
  • @double-beep Right, makes sense now. Thanks for the clarification!
    – Panda
    Commented Dec 22, 2020 at 15:21
  • 3
    Welcome to the white hat hall of fame. Enjoy the hat! Commented Dec 22, 2020 at 15:26
  • @YaakovEllis Excited to be in, thank you!!
    – Panda
    Commented Dec 22, 2020 at 15:29
4
+50

Since Panda already mentioned it, I was also trying to automate the game using a different approach taken from here and originally meant to play the T-Rex run game.

The code is even simpler than the one posted by Panda. I don't know if this was what got me banned, but here is the modified version I used for reference.

(function loop() {
    var rand = Math.round(Math.random() * (3)) + 3;
    setTimeout(function() {
      try{
        DoAction()
      }
      catch(e){
              
      }          
      loop();  
    }, rand);
}());

function DoAction(){      
  if (Runner.instance_.horizon.obstacles.length > 0){ // if obsticles exist
    if (Runner.instance_.horizon.obstacles[0].xPos < Runner.instance_.currentSpeed * 20 - Runner.instance_.horizon.obstacles[0].width/3 && Runner.instance_.horizon.obstacles[0].yPos > 75){
      keyUp(40);
      keyDown(38);
    }
    else if (Runner.instance_.horizon.obstacles[0].xPos < Runner.instance_.currentSpeed * 20 - Runner.instance_.horizon.obstacles[0].width && Runner.instance_.horizon.obstacles[0].yPos > 75){
      keyDown(40);
    }    
   }
}

Notice that I am currently banned on the game, so probably there is either a check on direct access to game variables that has been implemented after the first hack-test phase, or the regularity of the jumps triggered some heuristic. This is also the reason I changed the code so that the loop now has a random delay that should make jumps appear more "human-like", but I can't test if that is enough to foil the in-place cheat prevention. Sadly, I didn't thought about that before getting the ban.

Based on what the network tab reports, the game seems to be performing three kinds of request:

  1. https://winterbash2020.stackexchange.com/hat-dash/start : just a signal that a game has started. Includes a timestamp
  2. https://winterbash2020.stackexchange.com/hat-dash/cp : the odd one. A message that contains a duration and a "jump total". Currently I didn't understand WHEN this is called, but it may be part of the client-side cheat prevention system. Yet, I noticed this being called even when playing without cheats, so I am not sure.
  3. https://winterbash2020.stackexchange.com/hat-dash/end : called on game end, contains expected info like duration and such. Also references the "total jumps" variable again. Oddly enough, it also contains a list of "history keys", like the game somehow referenced the previous scores too.

Originally I though that "totaljump" seemed the most probable value to be tied to cheat prevention, followed by the history array.

UPDATE: I am starting to doubt that I was banned because of the bot in the first place. I am thinking that must be related to other server-side heuristics that at least technically have nothing to do with cheating (could be triggered just by testing things). Some random ideas I had:

  • I tried to play in an inprivate browser window so that I would be logged off in order to do some testing without actually submitting any score.... Maybe having a client with the same IP result both unlogged and logged is a trigger;
  • I use two version of Firefox, standard and developer. Out of curiosity I also used developer to play the game in "mobile mode" to see if the feature someone reported (low triangles disappearing) was true and compare the behavior with the T-Rex Chrome game. Some users have reported to be banned for "switching user-agent string too much"
  • Since the api seems to track a player score history, perhaps it also tries to perform some reasoning based on "skill" - if someone suddenly gets a score that seems to be incompatible with their skill that could be a trigger. Seems quite odd - if that was the case I would assume many out-of-the-world scores would have resulted in instant bans (btw, considering that the game probably becomes human inplayable at about ... 20k max due to the speed changes, why is this not a thing? I doubt an human could reach 100k).

That said, this was just a test to see if the logic is still similar enough to the original T-Rex run that cheats meant for the original game would run on the Hat-Dash version too (something someone already tried and demonstrated in the old post). I don't have much free time on hand now, so I will simply report back later if I find more info worth noticing.


PS: in case someone is wondering WHAT this line is supposed to do...

if (Runner.instance_.horizon.obstacles[0].xPos < Runner.instance_.currentSpeed * 20 - Runner.instance_.horizon.obstacles[0].width && Runner.instance_.horizon.obstacles[0].yPos > 75){
  keyDown(40);
}

That is a very primitive attempt at solving the issues with the "impossible" jumps that this game has at times. As Magish noticed, sometime the Unicorn doesn't fall fast enough to be able to jump an obstacle too close to the previous one. In this cases, pressing down helps by making the Unicorn fall faster.

3
  • I would argue that cp in /hat-dash/cp stands for "checkpoint". Commented Dec 22, 2020 at 18:39
  • @iBugsaysReinstateMonica could be, but it seems to activate at random times. Some games, I don't even get a single call, some multiple ones. Will look into this, maybe the server checks for "skipped" checkpoints... will have to search for the logic that triggers them and find a way to replicate them. Commented Dec 22, 2020 at 18:50
  • @iBugsaysReinstateMonica this seems to be right; one of the response headers is x-route-name: HatDash/CheckPoint. Commented Dec 22, 2020 at 19:39
4
+50

I've been doing a bunch of work lately with the Playwright browser automation library, and when all you have is a hammer... I'm not saying this is a good way to cheat—it is not—but I was curious if I could do it without messing with the game itself at all.

My original idea was to use Playwright to load the page and start the game and then have it repeatedly capture screenshots for analysis. This would mean I wouldn't have to touch anything within the browser's environment besides pressing space, for maximum stealth. This worked, but it turned out that asking Chromium to take a screenshot (which caused a resize of the whole page), loading it into a buffer, and then processing it took on the order of 150ms, which was too slow to achieve even a respectable score using the very simple logic I had in mind. I was cheating, just badly.

So I switched to asking the client browser to invoke toDataURL() on the game's <canvas> element and getting the image data back that way. This could achieve latencies on the order of 10-15ms. I repeatedly capture an image of the game and look at a small detection rectangle for obstacles (non-white pixels; computer vision, this is not). If I find any, I press the space bar. This isn't particularly clever (I make no attempt to use the down arrow, and at high speeds it can fail because it tries to jump while we're still in the air), but the game is automated, so we can just keep playing until we get a lucky enough run.

This process is controlled by two constants. There's XCROP, which determines the detection region: how far in advance to look ahead when detecting obstacles. Setting it wrong means we jump too early or late. And there's SPEED_FACTOR, which linearly increases xcrop as the game progresses to adjust for the faster speed (does the speed increase linearly? I didn't check). These constants are highly sensitive to the environment on which the script is being run—no attempt is made to synchronize the automation with the game, and SCALE_FACTOR is entirely dependent on how long the image capture/processing loop takes to execute; even adding/removing a console.log() statement in testing was enough to throw them off. So these values will probably not work on your system. But if tuned decently (which can be automated by playing many games with different values), it can easily achieve scores good enough for the daily leaderboard.

This doesn't particularly try to avoid detection—it spams the space bar repeatedly at superhuman speed when an object is detected and has superhuman endurance to play game after game—and the detection logic of looking at a 33px-wide rectangle for non-white pixels is hardly clever, but it always seems to print "Is Game Suspect = false."

const Jimp = require('jimp');
const {chromium} = require('playwright');

(async () => {
  const XCROP = 122
  const SPEED_FACTOR = 0.008

  const browser = await chromium.launch({
    headless: false
  })

  const context = await browser.newContext()
  const page = await context.newPage()

  // pipe the browser console to our console so we can see the "Is Game Suspect" message
  page.on('console', msg => console.log(msg.text()))

  const playGame = async (xcrop, speedFactor) => {
    await page.goto('https://winterbash2020.stackexchange.com/run-with-the-hats')
    const container = await page.waitForSelector('.runner-container')

    // start the game
    await container.click()
    await container.press(' ')

    await page.waitForTimeout(500)

    const canvas = await page.$('canvas')

    let weLost = false
    for (let count=0; !weLost; count++) {
      // check if we lost and return our score if we did
      if (await page.$('.js-personal-stats > div')) {
        weLost = true
        const scoreElems = await page.$$('.js-personal-stats strong')
        const score = parseInt(await scoreElems[2].innerText())
        return score
      }

      // ask the game's <canvas> for its image data as a data url and stuff it in a buffer
      const dataURL = await page.evaluate((elem) => {
        return elem.toDataURL()
      }, canvas)
      const buffer = Buffer.from(dataURL.substr(22), 'base64')

      // parse the image data and crop out a small rectangle from it with 1-bit color depth for analysis
      const img = await Jimp.read(buffer)
      let foundPixel = false
      img
        .crop(xcrop + (speedFactor * count), 100, 33, 20)
        .posterize(2)

      // look though the image data for any non-white pixels, which indicates we found an object
      // the image is in RGBA format, so we can skip bits
      for (let i = img.bitmap.data.length - 1; i >= 0; i -= 4) {
        if (img.bitmap.data[i] != 0) {
          foundPixel = true
          break
        }
      }

      // if we detected an object, press space to jump
      if (foundPixel) {
        await container.press(' ')
        console.log(xcrop + (speedFactor * count), speedFactor, speedFactor * count)
      }
    }
  }

  const playMultipleGames = async (tries, ...args) => {
    const results = []
    for (let i=0; i<tries; i++) {
      const score = await playGame.apply(null, args)
      results.push(score)
    }
    console.log(results, args)
  }

  console.log(await playMultipleGames(20, XCROP, SPEED_FACTOR))

  await page.close()
  await context.close()
  await browser.close()
})().catch((ex) => {
  console.error(ex);
  process.exit(1)
});

I did most of my testing logged out, but added a login step to the script for some final tests (after a while, I did start triggering the captcha on the login form, so that works at least). You can ban me now.

4

Here's a solution for those who are too lazy to press Space to start the game and try the other hacks (me):

(async () => {
    const score = 4000, seconds = 200, jumps = 400;
    Runner.gameStarted();
    Runner.setCurrentScore(score);
    await new Promise(resolve => setTimeout(resolve, seconds * 1000));
    Runner.gameEnded(Runner.instance_.startedAt, jumps);
})();

Replace score, seconds and jumps accordingly. It would be helpful to watch other people's legitimate scored in the leaderboard.

Adapted from my previous answer. Added jumpCount, since it's also sent to the server with the new anti-cheating features.

3

I revised the auto-play script from Panda's answer, replaced a lot of implementation details, used some formulae from SPArcheon's answer (jump distance calculation), and inserted some fraudulent code taken from Tuqay's answer (resetting the collision box of obstacles). I also attempt to truncate game speed to avoid some client-side cheat detection and it plays well actually.

I turned off Resource Override this time - so no SE file is manipulated. Vanilla Google Chrome without any extension is enough.

I've submitted two scores from a sockpuppet (my main account is banned) and it's still not banned, see the screenshot below. However, the top scores have yet to appear on the leaderboard.

P.S. I recommend Microsoft Visual Studio Code for editing and debugging JavaScript running in Google Chrome or Microsoft Edge (new Chromium-based).

const keySpace = {
    key: " ",
    keyCode: 32,
    code: "Space",
    which: 32,
    shiftKey: false,
    ctrlKey: false,
    metaKey: false,
    isDown: false,
  },
  keyDown = {
    key: "down",
    keyCode: 40,
    code: "Down",
    which: 40,
    shiftKey: false,
    ctrlKey: false,
    metaKey: false,
    isDown: false,
  };

const createKeyEvent = function (keyObj, duration) {
  if (!keyObj.isDown) {
    document.dispatchEvent(new KeyboardEvent("keydown", keyObj));
    keyObj.isDown = true;
  }
  setTimeout(() => {
    if (keyObj.isDown) {
      document.dispatchEvent(new KeyboardEvent("keyup", keyObj));
      keyObj.isDown = false;
    }
  }, duration)
  ;
};

const autoPlayLoop = function () {
  const instance = Runner.instance_;
  const speed = instance.currentSpeed;
  if (speed > instance.config.MAX_SPEED) {
    instance.currentSpeed = instance.config.MAX_SPEED - 0.1;
  }

  const tRex = instance.tRex;
  // if (tRex.jumping) {
  //   requestAnimationFrame(autoPlayLoop);
  //   return;
  // }

  const tRexPos = tRex.xPos;
  const obstacles = instance.horizon.obstacles;
  const prevObstacle = obstacles.find((o) => o.xPos <= tRexPos);
  const nextObstacle = obstacles.find((o) => o.xPos > tRexPos);

  if (tRex.jumping) {
    if (prevObstacle) {
      createKeyEvent(keyDown, 200);
    }
  } else if (nextObstacle) {
    nextObstacle.collisionBoxes = [];

    const DISTANCE_BEFORE_JUMP = 20 * speed - nextObstacle.width / 3;
    if (nextObstacle.xPos - tRexPos <= DISTANCE_BEFORE_JUMP) {
      if (nextObstacle.yPos < 80) {
        // dodge instead of jump
        createKeyEvent(keyDown, 300);
      } else {
        if (speed >= 18) {
          createKeyEvent(keySpace, 30);
          //setTimeout(() => createKeyEvent(keyDown, 300), 150);
        } else {
          createKeyEvent(keySpace, 30);
        }
        //tRex.startJump(JUMP_SPEED);
      }
    }
  }
  requestAnimationFrame(autoPlayLoop);
};
requestAnimationFrame(autoPlayLoop);
1

The updated auto-ban system checks for illegal values for gravity, drop velocity, initial jump velocity, current speed and obstacle count. Since I wasn't modifying any of those values, I only had to worry about the current speed because as the game progresses, the current speed increases. To avoid tripping the auto-ban system, I only shifted the collision boxes up on the y axis whenever the current speed was less than 25. This is the code that I used:

setInterval(function() {
    var yPosition = 1000;
    function shiftCollisionBoxes(obstacles, yPosition) {
        if (obstacles && yPosition >= 0) {
            for (var i = 0; i < obstacles.length; i++){
                for (var j = 0; j < obstacles[i].collisionBoxes.length; j++){
                    obstacles[i].collisionBoxes[j].y = yPosition;
                }
            }
        }
    }
    if (Runner.instance_.currentSpeed < 25) {
        shiftCollisionBoxes(Runner.instance_.horizon.obstacles, yPosition);
        shiftCollisionBoxes(Runner.instance_.obstacles, yPosition);
    }
},200);

enter image description here enter image description here

I did pledge to be good. I guess I forgot to mention when. I acknowledge that this solution isn't 100% unique as other solutions already messed around with collision boxes and y axis positioning. However, this solution does demonstrate that checking for the existence of the collision boxes isn't enough. The code must also check that the positioning of the collision boxes wasn't tampered with. This solution also explicitly avoids invoking the updated auto-ban system. If I manage to make it into the hall of fame, this hat is going to match well with my white vans. Happy holidays everyone!

0

I can change the score by editing the Number function. When it is replaced, the code tries to convert the score string to a number on line 1168 of Firefox's pretty-printed code and runs the custom function instead. Paste this code into the JavaScript console:

function Number(n) {
  return 1000000;
}
2
  • 1
    Have you tried it? Is Game Suspect is true, which means the score isn't accepted. Commented Dec 28, 2020 at 11:16
  • @double-beep I've tried it with another number, if the game doesn't allow for this specific score, then you can put in another.
    – Anonymous
    Commented Dec 28, 2020 at 21:00
-1

Start the game, move the browser window to another screen, and the game will freeze. But if you wait for a while and press spacebar, the next obstacle will disappear as though the game was started from scratch. The score counter keeps increasing during the waiting time though. So you can move the window to another screen, wait for 10 minutes, press spacebar, and kill the unicorn. Voila, you got a very high score.

image

2
  • Can you include a screenshot, if you're on top?
    – Ollie
    Commented Dec 31, 2020 at 18:35
  • Oh, i actually got banned automatically
    – user914090
    Commented Dec 31, 2020 at 18:49

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .