In [79]:
import numpy as np
from random import shuffle

#################################################################################
######################################SETUP######################################
#################################################################################
numbers=(1,1,1,2,2,3,3,4,4,5)
colors=('r','y','g','b','w')           #possibly add multi... "m"
ManyCol=len(colors)

#CREATE AN ARRAY OF FIVE HANDS, ONE ON EACH ROW WITH CARDS REPRESENTED BY SERIAL NUMBERS FROM
#THE ORIGINAL DECK, TOP TO BOTTOM, STARTING AT 0. WE DEAL EACH HAND FROM THE TOP OF THE DECK.
table=np.arange(20).reshape(5,4)
#[A,B,C,D,E]=table  #MAY USE LETTERING OF PLAYERS, BUT PRLY NOT

#CREATE A FULL SHUFFLED DECK WITH NOMENCLATURE AS IN [2,0] FOR "3 red." THINK OF THE INDICES IN deck
#AS SERIAL NUMBERS WRITTEN ON THE BACKS OF THE CARDS. DeckOut CONVERTS TO EXTERNAL FORMAT LIKE "r3".
def CreateDeck():
    deck=[]
    for color in range(ManyCol):
        for number in numbers:
            deck.append([color,number-1])    #internal format numbers range 0-4; colors are numbered
    shuffle(deck)
    #deck=tuple(deck)        #converts deck from list to tuple (mutable to immutable)
    return deck             #...maybe keep as list if want multiple deals

deck=CreateDeck()
def PrintDeckOut(deck):
    DeckOut=[colors[card[0]]+str(card[1]+1) for card in deck]
    for i in range(int(len(deck)/4)+1):          #PRINTS DECK 4 AT A TIME
        print(DeckOut[4*i:4*(i+1)])
####################################################################################################
#####################################PUBLIC DATA####################################################
####################################################################################################

#############           stacks/extras******************for logging played/publicly visible cards 
#THE ARRAY stacks GIVES THE LAST NUMBER SUCCESSFULLY PLAYED FOR EACH COLOR AND extras IS A
#ManyColx5 ARRAY TELLING THE NUMBER OF EXTRAS OF EACH CARD. FOR EXAMPLE, A 5 COLOR GAME WITH ONLY
#A 1R DISCARD AND 1Y PLAYED MIGHT LOOK LIKE stacks=[0,1,0,0,0] AND
#extras=[[1,1,1,1,0],[1,1,1,1,0],[2,1,1,1,0],[2,1,1,1,0],[2,1,1,1,0]]. 
stacks=[[1,0,0,0,0]]*ManyCol
extras=[[2,1,1,1,0]]*ManyCol

#THE ROWS IN THE ARRAY priority TELL THE CARDS SERIAL NUMBERS IN THE ORDER THAT PLAYERS
#WILL SEARCH. TOP PRIORITY ARE CARDS CLUED BOTH NUMBER AND COLOR. NEXT COME NUMBER OR COLOR. THEN,
#UNCLUED CARDS. CHRONOLOGICAL ORDER BREAKS TIES. FOR EXAMPLE, IF A PLAYER HAS BEEN TOLD CARDS 6
#AND 8 ARE 4's, BUT DOESN'T KNOW ABOUT THE OTHER CARDS 21, 22, THEN HER SEARCH PRIORITY WILL BE
#priority[2]=[8,6,22,21]. 6 AND 8 HAVE BEEN CLUED, SO THEY GET PRIORITY OVER 21 AND 22. THEN, THE NEWER
#CARDS 8 AND 22 GET PRIORITY OVER 6 AND 21 (RESP.) THEN PLAYER 2 WILL SEARCH FOR PLAYS FROM 8 TO 21. 
#PRIORITY CAN CHANGE IN CERTAIN SITUATIONS, SUCH AS A CLUED CARD MOVING TO THE LOWEST PRIORITY WHEN IT IS
#KNOWN TO BE DEAD. THE LOWEST PRIORITY CARD IS CALLED CHOP BECAUSE IT IS UP NEXT FOR DISCARD.
priority=np.copy(table[:,::-1])

#############           PubInfo/ClueKinds*********************for logging public clue information
#THE COORDINATES (i,j,k) IN THE ARRAY PubInfo TELL WHETHER THE HOLDER OF THE CARD WITH SERIAL NUMBER
#i THINKS IT'S POSSIBLE, BASED ONLY ON PUBLIC INFORMATION (STACKS, CLUES, AND DISCARDS) FOR CARD i
#TO BE COLOR j AND NUMBER k. THE COORDINATES (i,j) IN THE ARRAY ClueKinds TELLS WHETHER CARD i HAS
#BEEN CLUED COLOR (j=0) OR NUMBER (j=1). IF A CLUED CARD IS KNOWN TO BE DISCARDABLE, THE COLOR VALUE
#CHANGES TO -1. ALL THIS AIDS IN DETERMINING priority WHEN INGENSTING A NEW CLUE OR DRAWING A NEW CARD.
PubInfo=np.ones(shape=(len(deck),ManyCol,5), dtype='uint8', order='C')
ClueKinds=np.zeros(shape=(len(deck),2),dtype='uint8',order='C')

 
####################################################################################################
#####################################PRIVATE DATA###################################################
####################################################################################################

#THE COORDINATES (i,j,k) IN THE ARRAY AllClues TELLS WHETHER PLAYER i CAN SEE THAT A COLOR j/NUMBER k
#CARD WAS EVER CLUED. WHEN A CLUED CARD IS DISCARDED, AllClues FORGETS IT WAS EVER CLUED.
AllClues=np.zeros(shape=(5,ManyCol,5),dtype='uint8',order='C')

#THE ROWS IN THE PRIVATE ARRAY status DESCRIBE EACH PERSPECTIVE ABOUT CARDS.
#NUMBERS AND STATUSES ARE TENTATIVE.
# -900) DISCARDED TO TRASH OR UNSUCCESSFULLY PLAYED
# -800) SURVIVOR, UNSAVED
# -700) ENDANGERED TWINLESS OR DOUBLE CHOP, UNSAVED
# -125) DEAD (UNKNOWN TO PLAYER)
# -100) DEAD (KNOWN TO PLAYER)
#  000) DEFAULT (DRAW PILE OR UNNOTEWORTHY)
#  025) MARKED (NUMBER AND/OR COLOR)
#  050) TWINLESS 4 (NOT ENDANGERED)
#  075) TWINLESS 3 (NOT ENDANGERED)
#  100) EXPECTED TO PLAY IN QUEUE
#  125) EXPECTED TO PLAY NEXT (NOT 250)
#  250) EXPECTED TO IGNITE CHAIN (UNMARKED)
#  900) PLAYED TO STACK
##################################status=np.zeros((5,len(deck)))



#THE PRIVATE ARRAY info HOLDS THE PERSPECTIVES OF EACH OF THE FIVE PLAYERS.
#COORDINATES (i,j,k) POINT TO AN 8 BIT STRING CORRESPONDING TO INFORMATION
#THAT PLAYER i KNOWS ABOUT PLAYER j'S HAND WITH RESPECT TO CLUE k. THE FIRST
#4 BITS HAVE NEGATIVE INFORMATION FOR THE CORRESPONDING 4 CARDS IN PLAYER j'S
#HAND. THE LAST 4 BITS HAVE POSITIVIE INFORMATION.
###################################info=np.ndarray(shape=(5,5,10), dtype=np.uint8, order='C')
#For 6 colors, need shape=5,11

#THE PRIVATE ARRAY metainfo HOLDS METAINFORMATION ABOUT EACH HAND, SIMILAR TO info.
#COORDINATES (i,j,k,l) DESCRIBE IN THE SAME WAY AS info WHAT PLAYER i THINKS
#PLAYER j KNOWS ABOUT PLAYER k'S HAND WITH RESPECT TO CLUE l.
#####################################metainfo=np.ndarray(shape=(5,5,5,10), dtype=np.uint8, order='C')

In [80]:
#TEST 5 COLOR CASE
def TEST():
    global deck, stacks, status, priority
    deck=[[0,4],[3,2],[1,1],[1,0],
          [0,1],[2,0],[2,3],[0,2],
          [4,0],[4,0],[3,3],[1,2],
          [1,4],[0,3],[0,2],[4,1],
          [0,0],[2,2],[1,3],[2,0]]+[[0,0]]*10
    status=[[1,0,0,0,0]]*ManyCol      #status edit?
    PrintDeckOut(deck)

In [81]:
#WHEN A CARD IS SUCCESSFULLY PLAYED, THE CORRESPONDING COLOR ROW IN stacks M SHIFT FORWARD.
#WHEN A 5 IS PLAYED, THAT COLOR ROW BECOMES A ROW OF ZEROS.
def play(color):
    stacks[color]=[0]+stacks[color][:-1]

#IN CASES OF AMBIGUITY, THE LATEST PLAYERS AFTER A CLUE ARE BEST TO RESPOND. Mod5SkipBack
#IS USED TO BACKWARD TRAVERSE PLAYERS WHO CAN RESPOND (CURRENTLY EXCLUDING skip, THE CLUE GIVER).
def Mod5SkipBack(value, skip, SkipAlso=-1):
    while True:
        value=(value-1)%5
        if value not in {skip,SkipAlso}:
            return value

#UPDATES PubInfo, ClueKinds, AND priority AFTER A CLUE IS GIVEN.
def RecordClue(player, value, ColOrNum):
    gather=[]                                  #gather will collect the clued cards
    if ColOrNum:                               #ColOrNum: =0 is color, =1 is number
        print('Recording Number %u to player %u'%(value+1, player)) #value+1 is external format
    else:
        print('Recording Color %c to player %u'%(colors[value], player))
    for card in range(4):                      #goes by pure chronological order to record clued cards
        serial=table[player,card]
        if deck[serial][ColOrNum]==value:
            gather.append(1)
            if stacks[deck[serial][0]].index(1)<=deck[serial][1]:
                ClueKinds[serial,ColOrNum]=1   #ClueKinds tracks whether cards are clued color or number
            else:
                ClueKinds[serial]=[-1,0]       #When a card is already played, it is marked as kind [-1,0]
        else: gather.append(0)                 #...to mean discard
    print('RecordClue updated which cards are clued and gathered:')
    print('ClueKinds:',[list(ClueKinds[card]) for card in table[player]])
    print('gather:',gather)
    if ColOrNum:                               #upd is the 2D array for how to update each card
        upd=[np.subtract([1,1,1,1],gather)]*5  #depending on color or number, it has to be applied
        upd[value]=gather                      #across all numbers and colors(resp.), hence the *5
        upd=np.transpose([upd]*ManyCol,axes=(2,0,1))  #and *ManyCol
    else:
        upd=[np.subtract([1,1,1,1],gather)]*ManyCol
        upd[value]=gather
        upd=np.transpose([upd]*5,axes=(2,1,0))
    PubInfo[table[player],:,:]=np.logical_and(PubInfo[table[player],:,:],upd)
    print('update array for player', player,':\n', upd)
    surry=[]                                   #surry is a surrogate for priority
    for s in [2,1,0,-1]:                       #possible numbers of clues; higher sum = higher priority 
        for card in [3,2,1,0]:                 #loop over cards in reverse chronological order
            serial=table[player,card]
            #print(serial, deck[serial],sum(ClueKinds[card]))
            if sum(ClueKinds[serial])==s:
                surry.append(serial)
    priority[player]=surry
    print('New priority for player %u is'%(player),surry,'\n\n')
def react(player, color, number, queue=[]):
    print('Looking for color %c and number %u in player %u'%(colors[color], number+1, player))
                                               #number+1 is external format
    print('and skipping serials', queue,'... priority is ', priority[player]) 
    for suspect in priority[player]:
        if suspect in queue:                   #skips cards already queued to play
            continue
        elif PubInfo[suspect,color,number]:    #returns highest priority card that is possibly color&number
            print('Returning', suspect)
            return suspect
    return -1

#Start at clue giver going backward to look for a possible chain starting at the next color
#card that needs to be played on color and ending at number-1. Once a card is found, look at
#that same player for the next card and keep going backward until number-1 is reached.
def respond(player, giver, color, number):
    print('Player %u responds to color %c and number %u from player %u'%(player, colors[color], number+1, giver))
    i=stacks[color].index(1)                    #...number +1 is external format
    queue=[]
    other=Mod5SkipBack(giver, player)
    print('The next card is numbered %u and the first player to search is %u'%(i+1, other)) #i+1 is external format
    fail=0                          ##Would like to allow giver to make chains involving cards in his hand
    while i<number:
        WouldPlay=react(other, color, i, queue)  #card that player 'other' would try to play if he thought
        print('Player %u would play %u in response'%(player, WouldPlay)) #...he was told to play [color,i]
        if deck[WouldPlay]==[color,i]:
            print('It matches color %c and number %u'%(colors[color], i+1)) #i+1 is external format
            queue.append(WouldPlay)
            i+=1                                 #success=traverse all hands looking for next number.
            fail=0
            print('New number and fail count are', i+1, fail)  #i+1 is external format
        else:
            print('It did not match color %c and number %u'%(colors[color], i+1)) #i+1 is external format
            other=Mod5SkipBack(other,player,giver)
            fail+=1
            print('New fail count is', fail)
        if fail==3:
            print('Did not find needed card in any visible hands. Appending next card to player', player)
            queue.append(react(player, color, i, queue))
            i+=1
            fail=0
            print('Looking for color %c and number %u.'%(colors[color],i+1)) #i+1 is external format
    print('Returning queue', queue, '\n\n')
    
    DeckOut=[colors[card[0]]+str(card[1]+1) for card in deck]
    if queue:
        print('Player %u is told that %s should play first'%(player,DeckOut[queue[0]]))
    if len(queue)>1:
        print('Then, %s should play second'%(DeckOut[queue[1]]))
    if len(queue)>2:
        print('Then, %s should play third'%(DeckOut[queue[2]]))
    if len(queue)>3:
        print('Then, %s should play fourth'%(DeckOut[queue[3]]))
    if len(queue)>4:
        print('Finally, %s should play last'%(DeckOut[queue[4]]))
    return queue

In [ ]:
TEST()              #TEST() is a good place to modify the deck, stacks, priorities, etc. and
                    #test out different scenarios.

play(0)             #Colors are valued 0=r,y,g,b,w=4.
play(0)             #Playing two reds makes r3 the next playable.

play(3)             #Playing three blues makes b4 the next playable
play(3)
play(3)


RecordClue(2,4,0)   #RecordClue(i,j,k) updates the arrays pertaining to public clue information
RecordClue(2,3,1)   #...when clue type k (k=0:color; k=1:number) of value j is given to player i.
                    #Colors and Numbers are ordered 0=r,y,g,b,w=4 and 0=1,2,3,4,5=4(resp.).
                    #e.g. The first RecordClue updates public information pertaining to a
                    #...w clue to player 2.

                    #e.g. The second RecordClue updates public information pertaining to a
                    #...4 clue to player 2.

respond(2,1,4,2)    #respond(i,j,k,l) evokes player i to respond to a color k/number l clue
                    #given by player j to, say, a 6th player that everyone can see.
                    #e.g. player 2 understands a "play 3w" clue (color=4, number=2) given
                    #...by player 1.