2023-03-16 14:07:42 +01:00
#! /bin/python3
2023-03-14 18:15:15 +01:00
import collections
2023-03-14 15:20:18 +01:00
from compress import DeckCard , Action , ActionType , link , decompress_deck
from enum import Enum
from database import conn
from time import sleep
2023-03-16 14:07:42 +01:00
import sys
2023-03-14 09:04:08 +01:00
COLORS = ' rygbp '
STANDARD_HAND_SIZE = { 2 : 5 , 3 : 5 , 4 : 4 , 5 : 4 , 6 : 3 }
NUM_STRIKES_TO_LOSE = 3
2023-03-14 15:20:18 +01:00
class CardType ( Enum ) :
Trash = 0
Playable = 1
Critical = 2
Dispensable = 3
class CardState ( ) :
def __init__ ( self , card_type : CardType , card : DeckCard , weight = 1 ) :
self . card_type = card_type
self . card = card
self . weight = weight
def __repr__ ( self ) :
match self . card_type :
case CardType . Trash :
return " Trash ( {} ) " . format ( self . card )
case CardType . Playable :
return " Playable ( {} ) with weight {} " . format ( self . card , self . weight )
case CardType . Critical :
return " Critical ( {} ) " . format ( self . card )
case CardType . Dispensable :
return " Dispensable ( {} ) with weight {} " . format ( self . card , self . weight )
2023-03-14 09:04:08 +01:00
class GameState ( ) :
2023-03-14 18:46:45 +01:00
def __init__ ( self , num_players , deck , debug = False ) :
2023-03-14 09:04:08 +01:00
assert ( 2 < = num_players < = 6 )
2023-03-14 18:46:45 +01:00
self . debug = debug
2023-03-14 09:04:08 +01:00
self . num_players = num_players
self . deck = deck
2023-03-14 15:20:18 +01:00
for ( idx , card ) in enumerate ( self . deck ) :
card . deck_index = idx
2023-03-14 09:04:08 +01:00
self . deck_size = len ( deck )
self . num_suits = max ( map ( lambda c : c . suitIndex , deck ) ) + 1
2023-03-15 11:16:09 +01:00
self . num_dark_suits = ( len ( deck ) - 10 * self . num_suits ) / / ( - 5 )
2023-03-14 09:04:08 +01:00
self . hand_size = STANDARD_HAND_SIZE [ self . num_players ]
2023-03-15 11:16:09 +01:00
self . num_strikes = 3
2023-03-15 15:44:03 +01:00
self . players = [ " Alice " , " Bob " , " Cathy " , " Donald " , " Emily " , " Frank " ] [ : self . num_players ]
2023-03-14 18:46:45 +01:00
2023-03-15 15:44:03 +01:00
# can be set to true if game is known to be in a lost state
self . in_lost_state = False
2023-03-14 09:04:08 +01:00
# dynamic game state
self . progress = self . num_players * self . hand_size # index of next card to be drawn
self . hands = [ deck [ self . hand_size * p : self . hand_size * ( p + 1 ) ] for p in range ( 0 , num_players ) ]
self . stacks = [ 0 for i in range ( 0 , self . num_suits ) ]
self . strikes = 0
self . clues = 8
self . turn = 0
2023-03-14 15:20:18 +01:00
self . pace = self . deck_size - 5 * self . num_suits - self . num_players * ( self . hand_size - 1 )
2023-03-14 09:04:08 +01:00
self . remaining_extra_turns = self . num_players + 1
2023-03-14 15:20:18 +01:00
self . trash = [ ]
2023-03-14 09:04:08 +01:00
# will track replay as game progresses
self . actions = [ ]
@property
def cur_hand ( self ) :
return self . hands [ self . turn ]
def __make_turn ( self ) :
assert ( self . remaining_extra_turns > 0 )
self . turn = ( self . turn + 1 ) % self . num_players
if self . progress == self . deck_size :
self . remaining_extra_turns - = 1
2023-03-14 18:46:45 +01:00
if self . debug :
print ( " Elapsed {} turns, last action was {} . Current board state: \n {} with stacks: {} " . format (
len ( self . actions ) , self . actions [ - 1 ] , self . hands , self . stacks
) )
2023-03-14 09:04:08 +01:00
def __replace ( self , card_idx ) :
2023-03-15 11:16:09 +01:00
idx_in_hand = next ( ( i for ( i , card ) in enumerate ( self . cur_hand ) if card . deck_index == card_idx ) , None )
assert ( idx_in_hand is not None )
2023-03-14 09:04:08 +01:00
for i in range ( idx_in_hand , self . hand_size - 1 ) :
self . cur_hand [ i ] = self . cur_hand [ i + 1 ]
if self . progress < self . deck_size :
self . cur_hand [ self . hand_size - 1 ] = self . deck [ self . progress ]
self . progress + = 1
def play ( self , card_idx ) :
card = self . deck [ card_idx ]
if card . rank == self . stacks [ card . suitIndex ] + 1 :
self . stacks [ card . suitIndex ] + = 1
if card . rank == 5 and self . clues != 8 :
self . clues + = 1
else :
self . strikes + = 1
2023-03-15 11:16:09 +01:00
assert ( self . strikes < self . num_strikes )
2023-03-14 15:20:18 +01:00
self . trash . append ( self . deck [ card_idx ] )
2023-03-14 09:04:08 +01:00
self . actions . append ( Action ( ActionType . Play , target = card_idx ) )
self . __replace ( card_idx )
self . __make_turn ( )
def discard ( self , card_idx ) :
assert ( self . clues < 8 )
self . actions . append ( Action ( ActionType . Discard , target = card_idx ) )
self . clues + = 1
2023-03-14 15:20:18 +01:00
self . pace - = 1
self . trash . append ( self . deck [ card_idx ] )
2023-03-14 09:04:08 +01:00
self . __replace ( card_idx )
self . __make_turn ( )
def clue ( self ) :
assert ( self . clues > 0 )
self . actions . append (
Action (
ActionType . RankClue ,
2023-03-14 09:14:40 +01:00
target = ( self . turn + 1 ) % self . num_players , # clue next plyaer
value = self . hands [ ( self . turn + 1 ) % self . num_players ] [ 0 ] . rank # clue index 0
2023-03-14 09:04:08 +01:00
)
)
2023-03-14 09:14:40 +01:00
self . clues - = 1
2023-03-14 09:04:08 +01:00
self . __make_turn ( )
2023-03-14 09:14:40 +01:00
def to_json ( self ) :
return {
" deck " : self . deck ,
" players " : self . players ,
" actions " : self . actions ,
" first_player " : 0 ,
" options " : {
" variant " : " No Variant " ,
}
}
2023-03-14 15:20:18 +01:00
def card_type ( self , card ) :
played = self . stacks [ card . suitIndex ]
if card . rank < = played :
return CardType . Trash
elif card . rank == played + 1 :
return CardType . Playable
elif card . rank == 5 or card in self . trash :
return CardType . Critical
else :
return CardType . Dispensable
def is_over ( self ) :
2023-03-16 14:07:42 +01:00
return all ( s == 5 for s in self . stacks ) or ( self . remaining_extra_turns == 0 ) or ( self . is_known_lost ( ) )
2023-03-15 15:44:03 +01:00
2023-03-14 15:20:18 +01:00
def holding_players ( self , card ) :
for ( player , hand ) in enumerate ( self . hands ) :
if card in hand :
yield player
2023-03-15 15:44:03 +01:00
@property
2023-03-14 15:20:18 +01:00
def score ( self ) :
return sum ( self . stacks )
2023-03-15 15:44:03 +01:00
def is_won ( self ) :
return self . score == 5 * self . num_suits
def is_known_lost ( self ) :
return self . in_lost_state
2023-03-14 15:20:18 +01:00
class GreedyStrategy ( ) :
def __init__ ( self , game_state : GameState ) :
self . game_state = game_state
self . earliest_draw_times = [ ]
for s in range ( 0 , game_state . num_suits ) :
self . earliest_draw_times . append ( [ ] )
for r in range ( 1 , 6 ) :
self . earliest_draw_times [ s ] . append ( max (
game_state . deck . index ( DeckCard ( s , r ) ) - game_state . hand_size * game_state . num_players + 1 ,
0 if r == 1 else self . earliest_draw_times [ s ] [ r - 2 ]
) )
# Currently, we do not add the time the 5 gets drawn to this, since this is rather a measurument on how
# bad a suit is in terms of having to hold on to other cards that are not playable *yet*
self . suit_badness = [ sum ( self . earliest_draw_times [ s ] [ : - 1 ] ) for s in range ( 0 , game_state . num_suits ) ]
def make_move ( self ) :
hand_states = [ [ CardState ( self . game_state . card_type ( card ) , card , None ) for card in self . game_state . hands [ p ] ] for p in range ( self . game_state . num_players ) ]
2023-03-14 18:15:15 +01:00
# find dupes in players hands, marke one card crit and the other one trash
2023-03-14 18:27:29 +01:00
p = False
2023-03-14 18:15:15 +01:00
for states in hand_states :
counter = collections . Counter ( map ( lambda state : state . card , states ) )
for card in counter :
if counter [ card ] > = 2 :
2023-03-14 18:27:29 +01:00
dupes = ( cstate for cstate in states if cstate . card == card )
first = next ( dupes )
if first . card_type == CardType . Dispensable :
first . card_type = CardType . Critical
for dupe in dupes :
dupe . card_type = CardType . Trash
2023-03-14 18:15:15 +01:00
2023-03-16 14:07:42 +01:00
def hand_badness ( states ) :
if any ( state . card_type == CardType . Playable for state in states ) :
return 0
crits = [ state for state in states if state . card_type == CardType . Critical ]
crits_val = sum ( map ( lambda state : state . card . rank , crits ) )
if any ( state . card_type == CardType . Playable for state in states ) :
return crits_val
def player_distance ( f , t ) :
return ( ( t - f - 1 ) % self . game_state . num_players ) + 1
2023-03-14 15:20:18 +01:00
for ( player , states ) in enumerate ( hand_states ) :
for state in states :
if state . card_type == CardType . Playable :
2023-03-16 14:07:42 +01:00
copy_holders = set ( self . game_state . holding_players ( state . card ) )
2023-03-14 15:20:18 +01:00
copy_holders . remove ( player )
2023-03-16 14:07:42 +01:00
connecting_holders = set ( self . game_state . holding_players ( DeckCard ( state . card . suitIndex , state . card . rank + 1 ) ) )
2023-03-14 15:20:18 +01:00
if len ( copy_holders ) == 0 :
2023-03-16 14:07:42 +01:00
# card is unique, imortancy is based lexicographically on whether somebody has the conn. card and the rank
state . weight = ( 6 if len ( connecting_holders ) > 0 else 1 ) * ( 6 - state . card . rank )
2023-03-14 15:20:18 +01:00
else :
2023-03-16 14:07:42 +01:00
# copy is available somewhere else
if len ( connecting_holders ) == 0 :
# card is not urgent
state . weight = 0.5 * ( 6 - state . card . rank )
else :
# there is a copy and there is a connecting card. check if they are out of order
turns_to_copy = min ( map ( lambda holder : player_distance ( player , holder ) , copy_holders ) )
turns_to_conn = max ( map ( lambda holder : player_distance ( player , holder ) , connecting_holders ) )
if turns_to_copy < turns_to_conn :
# our copy is not neccessary for connecting card to be able to play
state . weight = 0.5 * ( 6 - state . card . rank )
else :
# our copy is important, scale it little less than if it were unique
state . weight = 4 * ( 6 - state . card . rank )
2023-03-14 15:20:18 +01:00
elif state . card_type == CardType . Dispensable :
try :
# TODO: consider duplicate in hand
copy_holders = list ( self . game_state . holding_players ( state . card ) )
copy_holders . remove ( player )
nextCopy = self . game_state . deck [ self . game_state . progress : ] . index ( card )
except :
nextCopy = 1
2023-03-14 18:15:15 +01:00
# state.weight = self.suit_badness[state.card.suitIndex] * nextCopy + 2 * (5 - state.card.rank)
state . weight = nextCopy + 2 * ( 5 - state . card . rank )
2023-03-14 18:27:29 +01:00
2023-03-14 15:20:18 +01:00
cur_hand = hand_states [ self . game_state . turn ]
plays = [ cstate for cstate in cur_hand if cstate . card_type == CardType . Playable ]
trash = next ( ( cstate for cstate in cur_hand if cstate . card_type == CardType . Trash ) , None )
2023-03-14 18:46:45 +01:00
# actual decision on what to do
2023-03-14 15:20:18 +01:00
if len ( plays ) > 0 :
play = max ( plays , key = lambda s : s . weight )
self . game_state . play ( play . card . deck_index )
elif self . game_state . clues == 8 :
self . game_state . clue ( )
elif trash is not None :
self . game_state . discard ( trash . card . deck_index )
elif self . game_state . clues == 0 :
dispensable = [ cstate for cstate in cur_hand if cstate . card_type == CardType . Dispensable ]
if len ( dispensable ) == 0 :
2023-03-15 15:44:03 +01:00
self . game_state . in_lost_state = True
# raise ValueError("Lost critical card")
2023-03-14 15:20:18 +01:00
else :
discard = min ( dispensable , key = lambda s : s . weight )
self . game_state . discard ( discard . card . deck_index )
else :
self . game_state . clue ( )
2023-03-14 09:04:08 +01:00
def test ( ) :
2023-03-14 15:20:18 +01:00
# seed p4v0s148
deck = decompress_deck ( " 15wpspaodknlftabkpixbxiudqvrumhsgeakqucvgcrfmfhynwlj " )
2023-03-14 09:04:08 +01:00
gs = GameState ( 5 , deck )
2023-03-14 15:20:18 +01:00
print ( gs . deck )
2023-03-14 09:04:08 +01:00
2023-03-14 15:20:18 +01:00
strat = GreedyStrategy ( gs )
while not gs . is_over ( ) :
strat . make_move ( )
# print(strat.suit_badness)
# print(COLORS)
# strat.make_move()
print ( gs . actions )
print ( link ( gs . to_json ( ) ) )
wins = open ( " won_seeds.txt " , " a " )
losses = open ( " lost_seeds.txt " , " a " )
2023-03-14 18:46:45 +01:00
crits = open ( " crits_lost.txt " , " a " )
2023-03-14 15:20:18 +01:00
lost = 0
won = 0
crits_lost = 0
def run_deck ( seed , num_players , deck_str ) :
global lost
global won
global crits_lost
deck = decompress_deck ( deck_str )
gs = GameState ( num_players , deck )
strat = GreedyStrategy ( gs )
2023-03-16 14:07:42 +01:00
while not gs . is_over ( ) :
strat . make_move ( )
if not gs . score == 25 :
losses . write ( " Seed {:10} {} : \n {} \n " . format ( seed , str ( deck ) , link ( gs . to_json ( ) ) ) )
lost + = 1
else :
won + = 1
2023-03-14 09:04:08 +01:00
2023-03-16 14:07:42 +01:00
def run_samples ( num_players , sample_size ) :
2023-03-14 15:20:18 +01:00
cur = conn . cursor ( )
2023-03-16 14:07:42 +01:00
cur . execute ( " SELECT seed, num_players, deck FROM seeds WHERE variant_id = 0 AND num_players = ( %s ) order by seed desc limit ( %s ) " , ( num_players , sample_size ) )
2023-03-14 15:20:18 +01:00
for r in cur :
run_deck ( * r )
print ( " won: {:4} , lost: {:4} , crits lost: {:3} " . format ( won , lost , crits_lost ) , end = " \r " )
2023-03-14 18:15:15 +01:00
print ( )
2023-03-14 18:27:29 +01:00
print ( " Total wins: {} % " . format ( round ( 100 * won / ( lost + won + crits_lost ) , 2 ) ) )
2023-03-16 14:07:42 +01:00
if __name__ == " __main__ " :
for p in range ( 2 , 6 ) :
print ( " Testing on {} players... " . format ( p ) )
run_samples ( p , sys . argv [ 1 ] )