我编写了一款非常简单的基于终端的扑克游戏(ascii art ftw),现在它是多人游戏,但你基本上必须通过一台电脑。是否有一种简单的方法可以让两个人从不同的机器连接并同时访问同一款游戏?它不需要很花哨,也不需要是图形化的,只要我们有终端访问。
我不确定如何做到这一点,如果它是实用的,但只是想学习和探索一些选项。
这是一个非常模糊的问题,但我可以给你一些模糊的答案。
首先,需要设计一个简单的协议。一个非常简单的基于行的协议应该可以很好地工作:UTF-8文本,换行符分隔消息,空格分隔参数。例如,您可以有这些客户机->服务器消息:
JOIN name
SAY message with spaces
FOLD
RAISE amount
# ...
…和这些服务器->客户端消息:
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
# ...
这样一个协议的好处是,你可以用telnet
或nc
手动输入它,所以你甚至不需要客户端进行测试。
现在您需要构建一个实现该协议的服务器,并将游戏逻辑构建到服务器中。
在这里,线程服务器可能是最简单的。然后主线程启动一个游戏线程,这个线程大部分时间都在阻塞Condition
,等待玩家行动。它还阻塞accept
,为每个连接启动一个新的客户端线程,该线程大部分时间阻塞for line in self.sock.makefile():
。在客户端对象中添加Lock
,以允许其他线程安全地发送消息。然后你只需要一个客户端对象的集合和一个锁,你就完成了。
因为我有一个类似设计的聊天服务器,让我从中改编一些来给你一个骨架。
首先,这是整个主线程:
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))
Client
对象是一个标准的调度程序:
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('OKn')
您可能还需要一个broadcast
函数:
def broadcast(msg):
with lock:
for client in clients:
client.send(msg)
然后在Client
上为每个命令编写方法。基本上,菜单代码中的每个elif response == 'FOO'
都变成了一个do_FOO
方法,每个print
变成了一个broadcast
方法,仅此而已。我将在后面展示一个更复杂的,但下面是它们中的大多数:
def do_SAY(self, *msg):
broadcast('SAID {} {}'.format(self.number, ' '.join(msg)))
最后是Game
对象。它在自己的线程上运行,就像每个Client
一样。在大多数情况下,它的run
方法与你的顺序非网络游戏中的逻辑相同。当然,您必须调用broadcast
而不是print
,但这很容易。唯一棘手的一点是你需要一点同步。
例如,在开始一个新的手牌之前,你必须复制玩家列表(可能还有一些其他相关的游戏状态),这样其他线程可以在不影响当前游戏的情况下修改它,你还需要等到有足够的玩家,这样你就不会开始一个人玩自己的手牌。所以:
def new_hand(self):
with self.condition:
while len(self.players) < 2:
self.condition.wait()
players = self.players
# all your existing sequential logic
您需要添加一个join
方法,以便客户端从自己的线程中调用:
def join(self, player):
with self.condition:
self.players.append(self)
self.condition.notify()
在Client
对象中:
def do_JOIN(self, name):
self.name = name
game.join(self)
broadcast('JOINED {} {}'.format(self.number, self.name)
让我们让等待下注尽可能复杂,只是为了看看即使在最坏的情况下它是多么容易。如果你想抢先下注,你可以。每个人都可以看到你的赌注,如果情况发生变化,你就会承诺(例如,如果你打电话,那么你前面的人就会增加,你就会给他的新赌注打电话)。下面是我们要做的:
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
Client
如何提交赌注:
def bet(self, player, bet):
with self.condition:
self.bets[player] = bet
self.condition.notify()
例如,在Client
中:
def do_FOLD(self):
game.bet(self, 'fold')
显然有一堆代码要写。但关键是,除了上面已经展示的内容,或者你现有的游戏中已经存在的内容,没有什么复杂的了。
您需要托管某种类型的服务器,并编写一个程序来处理包含某些类型数据的请求,并将其通信回客户机。因为这不是一个实时的游戏,你不需要在TC/IP, UDP或任何东西上弄得太乱,简单的HTTP请求可能就可以了。
事实上,你甚至可以使用一个名为Scoreoid的免费服务。我用它来玩游戏。它是为高分排行榜而设计的,但它可能符合你的需求。它非常容易使用。由于API完全基于url工作,因此您可以使用标准库的urllib
模块。这可能是开始做这类事情的一个很好的方法