diff --git a/README.md b/README.md index bcfcc4b..e3b5f5e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,167 @@ -nertz -===== - -Implement rules for Nertz card game +# NERTZ! +A card game thats like multiplayer solitare. + +Quick project that I did after talking to an old family friend about it. + +Project will be split into 3 parts: + +## Coding the game (finished) + +I will write the rules for the game in python while my friend writes a java version + +## Coding server {work in progress} + +Thinking of using event driven model, I started writing some code but I will +probably have to change things up since client code must run in safari browser +on ipad + +## UI + +This is the hard part for us since neither of us have any experience with UI. +Our idea is that we will run our program inside a touch enabled browser and +connect to external computer on lan that acts as a server. + +# Example +``` +$ python +Python 2.6.6 (r266:84292, Sep 15 2010, 16:22:56) +[GCC 4.4.5] on linux2 +Type "help", "copyright", "credits" or "license" for more information. +>>> from nertzplayer import * #import functions into namespace +>>> game = start_game() +testing Score: 0 Cards remaining: 12 +0F | Finished: 0 +-N 5♣ -- -- -- -- -- -- -- -- -- -- -- | Cards left: 12 +1T T♣ +2T A♣ +3T 4♢ +4T 4♣ +-S J♣ Kâ™  9♢ +>>> move(game, 2, 0) +testing Score: 0 Cards remaining: 12 +0F A♣ | Finished: 0 +-N 5♣ -- -- -- -- -- -- -- -- -- -- -- | Cards left: 12 +1T T♣ +2T None! +3T 4♢ +4T 4♣ +-S J♣ Kâ™  9♢ +>>> move(game, 'n', 2) +testing Score: 0 Cards remaining: 11 +0F A♣ | Finished: 0 +-N 8♢ -- -- -- -- -- -- -- -- -- -- | Cards left: 11 +1T T♣ +2T 5♣ +3T 4♢ +4T 4♣ +-S J♣ Kâ™  9♢ +>>> draw(game) +testing Score: 0 Cards remaining: 11 +0F A♣ | Finished: 0 +-N 8♢ -- -- -- -- -- -- -- -- -- -- | Cards left: 11 +1T T♣ +2T 5♣ +3T 4♢ +4T 4♣ +-S 9♡ 3♡ 3â™  +>>> move(game, 's', 1) +testing Score: 0 Cards remaining: 11 +0F A♣ | Finished: 0 +-N 8♢ -- -- -- -- -- -- -- -- -- -- | Cards left: 11 +1T 9♡ T♣ +2T 5♣ +3T 4♢ +4T 4♣ +-S 3♡ 3â™  J♣ +>>> draw(game) +testing Score: 0 Cards remaining: 11 +0F A♣ | Finished: 0 +-N 8♢ -- -- -- -- -- -- -- -- -- -- | Cards left: 11 +1T 9♡ T♣ +2T 5♣ +3T 4♢ +4T 4♣ +-S K♡ T♢ Qâ™  +>>> draw(game) +testing Score: 0 Cards remaining: 11 +0F A♣ | Finished: 0 +-N 8♢ -- -- -- -- -- -- -- -- -- -- | Cards left: 11 +1T 9♡ T♣ +2T 5♣ +3T 4♢ +4T 4♣ +-S 4â™  7♡ 7â™  +>>> draw(game) +testing Score: 0 Cards remaining: 11 +0F A♣ | Finished: 0 +-N 8♢ -- -- -- -- -- -- -- -- -- -- | Cards left: 11 +1T 9♡ T♣ +2T 5♣ +3T 4♢ +4T 4♣ +-S 8♣ 5♢ Jâ™  +>>> draw(game) +testing Score: 0 Cards remaining: 11 +0F A♣ | Finished: 0 +-N 8♢ -- -- -- -- -- -- -- -- -- -- | Cards left: 11 +1T 9♡ T♣ +2T 5♣ +3T 4♢ +4T 4♣ +-S Q♣ 4♡ 9â™  +>>> draw(game) +testing Score: 0 Cards remaining: 11 +0F A♣ | Finished: 0 +-N 8♢ -- -- -- -- -- -- -- -- -- -- | Cards left: 11 +1T 9♡ T♣ +2T 5♣ +3T 4♢ +4T 4♣ +-S Tâ™  2â™  Aâ™  +>>> draw(game) +testing Score: 0 Cards remaining: 11 +0F A♣ | Finished: 0 +-N 8♢ -- -- -- -- -- -- -- -- -- -- | Cards left: 11 +1T 9♡ T♣ +2T 5♣ +3T 4♢ +4T 4♣ +-S A♡ 8♡ 3♢ +>>> move(game, 3, 2) +testing Score: 0 Cards remaining: 11 +0F A♣ | Finished: 0 +-N 8♢ -- -- -- -- -- -- -- -- -- -- | Cards left: 11 +1T 9♡ T♣ +2T 4♢ 5♣ +3T None! +4T 4♣ +-S A♡ 8♡ 3♢ +>>> move(game, 's', 0) +testing Score: 0 Cards remaining: 11 +0F A♣ A♡ | Finished: 0 +-N 8♢ -- -- -- -- -- -- -- -- -- -- | Cards left: 11 +1T 9♡ T♣ +2T 4♢ 5♣ +3T None! +4T 4♣ +-S 8♡ 3♢ Tâ™  +>>> draw(game) +testing Score: 0 Cards remaining: 11 +0F A♣ A♡ | Finished: 0 +-N 8♢ -- -- -- -- -- -- -- -- -- -- | Cards left: 11 +1T 9♡ T♣ +2T 4♢ 5♣ +3T None! +4T 4♣ +-S K♣ T♡ 9♣ +>>> move(game, 's', 3) +testing Score: 0 Cards remaining: 11 +0F A♣ A♡ | Finished: 0 +-N 8♢ -- -- -- -- -- -- -- -- -- -- | Cards left: 11 +1T 9♡ T♣ +2T 4♢ 5♣ +3T K♣ +4T 4♣ +-S T♡ 9♣ 8♡ +>>> +``` \ No newline at end of file diff --git a/nertz.py b/nertz.py new file mode 100644 index 0000000..618d40a --- /dev/null +++ b/nertz.py @@ -0,0 +1,273 @@ +# Nertz! card game, rules based on julie's variant +# Author: Ying Wu working w/Michael Chen +# Last updated: Jan 2014 + +# todo: activity log for rollback +# make private/public variables and function names +# http://stackoverflow.com/questions/70528/why-are-pythons-private-methods-not-actually-private +# http://www.diveintopython.net/object_oriented_framework/private_functions.html +# follow style guide more closely +# http://www.python.org/dev/peps/pep-0008/ +# maybe clean up some self vs class stuff +# http://stackoverflow.com/questions/12031571/self-variable-in-python-vs-class-variable +# handling no possible movie scenario +# remember to exit and re-import when testing +# +# resources: +# http://www.tutorialspoint.com/python/python_functions.htm +# http://www.diveintopython.net/object_oriented_framework/defining_classes.html +# http://docs.python.org/3/tutorial/classes.html +# http://docs.python.org/2/tutorial/classes.html +# http://www.joelonsoftware.com/articles/Unicode.html +# http://docs.python.org/2/tutorial/modules.html + +# convert numerical format to cards +# cards will be stored as follows: +# Clubs 1-K = 0-12 +# Diamonds 1-K = 13-25 +# Hearts 1-K = 26-38 +# Spades 1-K = 39-51 +# use card%13 to get card (0-9 is 1-10, 10 is J, 11 is Q, 12 is K) +# use card/13 to get suit (0 = clubs, 1 = diamonds, 2 = hearts, 3 = spades) +def get_card(num) : + + suitchar = "_" + numchar = "0" + cardnum = num%13 + cardsuit = num/13 + + if cardsuit > 3 or cardsuit < 0 : + raise Exception("Invalid card suit") + if cardnum > 12 or cardnum < 0 : + raise Exception("Invalid card number") + + # todo: catch unicode for windows + # http://stackoverflow.com/questions/6725249/how-to-print-a-unicode-string-in-python-in-windows-cmd + # if sys.platform == "win32": + + # http://unicode-table.com/en/sets/suits-of-the-cards/ + if cardsuit == 0 : # clubs + suitchar = u"\u2663" # u2667 + elif cardsuit == 1 : # diamonds + suitchar = u"\u2662" # u2666 + elif cardsuit == 2 : # hearts + suitchar = u"\u2661" # u2665 + elif cardsuit == 3 : # spades + suitchar = u"\u2660" # u2664 + + if cardnum == 0 : + numchar = 'A' + elif cardnum == 9 : + numchar = 'T' + elif cardnum == 10 : + numchar = 'J' + elif cardnum == 11 : + numchar = 'Q' + elif cardnum == 12 : + numchar = 'K' + else : + numchar = str(cardnum+1) + + return numchar+suitchar + +def shuffle_deck(seed = None) : + # http://docs.python.org/2/library/random.html + # alternative: http://code.activestate.com/recipes/272884-random-samples-without-replacement/ + + import random + if seed != None : random.seed(seed) #for testing + return random.sample(range(52), 52) + + ''' + # old way + deck_clubs = random.sample(range(1, 13), 13 ) + deck_hearts = random.sample(range(1, 13), 13 ) + deck_diamonds = random.sample(range(1, 13), 13 ) + deck_spades = random.sample(range(1, 13), 13 ) + + deck_hearts = deck_hearts + 20 + deck_diamonds = deck_diamonds + 40 + deck_spades = deck_spades + 60 + + deck = zip(deck_clubs, deck_hearts, deck_diamonds, deck_spades) + ''' + +class Reserve: + + def __init__(self, cards): + self.deck = cards + + def show(self): + print "-N\t"+get_card(self.deck[1]) + " --"*(len(self.deck)-1) + " | Cards left: " + str(len(self.deck)) + + def move(self, dest_deck): + if(dest_deck.add(self.deck[1])): + self.deck.pop(1) + return len(self.deck) + else: + raise Exception("Not a valid move") + +class Tableau: + + def __init__(self, card, num): + self.deck = [card] + self.num = num # tableau number + + def add(self, card): + if(len(self.deck) == 0): + self.deck.append(card) + return 1 + elif(card in self.next_card(self.deck[len(self.deck)-1])): + self.deck.append(card) + return len(self.deck) # all numbers greater than 0 are true + # http://docs.python.org/2/library/stdtypes.html#truth-value-testing + else: + raise Exception("Not a valid move") + return 0 + + def move(self, dest_deck): # could be extension of deck class + if(dest_deck.add(self.deck[len(self.deck)-1])): + self.deck.pop() + return len(self.deck) + else: + raise Exception("Not a valid move") + + def show(self): #card on left will be bottom card + if(len(self.deck) > 0) : + # use list comprehension and also index backwards + # http://docs.python.org/2/tutorial/datastructures.html#list-comprehensions + # http://stackoverflow.com/questions/4048964/printing-tab-separated-values-of-a-list + print(self.num+"T\t"+' '.join([get_card(self.deck[i]) for i in range(len(self.deck)-1,-1,-1)])) + else: + print self.num+"T\tNone!" + + def next_card(self, card): # alternating suit of lower number + cur_suit = card/13 + cur_num = card%13 + + match = [-1, -1] + + if(cur_num == 0): # Aces + raise Exception("Cannot add card to ace") + + if(cur_suit in [0,3]): # black + match[0] = cur_num-1 + 13*1 + match[1] = cur_num-1 + 13*2 + elif(cur_suit in [1,2]): # red + match[0] = cur_num-1 + match[1] = cur_num-1 + 13*3 + else: + raise Exception("Invalid card suit") + + return match + +# cards you draw from, waste deck is combined into this class +class Stock: + + def __init__(self, cards): + self.deck = cards + self.current = 2 + + def draw(self): + self.current += 3 + if self.current >= len(self.deck): + print "END OF DECK!" + self.current = 2 + if len(self.deck) < 3: #catch rare edge case + self.current = len(self.deck)-1 + if len(self.deck) == 0: #catch rare edge case + raise Exception("Deck is empty!") + + def move(self, dest_deck): # could be extension of deck class + if(dest_deck.add(self.deck[self.current])): + self.deck.pop(self.current) + self.current -= 1 + if(self.current < 0): #edge case + self.current = 2 + return len(self.deck) + else: + raise Exception("Not a valid move") + + def show(self): + print("-S\t"+' '.join([nertz.get_card(deck[i]) for i in range(self.current, max(-1,self.current-3), -1)])) + ''' + # does not account for case where current is < 2 + print("-S\t"+get_card(self.deck[self.current])+ + ' '+get_card(self.deck[self.current-1])+ + ' '+get_card(self.deck[self.current-2])) + ''' + +# Only hold top card +class Foundation: + + def __init__(self): + self.decks = [] + self.points = 0 + self.finished = 0 # fun stat + + def add(self, card): + if(self.update(card)): + self.points += 1 + return True + else: + raise Exception("Not a valid move") + + def update(self, card): + if(card%13 == 0): # Aces + #self.decks.append([card]) + self.decks.append(card) + return len(self.decks) + else: #search + if(self.decks.count(card-1)): + i = self.decks.index(card-1) + self.decks[i] += 1 + if(card%13 == 12): #King + self.decks.pop(i) + self.finished += 1 + return True + return False + + def show(self): + print("0F\t"+' '.join([get_card(self.decks[i]) for i in range(len(self.decks))]) + + " | Finished: " + str(self.finished)) + +# player +class Player: + + def __init__(self, name = "nertz player", seed = None): + + self.name = name + self.score = 0 + self.winner = [] + + if(seed): + self.deck = shuffle_deck(seed) + else: + self.deck = shuffle_deck() + self.reserve_deck = Reserve(self.deck[0:12]) + self.stock_deck = Stock(self.deck[16:52]) + self.foundation_local = Foundation() + + # create 4 empty Tableaus + self.tableau_deck = [] + self.tableau_deck.append(Tableau(self.deck[12], "1")) + self.tableau_deck.append(Tableau(self.deck[13], "2")) + self.tableau_deck.append(Tableau(self.deck[14], "3")) + self.tableau_deck.append(Tableau(self.deck[15], "4")) + + self.show() + + def show(self): + print self.name + "\tScore: " + str(self.score) + \ + "\tCards remaining: " + str(len(self.reserve_deck.deck)) + self.foundation_local.show() + self.reserve_deck.show() + for i in range(4): #len(self.tableau_deck)): + self.tableau_deck[i].show() + self.stock_deck.show() + + def game_over(win) : + score += foundation_local.points - len(reserve_deck.deck) + winner.append(win) + return score + diff --git a/nertzplayer.py b/nertzplayer.py new file mode 100644 index 0000000..b786e03 --- /dev/null +++ b/nertzplayer.py @@ -0,0 +1,58 @@ +# Actions that will be replaced by gui +# All possible commands from gui + +# start game as single player client + hosting + +import nertz + +def start_game(): + return nertz.Player("testing", 123) #todo: add in server status + +# draw next 3 cards and print cards +def draw(game): + game.stock_deck.draw() + game.show() + +# move card has 6 possibilities: +# 1. stock to tableau +# 2. stock to foundation +# 3. reserve to tableau +# 4. reserve to foundation +# 5. tableau to tableau +# 6. tableau to foundation +# move card from waste to tableau (1-4) or foundation (0) +def move_stock(game, num): + if num == 0: + game.stock_deck.move(game.foundation_local) + else: + game.stock_deck.move(game.tableau_deck[num-1]) + game.show() + +def move_reserve(game, num): + if num == 0: + game.reserve_deck.move(game.foundation_local) + else: + game.reserve_deck.move(game.tableau_deck[num-1]) + game.show() + +def move_tableau(game, tableau_num, num): + if num == 0: + game.tableau_deck[tableau_num-1].move(game.foundation_local) + else: + game.tableau_deck[tableau_num-1].move(game.tableau_deck[num-1]) + game.show() + +def move(game, type, dest): + if type == "r" or type == "n" or type == "N": + move_reserve(game, dest) + elif type == "1T" or type == 1: + move_tableau(game, 1, dest) + elif type == "2T" or type == 2: + move_tableau(game, 2, dest) + elif type == "3T" or type == 3: + move_tableau(game, 3, dest) + elif type == "4T" or type == 4: + move_tableau(game, 4, dest) + elif type == 's' or type == 'S' or type == 'stock': + move_stock(game, dest) + diff --git a/nertzserver.py b/nertzserver.py new file mode 100644 index 0000000..dc35302 --- /dev/null +++ b/nertzserver.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +# Work in progress, see nertz.py for details + +# When acting as nertz server this program will keep track of +# 1. total number of players +# 2. name of all players +# 3. score for all players +# 4. current global foundation +# +# Functions: +# 1. init (playernames) +# 2. declare winner +# 3. calculate and update score +# 4. track/update reserve +# 5. notify reserve updates +# 6. track/update foundation +# 7. notify foundation updates (and conflicts) +# +# Win conditions are by numnber of games and score +# server should store local copy +# +# Nertz game can start in 3 modes +# 1. host + player +# 2. player +# 3. host only + +# guide +# http://ilab.cs.byu.edu/python/socketmodule.html +# other reading +# http://sourceforge.net/projects/eventdrivenpgm/files/ +# http://www.pythonschool.net/eventdrivenprogramming + +import select +import socket +import sys +import threading + +class nertz_server: + + def __init__(self, port = 58692, numplayers): + self.host = '' + self.port = port + self.backlog = 5 + self.size = 1024 + self.server = None + self.threads = [] + + self.playernum = numplayers + self.playernames = [] + self.playerscore = [] #2d list + + # command queue + + def add_player: + # check if game started + + def start_game: + + def get_scores: + +class nertz_client: + + def __init__(self): + + + +# based on function from nertz.py +# keep track of what player the card came from +class Foundation: + + def __init__(self): + self.decks = [] + self.points = 0 + self.finished = 0 # fun stat + + def add(self, card): + if(self.update(card)): + self.points += 1 + return True + else: + raise Exception("Not a valid move") + + def update(self, card): + if(card%13 == 0): # Aces + #self.decks.append([card]) + self.decks.append(card) + return len(self.decks) + else: #search + if(self.decks.count(card-1)): + i = self.decks.index(card-1) + self.decks[i] += 1 + if(card%13 == 12): #King + self.decks.pop(i) + self.finished += 1 + return True + return False + + def show(self): + print("0F\t"+' '.join([get_card(self.decks[i]) for i in range(len(self.decks))]) + + " | Finished: " + str(self.finished)) + + diff --git a/outline.md b/outline.md new file mode 100644 index 0000000..b98e81d --- /dev/null +++ b/outline.md @@ -0,0 +1,57 @@ +#NERTS! + +## Reserve + + **Layman’s Terms** + +The (12) card pile you must empty to win Nerts. + +The top card may be faceup, while the remaining cards must be facedown. + +You may move the top card in your Reserve to the top of one of your Tableau piles or the top of a Foundation pile (see Tableau and Foundation for restrictions). + +## Tableau + + **Layman’s Terms** + +The four faceup (1) card piles that you may “build down” on. + +You may move the top card from your Reserve or Waste to one of your Tableau piles IF its suit alternates colors with AND its value is exactly one lower than that Tableau’s top card. + +You may move the top card in your Tableau piles to the top of a Foundation pile (see Foundation for restrictions). + +## Stock + + **Layman’s Terms** + +The facedown (36) card pile (of cards not distributed to your Reserve and Tableau piles). + +You may flip three cards (or less, if fewer remain) from your Stock faceup into your Waste. + +You may flip your Waste facedown into your Stock ONLY IF the Stock is empty. + +## Waste + + **Layman’s Terms** + +The faceup (0) card pile that you may flip cards from your Stock into. + +You may move the top card in your Waste to the top of one of your Tableau piles or the top of a Foundation pile (see Tableau and Foundation for restrictions). + +You may flip your Waste facedown into your Stock ONLY IF the Stock is empty. + +## Foundation + + **Layman’s Terms** + +Suited, faceup card piles that anyone may “create” and “build up” on. + +You may move the top card in your Reserve, one of your Tableau piles, or your Waste to create a new Foundation pile of the same suit IF it is an Ace. + +You may move the top card in your Reserve, one of your Tableau piles, or your Waste to the top of a Foundation pile IF its suit matches the Foundation AND its value is exactly one higher than the Foundation pile’s top card. + +## Resources: + +http://en.wikipedia.org/wiki/Nertz + +http://www.pagat.com/patience/nerts.html