Jump to content

Rock, paper, scissors!


Demonic God
 Share

Recommended Posts

As you can tell from... the Darvin quest, I am a bit obsessed with Advanced Aramors!

So, as part of an experiment, I would love to craft Aramors that are capable of doing more, on their own. To that end, I'd love to start with making Aramors that can, for now, participate in simple games of Rock Paper Scissors!

Your goal for this quest, would be to aid me in making such a creation. Specifically, I ask for you to design a strategy, one that is unambiguous and non randomized. Something that an Aramor can be programmed with!

Your strategy will compete with others in a tournament. The rules for the tournament is as follow:

  • Every strategy will compete with every other strategy
  • Each duel will last until one side receive 3 10 wins. If it is mathematically impossible exceeds 500 draws, both side receives a loss it's considered a draw.
    • This was added because the 500 draws checker got tripped... a lot.
  • The worst strategy will be eliminated after every duel has happened. This will repeat with the surviving strategies until one emerge victorious
  • The worst strategy would be evaluated with the following criteria:
    • Least wins
    • Most losses
    • Lose against strategies with similar win/loss (i.e if A has 2 wins and 5 losses, and B also has 2 wins and 5 losses, and B lost against A, then B is eliminated)
    • If the above criteria fail to select a single loser, all of them would be eliminated
  • The tournament will repeat without the winner(s) until all prizes are filled

The rules for strategies are as follow:

  • Your goal is to design a strategy, to participate in a tournament of rock, paper, scissors!
  • Your strategy must not be random and must cover all scenarios.
  • Your strategy is not limited to the game itself. It can adapt to the round it's on, the opponent, etc. Possible strategy includes:
    • Always choose rock
    • If there is less than 3 participant left, always choose rock
    • If opponent is Aia, always choose rock
  • Cooperation is not only allowed, it is welcomed. You are free to rig matches as you please.

Rewards

  • 1st - Anniversary creature. A choice between a colored pope, and a colored elemental, courtesy of Lady @Aia del Mana
  • 2nd - Anniversary creature. A colored pope or elemental, whichever remains after the winner had their pick
  • 3rd -  Anniversary creature.

Deadline:

I wish to wrap up the competition as soon as everyone who wish to join had done so. Thus, there would be two deadlines:

  • Hard deadline: End of anniversary + 3 days.
  • Soft deadline: 9 days (April 21st) from now. After this date, I will begin judging as soon as everyone who had announced their participant has submitted their strategy.
    • You may join in after the soft deadline, as long as judging has not begun. Soft deadline is meant to avoid unnecessary waiting.
Edited by Demonic God
Some rule changes
Link to comment
Share on other sites

7 minutes ago, Tissy said:

Good tournament. Should we know the list of the participants before submitting the strategy?

You could announce your participation in this thread, if you want to. Announcing your participation or keeping it a secret would be an interesting strategic choice. The list of participants would be announced before judging begins and admission closed for clarity, but I won't confirm nor deny if someone is playing.
 

Who knows, maybe you could even convince someone to fake their participant for some drama :)

Edited by Demonic God
Link to comment
Share on other sites

player_move.py

from enum import Enum, auto
class PlayerMove(Enum):
	ROCK = 'Rock'
	PAPER = 'Paper'
	SCISSORS = 'Scissors'

	def __gt__(self, obj):
		if isinstance(obj, PlayerMove):
			return self == PlayerMove.ROCK and obj == PlayerMove.SCISSORS or \
				self == PlayerMove.PAPER and obj == PlayerMove.ROCK or \
				self == PlayerMove.SCISSORS and obj == PlayerMove.PAPER
		else:
			raise TypeError(f'Not instance of PlayerMove{obj}')

	def __ge__(self,obj):
		if isinstance(obj, PlayerMove):
			return self > obj or self == obj
		else:
			raise TypeError(f'Not instance of PlayerMove{obj}')

	def __lt__(self,obj):
		if isinstance(obj, PlayerMove):
			return not self >= obj 
		else:
			raise TypeError(f'Not instance of PlayerMove{obj}')

	def __le__(self,obj):
		if isinstance(obj, PlayerMove):
			return self < obj or self == obj
		else:
			raise TypeError(f'Not instance of PlayerMove{obj}')

def getCounter(move):
	if isinstance(move, PlayerMove):
		return PlayerMove.PAPER if move == PlayerMove.ROCK else PlayerMove.SCISSORS if move == PlayerMove.PAPER else PlayerMove.ROCK
	else:
		raise TypeError(f'Not instance of PlayerMove{obj}')

def getNotCounter(move):
	if isinstance(move, PlayerMove):
		return PlayerMove.PAPER if move == PlayerMove.SCISSORS else PlayerMove.SCISSORS if move == PlayerMove.ROCK else PlayerMove.ROCK
	else:
		raise TypeError(f'Not instance of PlayerMove{obj}')

Just an enum representing Rock, Paper and Scissors

tournament.py

from player_move import *
import random

class Tournament:
	def __init__(self, *participants, output = 'output.txt'):
		self.log = open(output, "w", encoding="utf-8")

		self.participants = {p.name: p for p in participants}
		self.moveAnnouncer = EventDispatcher()  #Announces player moves result (announce what the two picked)
		self.matchAnnouncer = EventDispatcher() #Announces match (starting match between two players)
		self.roundAnnouncer = EventDispatcher() #Announces round (list of players)

		for participant in participants:
			self.moveAnnouncer.addListener(participant.moveListener)
			self.matchAnnouncer.addListener(participant.matchListener)
			self.roundAnnouncer.addListener(participant.roundListener)

		self.currentPlayers = [p.name for p in participants]
		self.currentMatches = []
		self.currentResults = {}

	def _generateMatches(self):
		# I hate myself for writing codes so ugly... but it's soooo convenient
		self.currentMatches = [[self.currentPlayers[p], self.currentPlayers[q]] for p in range(len(self.currentPlayers)) for q in range(len(self.currentPlayers)) if p < q]
		random.shuffle(self.currentMatches)
		self.currentResults = {p: {q: None for q in self.currentPlayers if p is not q} for p in self.currentPlayers}

	def _executeMove(self, player1, player2, pos):
		move1 = self.participants[player1].getMove()
		move2 = self.participants[player2].getMove()

		self.moveAnnouncer.broadcast({
				'player1': player1,
				'player2': player2,
				'move1': move1,
				'move2': move2,
				'pos': pos,
			})

		return move1, move2

	def _executeMatch(self, player1, player2):
		win1 = 0
		win2 = 0
		draw = 0
		movePos = 0

		self.log.write(f'Starting match between {player1} and {player2}\n')
		self.matchAnnouncer.broadcast({
				'player1': player1,
				'player2': player2,
				'event': 'start',
			})

		while win1 < 10 and win2 < 10:
			if draw >= 500:
				self.log.write(f'Stalemate!\n')
				break

			# Executing a single move
			move1, move2 = self._executeMove(player1, player2, movePos)

			msg = 'ERR' #this should change
			if move1 > move2:
				win1 += 1
				msg = f'{player1} wins!'
			elif move1 < move2:
				win2 += 1
				msg = f'{player2} wins!'
			else:
				draw += 1
				msg = "it's a draw!"

			self.log.write(f'Turn {movePos}: {player1} plays {move1.value}, {player2} plays {move2.value}, {msg}\n')
			movePos += 1

		result = player1 if win1 == 10 else player2 if win2 == 10 else 'draw'
		
		if result == 'draw':
			self.log.write(f'Match ended in a draw!\n\n')
			self.currentResults[player1][player2] = 'draw'
			self.currentResults[player2][player1] = 'draw'
		else:
			self.log.write(f'Player {result} won the match!\n\n')
			self.currentResults[player1][player2] = 'win' if result == player1 else 'lose'
			self.currentResults[player2][player1] = 'lose' if result == player1 else 'win'

		self.matchAnnouncer.broadcast({
			'player1': player1,
			'player2': player2,
			'event': 'end',
			'result': result,
		})

	def _executeRound(self):
		self.roundAnnouncer.broadcast({
			'event': 'start',
			'players': self.currentPlayers,
		})

		self.log.write(f"Here's our contestants for this round: {', '.join(self.currentPlayers)}!\n")

		self._generateMatches()
		assert len(self.currentMatches) > 0

		while self.currentMatches:
			player1, player2 = self.currentMatches.pop()
			self._executeMatch(player1,player2)

		worst = self._findWorst()
		stats = self._generateStats()

		self.log.write(f"Round ended! Here's the score!\n")
		for p in self.currentPlayers:
			self.log.write(f"{p}: {stats[p]['wins']} wins, {stats[p]['loses']} loses, {stats[p]['draws']} draws\n")
		self.log.write(f"The following contestants dropped out: {' '.join(worst)}\n\n")

		self.currentPlayers = [p for p in self.currentPlayers if p not in worst]

		self.roundAnnouncer.broadcast({
			'event': 'end',
			'dropout': worst,
		})


	def _generateStats(self):
		stats = {p: {'wins': 0, 'loses': 0, 'draws': 0} for p in self.currentPlayers}
		for p in self.currentPlayers:
			for r,v in self.currentResults[p].items():
				assert v is not None
				stats[p]['wins'] += 1 if v == 'win' else 0
				stats[p]['loses'] += 1 if v == 'lose' else 0
				stats[p]['draws'] += 1 if v == 'draw' else 0
		return stats

	def _findWorst(self):
		stats = self._generateStats()

		min_win = min([v['wins'] for p,v in stats.items()])
		max_lose_in_min_win = max([v['loses'] for p,v in stats.items() if v['wins'] == min_win])
		worst_players = [p for p in stats if stats[p]['wins'] == min_win and stats[p]['loses'] == max_lose_in_min_win]

		assert len(worst_players) > 0

		if len(worst_players) == 1:
			return worst_players

		worst_of_worst = []
		for p in worst_players:
			for p2 in worst_players:
				if p is not p2 and self.currentResults[p][p2] == 'win':
					break
			else:
				worst_of_worst.append(p)

		if len(worst_of_worst) > 0:
			return worst_of_worst
		else:
			return worst_players

	def executeGame(self):
		self.log.write(f"The tournament has begun!\n")
		draws = []
		result = None
		while len(self.currentPlayers) > 1:
			draws = self.currentPlayers
			self._executeRound()

		if len(self.currentPlayers) == 1:
			self.log.write(f"Congratulations to {self.currentPlayers[0]} for winning the tournament!\n")
			result = self.currentPlayers
		if len(self.currentPlayers) == 0:
			self.log.write(f"Looks like there's no apparent victor! The following players tied for top place: {' '.join(draws)}\n")
			result = draws

		self.log.close()
		return result

class EventDispatcher:
	def __init__(self):
		self.listeners = []

	def addListener(self, listener):
		self.listeners.append(listener)

	def removeListener(self, listener):
		if listener in self.listeners:
			self.listeners.remove(listener)

	def broadcast(self, *data):
		for listener in self.listeners:
			listener(*data)
			# try:
			# except:
			# 	print(f'Error broadcasting {data} to {listener}')

The current tournament emulator. It outputs text into "output.txt" (customizable).

strategies.py

from player_move import *

# ALL STRATEGIES SHOULD EXTEND BASE STRATEGY. Or at least implement the same thing as base strategy
class BaseStrategy:
	def __init__(self):
		self.name = 'Scissors'

	# 'player1': player1
	# 'player2': player2
	# 'move1': move by player1
	# 'move2': move by player2
	# 'pos': turn count (position of the "move")
	def moveListener(self, move):
		pass

	# 'player1': player1
	# 'player2': player2
	# 'event': 'start' or 'end'
	# 'result': result if event is 'end'
	def matchListener(self, match):
		pass

	# 'event': 'start' or 'end'
	# 'players': list of players if event is 'start'
	# 'dropouts': dropouts if event is 'end'
	def roundListener(self, round):
		pass

	# Your main logic
	def getMove(self):
		return PlayerMove.SCISSORS

class Rock(BaseStrategy):
	def __init__(self):
		self.name = 'Rocks'

	def getMove(self):
		return PlayerMove.ROCK

class Paper(BaseStrategy):
	def __init__(self):
		self.name = 'Papers'

	def getMove(self):
		return PlayerMove.PAPER

class Rand(BaseStrategy):
	def __init__(self):
		self.name = 'Rand'

	def getMove(self):
		import random
		r = random.randrange(0,3)
		return PlayerMove.ROCK if r == 0 else PlayerMove.PAPER if r == 1 else PlayerMove.SCISSORS

Where all the strategies are implemented. BaseStrategy should give you an idea of what is provided by the tournament. A few sample strategy is provided here. Note that random is used only for testing purposes - your strategy cannot be random (fixed seed pseudorandom is acceptable)

main.py

from tournament import *
from strategies import *

participants = []
participants.append(Rock())
participants.append(Paper())
participants.append(Rand())
participants.append(BaseStrategy())

tournament = Tournament(*participants)
tournament.executeGame()

simple main to create a tournament instance, and executes it.

Have fun! Please report any bugs you spot too :D

I'll be sending over each player individual strategy as I implement them (via PM). You're free to do so yourself (and save me effort!), though you'd be responsible for your own mistakes hehe.

If you have any question, feel free to PM

If you really, really wanna toy/benchmark your strategy, send me a PM and I can rush the implementation of your strategy :D

Additionally - The tournament will be postponed to 3 days after I've sent everyone their strategy in code form for any last minute changes and toying around. Sorry for all the delays!

Edited by Demonic God
added support for logging multiple tournament runs
Link to comment
Share on other sites

Ehem, there are some new development during trial runs!

Apparently, a lot of you have strategies relying on observation and rounds! And other stuff.

Which led to an interesting thing: There's no definite winner. Those strategies works really, really well, and depending on the ordering, can change the winner completely.

So I present the following two compromises:

  1. We run the simulations 100 times, and tally up!
  2. We run it 1 time, and let whoever wins, wins.

I've also updated the judging rules a bit on draws and such. As well as how second spots are chosen.

Link to comment
Share on other sites

Also - to clarify

I guess since the randomized order does give some issue... but well

Straight up random is still not allowed. You can try to implement some strange ways to confuse your opponents. Even as far as a fixed list of moves you wanna do.

I guess the line between random and strategy kinda blurs there but...

I also don't wanna rule lawyer exactly how can you implement your own version of pseudo random. Line's blurry. Is looping 10 choices on repeat "random"? 20? 50? What about changing where you start? Arg. Just keep it reasonable I suppose.

Those of you who wish to make use of that info is free to add a revision :3

Edited by Demonic God
Link to comment
Share on other sites

For the sake of auditability, I would suggest to order the contestants in a random order, and then to run a single simulation; it is quite impossible to audit one hundred such simulations.

If a random order were not feasible, one could order the contestants by the time of their first submission, or final amendment to the same.

Edited by Aia del Mana
Link to comment
Share on other sites

8 minutes ago, Aia del Mana said:

For the sake of auditability, I would suggest to order the contestants in a random order, and then to run a single simulation; it is quite impossible to audit one hundred such simulations.

If a random order were not feasible, one could order the contestants by the time of their first submission, or final amendment to the same.

Random order is what gave rise to our current dilemma, and is the default.

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
 Share

  • Forum Statistics

    16.8k
    Total Topics
    180k
    Total Posts
×
×
  • Create New...