KOTH: Every coin has two sides
Oracle, Python 3
Update: changed the order of the various tries to favor low pile of coins over rotations.
import sys
import itertools
from copy import deepcopy
MOVES_REQUIRED = 3
FLIPPED = 0
UNFLIPPED = 1
def filter_neighbors(neighbors, me, size):
limit = size - MOVES_REQUIRED
for data in neighbors:
i, _, flipped, unflipped = map(int, data.split('_'))
if MOVES_REQUIRED < (me - i) % size < limit:
continue # Skip neighbors that are too far away
yield i, [flipped, unflipped]
class Player:
def __init__(self, raw_data):
_, me, coins, *data = raw_data.split(';')
self.num_players = len(data)
self._me = int(me)
self._coins = int(coins)
self._state = dict(filter_neighbors(data, self._me, self.num_players))
def reset(self):
self.me = self._me
self.coins = self._coins
self.state = deepcopy(self._state)
self.my_state = self.state[self.me]
def invalid_move(self, move):
if move in 'NRT':
return False
if move in '123'[:self.coins]:
return False
flipped, unflipped = self.my_state
if flipped and move == 'U':
return False
if unflipped and move == 'F':
return False
if move in 'AXBYCZ'[:2 * unflipped]:
return False
return True
def N(self):
return 0
def one(self):
self.coins -= 1
self.my_state[UNFLIPPED] += 1
return -1
def two(self):
self.coins -= 2
self.my_state[UNFLIPPED] += 2
return -2
def three(self):
self.coins -= 3
self.my_state[UNFLIPPED] += 3
return -3
def A(self):
self.coins += 1
self.my_state[UNFLIPPED] -= 1
return 1
def B(self):
self.coins += 2
self.my_state[UNFLIPPED] -= 2
return 2
def C(self):
self.coins += 3
self.my_state[UNFLIPPED] -= 3
return 3
def X(self):
self.my_state[UNFLIPPED] -= 1
return 0
def Y(self):
self.my_state[UNFLIPPED] -= 2
return 0
def Z(self):
self.my_state[UNFLIPPED] -= 3
return 0
def R(self):
self.me = (self.me + 1) % self.num_players
flipped, unflipped = self.my_state = self.state[self.me]
return 2 * flipped - unflipped
def T(self):
self.me = (self.me - 1) % self.num_players
flipped, unflipped = self.my_state = self.state[self.me]
return 2 * flipped - unflipped
def F(self):
self.my_state[FLIPPED] += 1
self.my_state[UNFLIPPED] -= 1
return 2
def U(self):
self.my_state[FLIPPED] -= 1
self.my_state[UNFLIPPED] += 1
return -2
setattr(Player, '1', Player.one)
setattr(Player, '2', Player.two)
setattr(Player, '3', Player.three)
def scenarii(player):
for tries in itertools.product('FUABCXYZ123NRT', repeat=MOVES_REQUIRED):
player.reset()
points = 0
for try_ in tries:
if player.invalid_move(try_):
break
points += getattr(player, try_)()
else:
yield points, ''.join(tries)
if __name__ == '__main__':
player = Player(sys.argv[1])
print(max(scenarii(player))[1])
Tries every single possible output and keep the one that yield the maximum amount of points for this turn.
Bird in the Hand, Ruby
def deep_copy(o)
Marshal.load(Marshal.dump(o))
end
ID = 0
PTS = 1
FLP = 2
UFL = 3
round, id, global, *players = ARGV[0].split(';')
round = round.to_i
id = id.to_i
global = global.to_i
players.map!{ |s| s.split('_').map(&:to_i) }
nplayers = players.size
my_pos = players.find_index { |i, p, f, u| i == id }
state = {
round: round,
id: id,
global: global,
players: players,
my_pos: my_pos,
me: players[my_pos],
prev_p: players[my_pos-1],
next_p: players[(my_pos+1)%nplayers],
ends_game: round == 50 && my_pos == nplayers-1,
score: 0
}
moves = {
'N' => ->s{deep_copy(s)},
'1' => ->s{t = deep_copy(s); coins = [1, t[:global]].min; t[:global] -= coins; t[:me][UFL] += coins; t[:score] -= coins; t},
'2' => ->s{t = deep_copy(s); coins = [2, t[:global]].min; t[:global] -= coins; t[:me][UFL] += coins; t[:score] -= coins; t},
'3' => ->s{t = deep_copy(s); coins = [3, t[:global]].min; t[:global] -= coins; t[:me][UFL] += coins; t[:score] -= coins; t},
'A' => ->s{t = deep_copy(s); coins = [1, t[:me][UFL]].min; t[:global] += coins; t[:me][UFL] -= coins; t[:score] += coins; t},
'B' => ->s{t = deep_copy(s); coins = [2, t[:me][UFL]].min; t[:global] += coins; t[:me][UFL] -= coins; t[:score] += coins; t},
'C' => ->s{t = deep_copy(s); coins = [3, t[:me][UFL]].min; t[:global] += coins; t[:me][UFL] -= coins; t[:score] += coins; t},
'X' => ->s{t = deep_copy(s); coins = [1, t[:me][UFL]].min; t[:me][UFL] -= coins; t},
'Y' => ->s{t = deep_copy(s); coins = [2, t[:me][UFL]].min; t[:me][UFL] -= coins; t},
'Z' => ->s{t = deep_copy(s); coins = [3, t[:me][UFL]].min; t[:me][UFL] -= coins; t},
'F' => ->s{t = deep_copy(s); coins = [1, t[:me][UFL]].min; t[:me][UFL] -= coins; t[:me][FLP] += coins; t[:score] += 2*coins; t},
'U' => ->s{t = deep_copy(s); coins = [1, t[:me][FLP]].min; t[:me][FLP] -= coins; t[:me][UFL] += coins; t[:score] -= 2*coins; t},
'R' => ->s{
t = deep_copy(s)
(-1...t[:players].size-1).each do |i|
t[:players][i][FLP] = s[:players][i+1][FLP]
t[:players][i][UFL] = s[:players][i+1][UFL]
end
t[:score] += 2*t[:me][FLP] - t[:me][UFL];
t
},
'T' => ->s{
t = deep_copy(s)
(0...t[:players].size).each do |i|
t[:players][i][FLP] = s[:players][i-1][FLP]
t[:players][i][UFL] = s[:players][i-1][UFL]
end
t[:score] += 2*t[:me][FLP] - t[:me][UFL];
t
}
}
results = {}
'N123ABCXYZFURT'.each_char { |c1|
s1 = moves[c1][state]
'N123ABCXYZFURT'.each_char { |c2|
s2 = moves[c2][s1]
'N123ABCXYZFURT'.each_char { |c3|
s3 = moves[c3][s2]
s3[:ends_game] ||= s3[:global] == 0
results[c1+c2+c3] = s3
}
}
}
endingMoves = results.keys.select{|k| results[k][:ends_game]}
endingMoves.each{|k| results[k][:score] += 2*results[k][:me][FLP] - results[k][:me][UFL]}
$> << results.keys.shuffle.max_by {|k| results[k][:score]}
If neither of us has a bug in their programs, the main algorithm of this is likely very similar to Mathias's Oracle. Based on the assumption that prior to the final round we can't know which coins we'll end up with, we evaluate the current set of moves purely based on the points received immediately, ignoring completely what sort of coins we'll end up with. Since there are only 143 = 2744 possible move sets we can easily simulate all of them to figure out how many points they'll bring.
However, if a move set ends the game (either because it reduces the global pot to zero, or because this is round 50 and we're the last one to move), then it also takes into account the coins owned at the end of the move set to determine the move set's value. I first considered terminating the game whenever possible, but this would result in the horrible move 333
when there are only 9 coins left in the pot.
If there are multiple move sets giving the same result, we choose a random one. (I might change this to bias it in favour of game-terminating move sets.)
Greedy Rotation, Ruby
round, id, global, *players = ARGV[0].split(';')
round = round.to_i
id = id.to_i
global = global.to_i
players.map!{ |s| s.split('_').map(&:to_i) }
nplayers = players.size
my_pos = players.find_index { |i, p, f, u| i == id }
prev_p = players[my_pos-1]
next_p = players[(my_pos+1)%nplayers]
prev_score = 2*prev_p[2] - prev_p[3]
next_score = 2*next_p[2] - next_p[3]
take_from = prev_p
$><< '3'
if prev_score > next_score || prev_score == next_score && prev_p[3] > next_p[3]
$><< 'T'
else
$><< 'R'
take_from = next_p
end
if take_from[3] >= 3
$><< 'C'
elsif take_from[3] >= 1
$><< 'F'
else
$><< 'N'
end
This is quite similar to ArtOfCode's approach, except that this checks from which neighbour we can get more points, and it chooses C
instead of F
if we end up with 3 or more coins after the rotation.
After writing this up, I'm pretty sure that a better approach would be just to greedily pick the best out of all moves every time, preceding rotation by taking if possible (instead of sticking to a fixed "get unflipped, rotate, get rid of unflipped" pattern).
This also doesn't take into account the implicit points represented by the coins actually owned (based on the assumption that the game is going to last enough rounds that I likely won't end up keeping my coins anyway).