سؤال

I wrote a pretty simple terminal based poker game (ascii art ftw), and right now it's multiplayer, but you basically have to pass a single computer around. Is there a simple(ish) way to make it so two people could connect from separate machines and access the same game to play together at the same time? It doens't have to be fancy, and doesn't need to be graphical, as long as we have terminal access.

I'm not sure about how to do this or if it's practical, but just want to learn and explore some options.

هل كانت مفيدة؟

المحلول

This is a very vague question, but I can give you some vague answers.

First, you need to design a simple protocol. A very simple line-based protocol should work fine: UTF-8 text, newlines separating messages, spaces separating parameters. For example, you could have these client->server messages:

JOIN name
SAY message with spaces
FOLD
RAISE amount
# ...

… and these server->client messages:

OK
ERROR error message
JOINED player name with spaces
LEFT player
SAID player message with spaces
NEWHAND player player player player…
DEALT player face suit
ANTED player amount
CHECKED player
# ...

The nice thing about a protocol like this is that you can type it manually with telnet or nc, so you don't even need a client for testing.

Now you need to build a server that implements that protocol, and build the game logic into the server.

A threaded server may be the simplest thing here. Then main thread kicks off a game thread, which spends most of its time blocking on a Condition waiting for players to act. It also blocks on accept, kicking off a new client thread for each connection, which spends most of its time blocking on for line in self.sock.makefile():. Add a Lock inside the client object to allow other threads to safely send messages. Then you just need a collection of client objects with a lock around it, and you're done.

Since I've got a chat server sitting around with a similar design, let me adapt some bits out of it to give you a skeleton.

First, here's the entire main thread:

lock = threading.Lock()
clients = []
game = Game()

ssock = socket.socket()
ssock.bind(('', 12345))
ssock.listen(5)
while True:
    sock, addr = ssock.accept()
    with lock:
        clients.append(Client(addr, sock, len(clients))

The Client object is a standard dispatcher:

class Client(object):
    def __init__(self, addr, sock, number):
        self.sock = sock
        self.name = '<{}> (not logged in)'.format(addr)
        self.number = number
        self.lock = threading.Lock()
        self.thread = threading.Thread(target=self.serve)
        self.thread.start()

    def send(self, msg):
        with self.lock:
            self.sock.send(msg)

    def run(self):
        for line in self.sock.makefile():
            args = line.rstrip().split()
            cmd = args.pop().upper()
            method = getattr(self, 'do_{}'.format(cmd), None)
            if method is none:
                self.write('ERROR unknown command {}\n'.format(cmd))
            else:
                try:
                    method(*args)
                except Exception as e:
                    self.send('ERROR in {}: {}\n'.format(cmd, e))
                else:
                    self.send('OK\n')

You probably also want a broadcast function:

def broadcast(msg):
    with lock:
        for client in clients:
            client.send(msg)

Then you write methods on Client for each command. Basically, each elif response == 'FOO' you had in your menu code becomes a do_FOO method, and each print becomes a broadcast, and… that's about it. I'll show a more complicated one later, but here's what most of them will look like:

def do_SAY(self, *msg):
    broadcast('SAID {} {}'.format(self.number, ' '.join(msg)))

Finally, there's the Game object. This runs on its own thread, just like each Client. For the most part, its run method is the same logic as in your sequential, non-networked game. Of course you have to call broadcast instead of print, but that's easy. The only tricky bit is that you need a bit of synchronization.

For example, before starting a new hand, you have to copy the list of players (and maybe some other related game state) so other threads can modify it without affecting the current game, and you also need to wait until there are enough players so you don't go starting hands with 1 person playing himself. So:

def new_hand(self):
    with self.condition:
        while len(self.players) < 2:
            self.condition.wait()
        players = self.players
    # all your existing sequential logic

And you need to add a join method for the clients to call from their own threads:

def join(self, player):
    with self.condition:
        self.players.append(self)
        self.condition.notify()

So, in the Client object:

def do_JOIN(self, name):
    self.name = name
    game.join(self)
    broadcast('JOINED {} {}'.format(self.number, self.name)

Let's make waiting for bets as complicated as possible, just to see how easy it is even in the worst case. If you want to bet out of turn, you can. Everyone can see your bet, and if circumstances change, you're committed (e.g., if you call, then the guy ahead of you raises, you're calling his new bet). So, here's what we do:

def wait_for_bets(self, bettor):
    with self.condition:
        while self.bets[self.bettor] is None:
            self.condition.wait()
        bettor, bet = self.bettor, self.bets[self.bettor]
        self.bets[self.bettor] = None
    # handle the bet

And here's how a Client submits a bet:

def bet(self, player, bet):
    with self.condition:
        self.bets[player] = bet
        self.condition.notify()

For example, in Client:

def do_FOLD(self):
    game.bet(self, 'fold')

Obviously there's a bunch of code to write. But the point is that there's nothing complicated beyond what's already shown above, or already in your existing game.

نصائح أخرى

You would need to host a server of some kind, and write a program than handles requests containing certain kinds of data, and communicates it back to the client. Since this isn't a real time game, you don't need to mess around too much with TC/IP, UDP, or anything, simple HTTP requests will probably be fine.

In fact, you can even use a free service called Scoreoid. I'm using it for my games. It's designed for high score leaderboards, but it'll probably suit your needs. It's super easy to use. Since the API works entirely out of URLs, you can just use the standard library's urllib modules. This is probably a very good way to get started with this sort of thing.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top