Dungeon of botdom

– of crisis and martyrdom

(that's the subtitle because subtitles are cool)

In this challenge in (yup; you need go no farther to know you may not submit in java), you need to create a bot that plays a game very similar to welcome to the dungeon

Game Rules

(note that this is not the original game)

There is a deck, an item set, and some reward cards and death cards. Base HP is 3. The deck consists of 13 monster cards numbered 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 9 to denote their strengths.

Item List

1. Demonic pact: Defeat the demon (strength 7 monster), and the monster below it on the dungeon pile. - (just defeats the demon if the demon was the last in the dungeon)

2. Health potion: When you fall to 0 HP, defeat the monster and return to 3 HP.

3. Holy grail: Defeat monsters of even-numbered strength (in the game, these are undead). If an even-numbered monster occurs after the demonic pact has been used, that acts first, and you will not get an extra-pact kill after this monster.

4. Vorpal dagger: Choose one monster before entering the dungeon; this type of monster is defeated. If the targeted monster occurs after the demonic pact has been used, that acts first, and you will not get an extra-pact kill after this monster.

5. Shield: Add 3 to total HP before spelunking. This does not affect usage of health potion, which will always return health to 3.

6. Armour: Add 5 to total HP before spelunking. This does not affect usage of health potion, which will always return health to 3.

Reward cards are used to keep track of who has succeeded in the dungeon. Death cards track who has failed in the dungeon.

Drawing Phase

Before the drawing phase begins all monster cards are returned to the deck, both players are restored to 3 HP, and all discarded items are restored such that there is one of each.

The first player decides whether to draw a card from the deck, hiding it from the other player. If so, they must choose to either place it on top of the dungeon pile or discard it along with an item of their choice. Discarded items and cards will be unavailable to either player until the next round.

After player one takes their turn, player two does the same. The players alternately decide whether to draw and what to do with the drawn card, until someone decides not to draw or a player takes the last card from the deck. If a player decides not to draw, or draws the last card, the drawing phase ends and the other player now has to enter the dungeon and begin spelunking.

Spelunking Phase

If the Vorpal dagger has not been discarded, the spelunking player must now decide which card to apply it to. There are no active decisions to be made for the rest of this phase.

The first player takes the top card; that is, the last card placed in the dungeon, and see its strength number. If demonic pact is active from the previous turn, the drawn card is discarded. Otherwise, the player's items will be checked in the order 'demonic pact', 'holy grail', 'Vorpal dagger'. The first un-discarded item capable of defeating the drawn card will then be used, and the card discarded. If demonic pact is used, it will now be active for the next card. The used item is not discarded.

If there is no applicable item available, the strength of the card is subtracted from the player's health. If their health is no longer positive they will be restored to 3 HP and the potion discarded if available, otherwise the dungeon crawl ends and they take a death card.

While the player is not defeated and there are cards remaining in the dungeon, this process of drawing the top card is repeated. Upon successfully defeating all cards in the dungeon the dungeon crawl ends and the spelunking player collects a reward card.

Full Game Description

A game consists of a series of rounds, each having a drawing phase and then a spelunking phase. At the end of each round one player will have collected either a death card or a reward card; once a player accumulates 5 of either type the game ends. If they have 5 death cards they lose the game. If they have 5 reward cards, they win. Either way, the other player receives the opposite result. If neither player has 5 cards of one type, play progresses to the next round and the player who went second in the previous round now goes first and vice versa.

KOTH details

Each bot will play 400 games against every other bot according to the rules described above. Which bot is player one (and so goes first in the first round) alternates each game, and all state is reset between games.

Here are the items again:

1. Demonic pact: Defeat the demon (strength 7 monster), and the monster below it on the dungeon pile. - (just defeats the demon if the demon was the last in the dungeon)

2. Health potion: When you fall to 0 HP, defeat the monster and return to 3 HP.

3. Holy grail: Defeat monsters of even-numbered strength (in the game, these are undead). If an even-numbered monster occurs after the demonic pact has been used, that acts first, and you will not get an extra-pact kill after this monster.

4. Vorpal dagger: Choose one monster before entering the dungeon; this type of monster is defeated. If the targeted monster occurs after the demonic pact has been used, that acts first, and you will not get an extra-pact kill after this monster.

5. Shield: Add 3 to total HP before spelunking. This does not affect usage of health potion, which will always return health to 3.

6. Armour: Add 5 to total HP before spelunking. This does not affect usage of health potion, which will always return health to 3.

and the deck: 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 9.

You must implement a bot class that does not use class variables, deriving from the following base class:

class BasePlayer:
def start_turn(self, last_turn):
raise NotImplementedError

def play(self, card):
raise NotImplementedError

def vorpal_choice(self, last_turn):
raise NotImplementedError

def result(self, bot, result, dungeon, vorped):
raise NotImplementedError


This base class shows methods your class needs to implement, and the number of arguments taken by each.

Method Argument Descriptions

• last_turn in vorpal_choice and start_turn is an integer or a None value. A value of 0 to 5 indicates that the enemy discarded the drawn card along with the item indicated by that value (see the list of items above). A value of 6 indicates that the enemy placed the card in the dungeon. A None value indicates that the bot is playing first in this round (not possible for vorpal_choice). In vorpal_choice last_turn is likely to be 7, indicating that they passed that turn. The only circumstance in which it is not 7 is when the enemy drew the last card.

• card is a number representing the strength of one of the cards from the deck as enumerated above.

Now, the arguments for result are a little bit more complex:

• bot indicates the bot which entered the dungeon. 0 indicates entering the dungeon, and 1 indicates that the enemy entered the dungeon.

• result indicates the success of the trip. False indicates the spelunking bot succeeded, while True indicates they failed.

• dungeon is a list of cards/ints representing the cards that were in the dungeon. The dungeon is ordered by order placed; the first card placed in the dungeon is first in the list, and the last card placed in is at the end. You will not receive any info about discarded cards; they are secret from the other bot.

• vorped is an integer representing the vorpal_choice made by the spelunking bot. If bot==0, you already know this, but if bot==1, this can be useful information.

I'll be honest, I don't entirely recall why I made winning result False, but I think it was a good idea at the time.

Return Values

• start_turn: Return 1 to draw a card, or 0 to pass.

• play: Return 0 to 5 to discard the corresponding item and the drawn card, or 6 to place the card in the dungeon (consistent with last_turn input, except for passing, which is done during start_turn).

• vorpal_choice: Return the number of the card you wish to eliminate with the Vorpal dagger (1 to eliminate 1s, 5 to eliminate 5s). Picking a non-existent card kills you (8 is illegal, 10 is illegal, 0 is illegal).

• result: You may return anything, because this is an informing function to update the bot's data.

Additional clarifications, or just repeating some small details you might have skipped past and might want to know quickly:

Bots play 400 games with each other bot.

There is no specific end time for this KOTH, except the end of the bounty for what that is worth. Just try to be winning at each time.

Results so far (sorry for being rather lazy with these guys :P)

1 GrailThief 2732 0.98
2 Steve 2399 0.86
3 DevilWorshipper 1854 0.66
4 RunAway 1336 0.48
5 BoringPlayer 1190 0.42
6 SlapAndFlap 783 0.28
7 DareDevilDumDum 750 0.27
8 RandomMandom 156 0.06


Grailthief "steals" the bounty. not really, because it earned it. Good work, Sleafar!

• I've actually played this game once or twice. Pretty challenging sometimes. – Draco18s Jun 15 '17 at 13:55
• ehhh, tbh I doubt it will be that popular :P – Destructible Lemon Jun 16 '17 at 0:17
• 2. place the item in the dungeon. The item goes in the dungeon (duh) appears to be a typo; there isn't an item mentioned at that point (you just drew a card from the monster deck). The rules should probably be clarified a bit. – user62131 Jun 16 '17 at 0:40
• @ais523 or any information known only to one player. am i being unclear again? the dungeon is only revealed at the end, so cards drawn by a bot is known only to one bot. as a bonus, cards discarded are not revealed ever. if you think "oh well then there is a probabilistic best strategy", opponent prediction is still greatly important so this is also invalid – Destructible Lemon Jun 16 '17 at 0:46
• Ah, it's currently unclear from the rules that you don't know the current contents of the dungeon (only the influence that you've personally had on it). That should almost certainly be clarified. – user62131 Jun 16 '17 at 0:59

GrailThief

An old and experienced dungeon crawler. He knows that most of the others hope, that the holy grail saves them, therefore he makes sure it disappears.

from base import BasePlayer
import copy

class GrailThief(BasePlayer):
class Stats:
def __init__(self):
self.deck = [1, 2, 3, 4, 5] * 2 + [6, 7, 9]
self.items = {0, 1, 2, 3, 4, 5}
self.dungeon_known = []
self.dungeon_unknown = 0
self.monsters_safe = {2, 4, 6, 7}
self.update()

def update(self):
self.dungeon_total = len(self.dungeon_known) + self.dungeon_unknown
deck_factor = float(self.dungeon_unknown) / len(self.deck) if len(self.deck) > 0 else 1.0
self.dungeon_weighted = [(i, 0.0 if i in self.monsters_safe else 1.0) for i in self.dungeon_known] + [(i, 0.0 if i in self.monsters_safe else deck_factor) for i in self.deck]
dungeon_weighted_sums = dict.fromkeys(self.dungeon_known + self.deck, 0.0)
for i in self.dungeon_weighted:
dungeon_weighted_sums[i[0]] += i[0] * i[1]
self.vorpal = max(dungeon_weighted_sums, key = dungeon_weighted_sums.get)
if 3 in self.items:
self.dungeon_weighted = [(i[0], 0.0 if i[0] == self.vorpal else i[1]) for i in self.dungeon_weighted]

new = copy.copy(self)
new.items = {i for i in new.items if i != item}
if item == 0:
new.monsters_safe = {i for i in new.monsters_safe if i != 7}
elif item == 2:
new.monsters_safe = {i for i in new.monsters_safe if i not in {2, 4, 6}}
if card is not None:
new.deck = list(new.deck)
new.deck.remove(card)
new.update()
return new

def to_dungeon(self, card):
new = copy.copy(self)
if card is None:
new.dungeon_unknown += 1
else:
new.deck = list(new.deck)
new.deck.remove(card)
new.dungeon_known = list(new.dungeon_known)
new.dungeon_known.append(card)
new.update()
return new

def effective_hp(self):
hp = 3.0
if 1 in self.items:
hp += 3.0
if self.dungeon_total > 0:
hp += sum([(i[0] - 1) * i[1] for i in self.dungeon_weighted]) / self.dungeon_total
if 4 in self.items:
hp += 3.0
if 5 in self.items:
hp += 5.0
return hp

def effective_damage(self):
damage = sum([i[0] * i[1] for i in self.dungeon_weighted])
if 0 in self.items:
if self.dungeon_total > 1:
damage -= damage / (self.dungeon_total - 1)
return damage

def __init__(self):
self.stats = self.Stats()

def process_last_turn(self, last_turn):
if last_turn in [0, 1, 2, 3, 4, 5]:
elif last_turn == 6:
self.stats = self.stats.to_dungeon(None)

def start_turn(self, last_turn):
self.process_last_turn(last_turn)
if self.stats.effective_hp() > self.stats.effective_damage() + 1.5:
return 1
else:
return 0

def play(self, card):
if 2 in self.stats.items:
return 2
else:
self.stats = self.stats.to_dungeon(card)
return 6

def vorpal_choice(self, last_turn):
self.process_last_turn(last_turn)
return self.stats.vorpal

def result(self, bot, result, dungeon, vorped):
self.stats = self.Stats()


DevilWorshipper

My first attempt at a KOTH challenge:

from base import BasePlayer
#from random import randint

class DevilWorshipper(BasePlayer):
def reset(self):
self.items = [0, 1, 2, 3, 4, 5]
self.turns = 0
self.demon = False
self.dragon = False

def __init__(self):
self.reset()

def start_turn(self, last_turn):
if last_turn in self.items:
self.items.remove(last_turn)

if last_turn is not None:
#self.demon = True if randint(1, 13 - self.turns) <= 2 else False
self.turns += 1

if (((self.demon == True and not (0 in self.items)) or (self.dragon == True)) and not (3 in self.items)):
return 0
if (len(self.items) <= 1):
return 0
return 1

def play(self, card):
self.turns += 1

if (card == 9):
self.dragon = True
return 6

if (card == 7):
self.demon = True
return 6

for i in [3, 0, 2, 1, 5, 4]:
if (i in self.items):
self.items.remove(i)
return i

return 6

def vorpal_choice(self, last_turn):
return 5 #If it works for others maybe it will work for us

def result(self, bot, result, dungeon, vorped):
self.reset()


Essentially, we get rid of the pact and the vorpal dagger, wait for the demon to get into the deck, and pass. Every round the opponent may have drawn the demon, it has the % chance that the last card the opponent drew was a demon to just assume they played the demon already.

Let me know if there's anything I did wrong; I haven't messed with python in a while, this is my first KOTH, and it's 2 am, so there's bound to be something.

EDITS:

Taking out the randomness turns out to help it a lot. With the randomness in it is very dumb. Also, as said in the comments below, it tries to summon the demon or dragon.

Steve

from base import BasePlayer
from random import choice

class Steve(BasePlayer):

def reset(self):
self.items = [0, 1, 2, 3, 4, 5]
self.turns = 0
self.dungeon = []
self.possibledungeon = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 9]
self.lastDied = 0

def __init__(self):
self.TRIALS = 10 #How many dungeon runs to do each turn
self.PASS = 8    #How many dungeon runs have to have died to pass
self.SMASHITEMS = 4 #If less dungeon runs died, smash items.
self.reset()

def start_turn(self, last_turn):
if (last_turn is not None):
self.turns += 1

if (last_turn in self.items):
self.items.remove(last_turn)
else:
self.dungeon.append(-1)

#Check if the dungeon is lethal
died = 0
total_hp = 3
if (5 in self.items):
total_hp += 5
if (3 in self.items):
total_hp += 3
vorpal = self.vorpal_choice(None)
for i in range(self.TRIALS):
hp = total_hp
temppossible = self.possibledungeon.copy()
usedpotion = False
killedDemon = False
#Going for a crawl
for j in self.dungeon[::-1]:
if (killedDemon == True): #If last round we killed the Demon
killedDemon = False
j = 0
if (j == -1): #If we don't know what this card is
j = choice(temppossible)
temppossible.remove(j)
if (j == 7 and 0 in self.items): #If we kill demon with the pact
j = 0
killedDemon = True
if (j % 2 == 0 and 2 in self.items) or (j == vorpal): #If we vorpal or grail
j = 0

hp -= j

if (hp <= 0):
if (not usedpotion and 1 in self.items):
hp = 3
usedpotion = True
else:
died += 1
break

if (died >= self.PASS):
return 0

died = self.lastDied
return 1

def play(self, card):
self.possibledungeon.remove(card)

if (self.lastDied < self.SMASHITEMS):
if (7 in self.dungeon) and (0 in self.items):
self.items.remove(0)
return 0
if ( (9 in self.dungeon) or (5 in self.dungeon) ) and (3 in self.items):
self.items.remove(3)
return 3
for i in [2, 1, 5, 4, 3, 0]:
if (i in self.items):
self.items.remove(i)
return i

self.dungeon.append(card)
return 6

def vorpal_choice(self, last_turn):
if (last_turn is not None):
self.turns += 1

if (last_turn in self.items):
self.items.remove(last_turn)
else:
self.dungeon.append(-1)

if (self.dungeon.count(5) == 2):
return 5
if (9 in self.dungeon):
return 9
if (self.dungeon.count(4) == 2 and not 2 in self.items):
return 4
if (7 in self.dungeon and not 0 in self.items):
return 7
for i in range(6)[::-1]:
if (i+1 in self.dungeon):
return i+1
return 5

def result(self, bot, result, dungeon, vorped):
self.reset()


Steve tries to guess whether or not the dungeon is lethal. If he thinks it is, he passes. Other than that, I tried to make him get rid of items smartly. He used to adjust his PASS threshold depending on if he died in the dungeon or the opponent lived, but it ended up making him a lot dumber so I got rid of it.

He still doesn't beat GrailThief on my machine, but at least comes closer.

• you know that 9 is the dragon, not the demon? – Destructible Lemon Jun 24 '17 at 5:57
• ...oh. I hadn't realised that. Well, this code seems to be doing fine as is at least – J. Dingo Jun 24 '17 at 5:58
• I've changed the code to attempt to summon the demon or dragon. – J. Dingo Jun 24 '17 at 6:05
• I would like to say: nice job and also thanks for joining the koth – Destructible Lemon Jun 24 '17 at 6:09
• I will attempt to execute this some time in the future, but I currently don't have my preferred computer so it may be hard... maybe I'll ask someone else nicely to execute it for me? – Destructible Lemon Jun 24 '17 at 6:29

SlapAndFlap

First time in KOTH, so slap me hard for mistakes.

This one simpleton is always trying to remove all good items with low-strength monsters, while keep powerful ones and then just forces opponent to play.
It beats RunAway and DumDum at least :D
My other bot in deleted answer for some time, I need to fix him by tommorow

from base import BasePlayer

class SlapAndFlap(BasePlayer):

def reset(self):
# Monsters that you pushed in
self.know_monster = list(self.deck)

# Items still in game
self.items_in_game = [True, True, True, True, True, True]

# List of items, sorted by value
self.valuables = [3,1,5,0,2,4]

# Counter
self.cards = 13

def __init__(self):
# Deck
self.deck = (1,1,2,2,3,3,4,4,5,5,6,7,9)
# Indexes of item cards
self.items = (0, 1, 2, 3, 4, 5)

self.reset()

def start_turn(self, last_turn):
if last_turn is not None:
self.cards -= 1

# Sneak peak at items removed by opponent
if last_turn is not None and  last_turn < 6:
self.items_in_game[last_turn] = False
self.valuables.remove(last_turn)

# Flap!
if self.cards < 6:
return 0
return 1

def play(self, card):
if card < 6 and any(self.items_in_game):
self.know_monster.remove(card)
to_return = self.valuables[0]   # remove the best of the rest
self.valuables = self.valuables[1:]
self.items_in_game[to_return] = False
else:
return 6

def vorpal_choice(self, last_turn):
# We can just guess what monster will be there
# But we know ones, we removed

# If we have pact, no need to remove demon
if self.items_in_game[0]:
self.know_monster.remove(7)
# If we have grail, no need to remove even monsters (kinda)
if self.items_in_game[2]:
self.know_monster = [i for i in self.know_monster if i%2]

# Find what threatens us the most, counting its strength multiplied by number
weight = [i * self.know_monster.count(i) for i in self.know_monster]
return weight.index(max(weight)) + 1

def result(self, bot, result, dungeon, vorped):
self.reset()  # we live for the thrill, not the result!

• Well, the first thing I'm going to slap you hard for is that it is KOTH, not KOHL. – Gryphon Jun 15 '17 at 16:33
• @Gryphon That one hurts :( – Dead Possum Jun 15 '17 at 16:35

RandomMandom

The obligatory random bot. Appropriately, he loses hard to the default bots, which is good, because it means that the game has at least some strategy.

from base import BasePlayer
from random import randint
from random import choice
class RandomMandom(BasePlayer):
def __init__(self):
self.items = [0,1,2,3,4,5]

def start_turn(self, last_turn):
if last_turn in self.items:
self.items.remove(last_turn)
if len(self.items) > 0:
return randint(0,1)
return 1

def play(self, card):
if len(self.items) > 0:
if randint(0,1) == 1:
selection = choice(self.items)
self.items.remove(selection)
return selection
return 6

def vorpal_choice(self, last_turn):
return choice([1,2,3,4,5,6,7,9])

def result(self, bot, result, dungeon, vorped):
# Never learns
pass

• I'm confused... why does the bot always keep drawing when all items are gone? also you might want to weight it, otherwise it has a 50/50 chance of not placing anything in the dungeon and then you just look silly. PS nice name of bot – Destructible Lemon Jun 18 '17 at 3:12
• (when I said not placing anything I meant not playing any turns) – Destructible Lemon Jun 19 '17 at 4:21
• I didn't analyze the controller very closely, because I assumed that it wouldn't present me with a choice if none existed. Also, it's a super-naive bot, because I wanted to get at least one entry in before my real entry. I will use your feedback in the next bot. – Andrew U Baker Jun 19 '17 at 4:52
• I think you confused the deck with items. you can have no items but still have a deck and have another turn. it is unlikely given how bots will probably play to not be stupid. – Destructible Lemon Jun 19 '17 at 4:54
• also you have 75% chance of winning the bounty at this point if you do make that new bot so that'll probably be fun for you (mostly because of people not answering) – Destructible Lemon Jun 19 '17 at 5:02

DareDevilDumDum

kinda clear. never backs down. the only way you can (consistently; RunAway loses sometimes but it still beats this most of the time) lose to this bot is if you don't remove any items or are a super coward. think of this bot as a reminder to remove items, otherwise even this can win.

from base import BasePlayer

class DareDevilDumDum(BasePlayer):
def start_turn(self, last_turn):
return 1  # damn squiggles. Draw a card

def play(self, card):
return 6+card*0  # put the card in the dungeon, and use card to avoid squiggles :P

def vorpal_choice(self, last_turn):
return 9+last_turn*0  # dragon

def result(self, bot, result, dungeon, vorped):
pass  # we live for the thrill, not the result!


And

RunAway

pretty much they remove armour and then run away some time before the end. as daredevildumdum, it doesn't remember anything, except for the number of cards in the deck (which tbh wouldn't be remembered in the actual game (you'd just check)) and whether someone removed the armour (mostly same as before).

from base import BasePlayer

class RunAway(BasePlayer):

def __init__(self):
self.cards = 13
self.armoured = True

def start_turn(self, last_turn):
if last_turn is not None:
self.cards -= 1  # opponents play
if last_turn is 5:
self.armoured = False

if self.cards < 4:
return 0 * last_turn  # avoid the ---noid--- squiggles
else:
return 1

def play(self, card):
if self.cards > 11 and self.armoured:  # if it is the first turn and armour has not been removed
choice = 5  # remove armour
else:
choice = 6  # put the card in the dungeon
self.cards -= 1  # this play
return choice

def vorpal_choice(self, last_turn):
return 5  # without using more memory, this is the best choice statistically

def result(self, bot, result, dungeon, vorped):
self.cards = 13
self.armoured = True


Also because I am special challenge poster, these bots do not count toward my bot count, because they are example bots that are dumb

• Fixed the second code snippet (the beginning of it) – Mr. Xcoder Jun 15 '17 at 14:11

BoringPlayer

The opposite of RandomMandom, BoringPlayer always makes the same choices. The problem is that it appears that it is too successful for such a simple bot. It scores 3800+ in my local test.

from base import BasePlayer

class BoringPlayer(BasePlayer):
def start_turn(self, last_turn):
return 1

def play(self, card):
return 6

def vorpal_choice(self, last_turn):
return 5

def result(self, bot, result, dungeon, vorped):
# Never learns
pass

• This is almost identical to Daredevil except it chooses the golem for its vorpal sword. weird that you get 3800+. this seems like an obscure bug – Destructible Lemon Jun 20 '17 at 3:05
• ok, it seems like picking 5 over nine is actually a really big advantage... O_o. I guess that this is not a bug – Destructible Lemon Jun 20 '17 at 4:35
• I think that 5 over 9 is significant against runaway (because 9 was overkill anyway), and the other bots kind of need more fine tuning with their numbers anyway – Destructible Lemon Jun 20 '17 at 4:45
• I didn't intend for it to be that close to DareDevil. I was actually working on a more sophisticated bot that tracked the cards more closely, and this was just the stub values. However, since it outperformed everything else, I decided I should submit it anyway. I will try to get my more sophisticated bot working to see if I can come up with a better bot. – Andrew U Baker Jun 20 '17 at 5:02
• Note to people seeing this: This bot does not mean that this simplistic strategy will dominate; the other bots are also very simple and this is why this bot wins – Destructible Lemon Jun 20 '17 at 5:04