King of the Hill: Nose Poker - An ogre game for ogres

Here's the official score table:

ogres.forEach( function(data_row){var row=$('<tr>'); var cell=$('<td>'); cell.html(data_row.name); row.append(cell); cell=$('<td>'); cell.html(data_row.hand); row.append(cell); cell=$('<td>'); cell.html(data_row.np); row.append(cell);row.append(cell);cell=$('<td>'); cell.html(data_row.reason); row.append(cell);$('tbody#clients').append(row);});</script>

Nose Poker is an ogre game played by a group of ogres. Each ogre brings a large nose and a small club.

On each hand, all the ogres will declare how many times they will poke their nose with the club.

Each time, the lowest bid(s) will be knocked out of the game, then the remaining ogres will poke their nose with the club their declared number of times.

Any ogre who has run out of nose will also be knocked out of the game.

The Challenge

Write the JavaScript code to play Nose Poker. You'll write a client class which plays the role of an ogre and makes the decision how to bid on each hand.

Your ogre starts the game with 500 nose points (np), and there is no way to recover np during the game. Each turn you will call controller.declare(pokes) and either fall out of the competition if you declare the lowest number of pokes, or your troll will lose the declared amount of np.

The goal is to stay in the game as long as you can.

There are three ways to be knocked out of the game:

• Failed to bid If something goes wrong and you don't make a bit, or bid zero or less, you're out.
• Cowardice! Your ogre chickened out and bid lower than everyone else.
• Has no nose Your ogre ran out of np and therefore has no nose. How does it smell? Like it's out of the game.

The Controller

Your client class will be passed a controller object which acts as it's interface to the game.

Here are the four functions it provides:

np(): The current amount of nose points that your ogre has.

this.controller.np();
// 450

hand_number() The number of the current hand the game is on.

this.controller.hand_number();
// 2

scores() An array of the amount of np each currently active ogre has.

this.controller.scores();
// [450, 250, 20]

Note: This is intentionally sorted in descending order of np so it is not possible to see how other ogres have been bidding.

declare(pokes) Here's the important one. Declare how many times your ogre will poke itself in the nose with a club.

this.controller.declare(10);

Getting the app

You're going to need node and npm, sorry.

• cd nose_poker
• npm install

Writing a Client

Here's the template for your client class, add it to a js file in src/clients/working

module.exports = class {

constructor(controller) {
this.controller = controller;
}

play_hand() {
}

};

You can then modify the play_hand() function, remember it must call this.controller.declare(pokes). (You can also be knocked out of the game for failing to do this).

When you're happy with it, run ./get_answers.sh from the nose_poker root directory. This will add your client to /src/clients.js (and also get the current set of clients from the Stack Exchange question). You only need to do this once.

After the first time you can run games to see the result of modifying your code by calling node script/play_game.js.

• Name your ogre by including a title (h1, by starting the line with '#' or underlining it with '=' symbols)
• The name should only contain alpha characters and underscores, no spaces please.
• Put the code in a code block.
• Please include a brief description of your ogre's strategy, and any other information you'd like to include.

The app will then periodically pick up answers, run the game, and update the score table in this question.

Forbidden stuff

In order to be accepted into the competition, your code must export a valid class which accepts a controller, has a play_hand() function, and will call this.controller.declare(pokes) at some point in that function. If it does not, the official instance will not import it.

The importer will also ignore any answers which use these terms:

• game - Please do not try to interact with the game directly
• Math.random - Let's keep this deterministic, so the result is definitive.
• setTimeout, setInterval - These lead to runtime oddities.
• eval, import - Just don't!

Default bots

Default Bot Behaviour
go_home Always bid 1
go_big Bids 90% of np
half_way Bids 50% of np
• Can you describe the default bots here as well? And will they be included in the official tournaments even when we have more submissions? Because by the nature of the game, the presence of certain bots will influence the best strategy. Jul 23 '21 at 8:21
• The defaults are pretty much what they say. go_big bids 90% of it's np, go_home bids 1, and half_way bids 50% of it's np. I'm hoping there'll be enough entries to make those an irrelevant baseline, sooner or later. Jul 23 '21 at 8:23
• Nope, with those 249 bots present (I assume pokes > 0, but still work if pokes can be 0, change it to 499 bots, bids 0 on their turn and 1 otherwise), there are 249 rounds, and if you bid more than 2, you will run out of np earlier than bot 249, and if you bid 1, you lose since you bid the lowest. So always bidding 2 is the most optimal bot (assuming the presence of these 249 bots). Basically the winner bot is the one who can bid (the lowest bid + 1) every round. Jul 23 '21 at 8:35
• @Shaggy Well, I’m using a library to make HTTP requests and another to interact with the file system. That would be a big undertaking to handle those myself. If you’d like to do it without node it’d be pretty simple. You’d just have to edit src/clients.js manually. If you want to do that, be my guest. Jul 23 '21 at 19:23
• @Shaggy how is Node different from 'normal JS'? Jul 24 '21 at 3:32

Sinus-oidal

module.exports = class {
constructor(controller) {
this.controller = controller;
}

play_hand() {
var turn = this.controller.hand_number();
var multiply = Math.sin(turn * Math.PI / 4);

this.controller.declare(Math.floor(2 ** (multiply + 1)));
}
};

Get it?

• Why do I get the feeling the name inspired the bot and not the other way around? :P
– user
Jul 22 '21 at 20:02
• @user It may or may not have :p Jul 22 '21 at 20:06
• I heartily approve of this pun! Jul 22 '21 at 20:26

super_smart_bot

module.exports = class {
constructor(controller) {
this.controller = controller;
}
try_certain_win()
{
//we need to have more points than everyone else combined, plus 1 per player (though this bound is probably too weak)
//bid as many points as the lowest-NP player has, plus 1
let tcost = 0;
let mcost = 999;
let skipped = false;
let other_scores = this.controller.scores().filter(a => skipped ? true : (a == this.controller.np() ? (skipped = true)*0 : true));
for(let c of other_scores)
{
tcost += c + 1;
if(c + 1 < mcost) mcost = c + 1;
}
if(this.controller.np() <= tcost)
return false;
this.controller.declare(mcost);
return true;
}
min_safe_bid()
{
let skipped = false;
let other_scores = this.controller.scores().filter(a => skipped ? true : (a == this.controller.np() ? (skipped = true)*0 : true));
let res = Math.min(...other_scores) + 1;
return res;
}
play_hand() {
if(this.controller.hand_number() == 1)
{
this.controller.declare(2);
return;
}
let scores = this.controller.scores();
if(scores.length == 2) //optimal because this is the last round
{
this.controller.declare(this.controller.np() - 1);
return;
}
if(this.try_certain_win()) return;
let bid = this.controller.np() / this.controller.scores().length;
bid = Math.min(bid, this.min_safe_bid());
this.controller.declare(bid);
}
};
• Congrats on being the new winner! The dot at the end of your username has messed up my file extension detection... I'll fix that soon. Jul 23 '21 at 15:33

Smart Median

module.exports = class {

constructor(controller) {
this.controller = controller;
}

play_hand() {
// Beat the simple bots
if(this.controller.hand_number() <= 5) {
this.bid(this.controller.hand_number() + 1);
return;
}
var scores = this.other_scores();
var bid = Math.floor(this.median(scores) / scores.length) + 1; // Add 1 to win once there's only 1 opponent left
this.bid(bid);
}

other_scores() {
var scores = this.controller.scores();
var myIndex = scores.indexOf(this.controller.np());
scores.splice(myIndex, 1);
return scores;
}

median(scores) {
var half = Math.floor(scores.length / 2);
return scores[half];
}

bid(value) {
// Never bid more than the weakest player has
var weak = Math.min(...this.controller.scores());
// Never bid more than I have
var me = this.controller.np();
var final_bid = Math.min(value, weak + 1, me);
this.controller.declare(final_bid);
}

};

Chooses a bid based on the median score divided by the number of opponents remaining. The idea is that the more opponents there are, the more likely one is to bid low, so I am safer to bid lower as well. I also threw in some smart bidding features, like not bidding more than a guaranteed pass, to not lose too many points. I imagine this strategy will burn through too many points in the midgame to win, but I do think it'll place near the top.

• Welcome to Code Golf. Nice bot! Jul 23 '21 at 19:32
• Hi Iceman! Welcome to Code Golf, and thanks for taking part. So close to a win, and yet so far. The 'super smart bot' has something clever going on to bid high when it's down to two ogres, and yours bid 2 in the last round, which probably isn't ideal ;) - Great start, tho! Jul 23 '21 at 19:37
• Whoops, just realized I missed out on a return after my shortcut for the first few rounds, which led me to overbidding at the start. That might actually put me in a position to win in the last round. Still have no idea why I bid 2 in the last round; I'll have to wait until I can actually run the program myself to figure out why. Jul 23 '21 at 19:53

Half-life

module.exports = class {
constructor(controller) {
this.controller = controller;
}

play_hand() {
var np = this.controller.np()
var bid = np < 10 ? np - 1 : Math.floor(np / 2);
this.controller.declare(bid);
}
};

Takes the number of nose points it has and bids half each time. However, if it has less than 10 np left, it goes all in. This is to hopefully defeat half_way.

• You might want to change it to np < 10 ? np - 1 : Math.floor(np / 2); atm, if it goes all in, it gets immediately eliminated due to having 0 np. Jul 24 '21 at 5:55
• @tjjfvi Ah, I didn't realize that, thanks!
– user
Jul 24 '21 at 14:56

Dumb Ogre

module.exports = class {
constructor(controller) {
this.controller = controller;
}

play_hand() {
this.controller.declare(2);
}
};

To be fair, ogres aren't known to be particularly smart.

A trivial bot to get the ball rolling :p

• The presence of this actually makes bots that bids 3 as their first bid, and then continue their strategy next, the most optimal ones, lol. Jul 23 '21 at 8:18
• @justhalf I mean, theoretically that would definitely win the game, but it seems pretty labour intensitve. Jul 23 '21 at 8:28

Exponential Ogre

module.exports = class {
constructor(controller) {
this.controller = controller;
}

play_hand() {
let bid = 1 << this.controller.hand_number();
this.controller.declare(bid);
}
};

Starting at 2, the ogre doubles its bid every round. Quite similar to Reverse Pinocchio, but I got the idea independently.

• Looks like we came up with the same idea. I probably should've picked something smaller than 4 though :p Jul 23 '21 at 15:08
• Congrats on being the new winner! Jul 23 '21 at 15:10
• @RedwolfPrograms Your round(4**((n+1)/2)) is the same as 2**(n+1), so this is just shifted by one hand
– ovs
Jul 23 '21 at 15:10

module.exports = class {

constructor(controller) {
this.controller = controller;
}

play_hand()
{
if (this.controller.hand_number() == 1) {
this.lastScores = this.controller.scores();
this.controller.declare(2);
return;
}

var scores = this.controller.scores();
var finalBid = 100;
for (var i = 0; i < scores.Length; i++) {
for (var j = 0; j < this.lastScores.Length; j++) {
if (this.lastScores[j] - scores[i] < finalBid) {
finalBid = this.lastScores[j] - scores[i];
}
}
}

this.lastScores = scores;

this.controller.declare(finalBid + 1);
}
};

Starts in the first round by bidding 2, to not get kicked out immediately (due to Go_Home bot).

Then, assuming the bots stay in the same order, find out what everyone bid by finding the smallest difference between the previous turn and the current one.

As all bots I have seen so far reduce their bid monotonically, finding the lowest bid that survived and adding 1 will ensure that you survive next round.

EDIT: I dont have access to real coding tools right now, so I cant upload it/fix bugs. But I hope I made the strategy clear, so if anyone wants to copy this into a working bot, feel free to do so :)

EDIT2: The bots was completely busted and only bid 101 each turn :O Now it should work properly, so please update it again :D

• Sorry, it looks like this is throwing errors. gist.github.com/AJFaraday/e05f430a9944dd6a2e481bfb7016fde8 Jul 23 '21 at 12:07
• I've fixed your bug, just to un-break the game. It's my fault that it was importing and breaking on the error. By the way, you're currently winning :) Jul 23 '21 at 12:10
• Awesome :) Lets see for how long :D Jul 23 '21 at 12:11
• The scoreboard in your question on top seems to be broken. There are only two entries, and all the standard bots are gone Jul 23 '21 at 12:12
• I think you have this.scores where it should just be scores, which explains why you always guess 100 Jul 23 '21 at 15:11

Oldgre

Oldgre has perfected his strategy over many years.

Whippersnappgres!

module.exports = class {

constructor(controller) {
this.controller = controller;
}

play_hand() {
const x = this.controller.hand_number();
this.controller.declare(
-     0.000006858186 * x ** 11
-     0.000199732658 * x ** 10
+     0.024797517263 * x ** 9
-     0.737278798843 * x ** 8
+    11.416562524617 * x ** 7
-   107.788528491826 * x ** 6
+   656.517974108233 * x ** 5
-  2615.799102867764 * x ** 4
+  6707.381768918599 * x ** 3
- 10515.725107130049 * x ** 2
+  8997.738815265786 * x ** 1
-  3130.528128281523 * x ** 0
);
}

};

This is largely a proof of concept and, for the sake of the competition, I won't keep updating it.

• This kind of ruins the competition (though this particular bot will fail to bet on the last turn once any new submission is added). Jul 24 '21 at 8:58
• This is basically a fixed list of bids, right? Jul 24 '21 at 9:46
• @justhalf yeah. Jul 24 '21 at 16:20
• @thedefault. agreed -- I mostly made this as a proof-of-concept, and will likely not keep it updated for the sake of the competition (though I do have a program that I could use to regenerate that polynomial as new answers come in) Jul 24 '21 at 16:22
• This is awesome! Wouldn't a switch statement have done the same though, or are you obfuscating the "formula" :p Jul 25 '21 at 23:39

Simple Optimization Thing

module.exports = class {
constructor(controller) {
this.controller = controller;
}

play_hand() {
var count = this.controller.scores().length;
var min = Math.min(...this.controller.scores());

this.controller.declare(min / count);
}
};

Not sure if this strategy will work at all :p

Quicksort Ogre

module.exports = class {
constructor(controller) {
this.controller = controller;
}

play_hand() {
var turns = this.controller.hand_number();

this.controller.declare(Math.round(turns * Math.log(turns)) + 4);
}
};

Ogres don't understand time complexity, but they do understand being poked on the nose $$\n\log n\$$ times. Small constant offset to prevent being immediately eliminated.

Probably my last somewhat trivial bot, I just want to see how something between the linear and exponential strategies would do (and quadratic was a bit too boring :p).

Exactly the same as Adaptive by @Nurator, except increases it's bid the second round to eliminate the original Adaptive:

module.exports = class {

constructor(controller) {
this.controller = controller;
}

play_hand()
{
if (this.controller.hand_number() == 1) {
this.lastScores = this.controller.scores();
this.controller.declare(2);
return;
}
else if (this.controller.hand_number() == 2) {
this.lastScores = this.controller.scores();
/* Adaptive will bid 3 since 2 is the lowest number. Thus, bidding 4 will eliminate Adaptive */
this.controller.declare(4);
return;
}

var scores = this.controller.scores();
var finalBid = 75;
for (var i = 0; i < scores.Length; i++) {
for (var j = 0; j < this.lastScores.Length; j++) {
if (this.lastScores[j] - this.scores[i] < finalBid) {
finalBid = this.lastScores[j] - this.scores[i];
}
}
}

this.lastScores = scores;

this.controller.declare(finalBid + 1);
}
};

Can easily be defeated by increasing your bid in the third round instead, and the bot after can be easily defeated in the fourth round etc.

Super dumb bot

According to the instructions as I understand them, my own bot's score is returned somewhere in scores(); if this is not the case, comment out the lines starting with the line if (score == sscore) and continuing to the else.

module.exports = class {
constructor(controller) {
this.controller = controller;
}

play_hand() {
if(this.controller.hand_number() == 1)
{
// Go Home Bot makes everybody else need to do this nonsense on the first turn rather than really try.
this.controller.declare(2);
return;
}
if(this.controller.hand_number() == 2)
{
// Get rid of the other not-really-competing bot.
this.controller.declare(3);
return;
}
var myscore = this.controller.np();
var sscore = myscore;
var scores = this.controller.scores();
var ctr = 0;
for (var i = 0; i < scores.length; i++) {
var score = scores[i];
if (score != 0)
if (score == sscore)
sscore = 0;
else
ctr+=1;
}
var value = (ctr < 2) ? 0
: Math.floor(myscore / ctr);
this.controller.declare(value);
}
};

Either I know the optimal strategy off the top of my head or I don't. I put no effort into finding out whether or not I am right. The long loop just determines how many other bots are in the game. If the specifications had been written tighter it could be replaced by var ctr = scores.length - 1;

• Hi Joshua - Yes, your ogres np is returned in the result of scores(). You got to hand 8. Jul 25 '21 at 18:11

Knockout

module.exports = class {

constructor(controller) {
this.controller = controller;
}

play_hand() {
let hand = this.controller.hand_number()
if(hand == 12){
this.controller.declare(2);
return;
}
if(this.controller.scores.length == 2){
this.controller.declare(this.controller.np() - 1)
return;
}
this.controller.declare(500 / (this.controller.scores().length + hand + 2) + 20 * (hand > 7) + 5 * (hand > 9) + 5 * (hand > 9))
}

};

A very simple strategy with some small tweaks that allow it to win! Has been complicated a bit.

Basically tries to spend roughly the same amount each turn by going 500 / original number of bots. Of course, it's impossible to calculate the original number of bots, so it counts the current amount + the hand number.

Has several tricks, including going all-in at the end, and playing 2 on round 12 because oldgre will always play 1.

I'm not sure what's up with the snippet, but it is winning locally: • Whippersnappers! Jul 27 '21 at 9:31

Incremental ogre

module.exports = class {

constructor(controller) {
this.controller = controller;
}

play_hand() {
var bid = this.controller.hand_number() * 10
this.controller.declare(bid);
}

};

Bids 10 more each round.

• Guess this defeats all adaptive variants :/ Jul 23 '21 at 14:14
• @mousetail I'm going to have to revisit how it declares the winner. This one goes out on hand 6 because it makes the lowest bid. _adaptive__bot is out because it runs out of nose. So it should be _troll__adaptive winning... I'll have to sort by active after hand. Jul 23 '21 at 14:41