Finnish first socket server protype

This commit is contained in:
Nguyễn Gia Phong 2018-02-26 21:02:11 +07:00
parent bc47fb3f30
commit 3e85f0c3a1
6 changed files with 213 additions and 88 deletions

View file

@ -86,11 +86,12 @@ class Hero:
if abs(self.spin_queue) > 0.5: if abs(self.spin_queue) > 0.5:
self.angle += sign(self.spin_queue) * pi / 2 / self.spin_speed self.angle += sign(self.spin_queue) * pi / 2 / self.spin_speed
self.spin_queue -= sign(self.spin_queue) self.spin_queue -= sign(self.spin_queue)
else:
# Follow the mouse cursor def update_angle(self, angle):
x, y = pygame.mouse.get_pos() """Turn to the given angle if the hero is not busy slashing."""
self.angle = atan2(y - self.y, x - self.x) if abs(self.spin_queue) <= 0.5:
self.spin_queue = 0.0 self.spin_queue = 0.0
self.angle = angle
def draw(self): def draw(self):
"""Draw the hero.""" """Draw the hero."""
@ -238,13 +239,18 @@ class Enemy:
x, y = self.get_pos() x, y = self.get_pos()
return atan2(y - self.maze.y, x - self.maze.x) return atan2(y - self.maze.y, x - self.maze.x)
def get_color(self):
"""Return current color of the enemy."""
return TANGO[self.color][int(self.wound)] if self.awake else FG_COLOR
def draw(self): def draw(self):
"""Draw the enemy.""" """Draw the enemy."""
if get_ticks() < self.maze.next_move and not self.awake: return if get_ticks() < self.maze.next_move and not self.awake: return
radious = self.maze.distance/SQRT2 - self.awake*2 radious = self.maze.distance/SQRT2 - self.awake*2
square = regpoly(4, radious, self.angle, *self.get_pos()) square = regpoly(4, radious, self.angle, *self.get_pos())
color = TANGO[self.color][int(self.wound)] if self.awake else FG_COLOR fill_aapolygon(self.maze.surface, square, self.get_color())
fill_aapolygon(self.maze.surface, square, color)
def update(self): def update(self):
"""Update the enemy.""" """Update the enemy."""

View file

@ -19,6 +19,8 @@
__doc__ = 'brutalmaze module for shared constants' __doc__ = 'brutalmaze module for shared constants'
from string import ascii_lowercase
from pkg_resources import resource_filename as pkg_file from pkg_resources import resource_filename as pkg_file
import pygame import pygame
from pygame.mixer import Sound from pygame.mixer import Sound
@ -69,6 +71,9 @@ TANGO = {'Butter': ((252, 233, 79), (237, 212, 0), (196, 160, 0)),
(136, 138, 133), (85, 87, 83), (46, 52, 54))} (136, 138, 133), (85, 87, 83), (46, 52, 54))}
ENEMIES = ['Butter', 'Orange', 'Chocolate', 'Chameleon', ENEMIES = ['Butter', 'Orange', 'Chocolate', 'Chameleon',
'SkyBlue', 'Plum', 'ScarletRed'] 'SkyBlue', 'Plum', 'ScarletRed']
COLOR_CODE = ascii_lowercase + '0'
COLORS = {c: COLOR_CODE[i] for i, c in enumerate(
color for code in ENEMIES + ['Aluminium'] for color in TANGO[code])}
MINW, MAXW = 24, 36 MINW, MAXW = 24, 36
ENEMY_HP = 3 ENEMY_HP = 3
HERO_HP = 5 HERO_HP = 5

View file

@ -26,7 +26,10 @@ try: # Python 3
from configparser import ConfigParser from configparser import ConfigParser
except ImportError: # Python 2 except ImportError: # Python 2
from ConfigParser import ConfigParser from ConfigParser import ConfigParser
from itertools import repeat
from math import atan2, degrees
from os.path import join, pathsep from os.path import join, pathsep
from socket import socket
from sys import stdout from sys import stdout
@ -35,9 +38,9 @@ from pygame import DOUBLEBUF, KEYDOWN, OPENGL, QUIT, RESIZABLE, VIDEORESIZE
from pygame.time import Clock, get_ticks from pygame.time import Clock, get_ticks
from appdirs import AppDirs from appdirs import AppDirs
from .constants import SETTINGS, ICON, MUSIC, HERO_SPEED from .constants import SETTINGS, ICON, MUSIC, HERO_SPEED, COLORS, WALL
from .maze import Maze from .maze import Maze
from .misc import sign from .misc import round2, sign
class ConfigReader: class ConfigReader:
@ -58,8 +61,33 @@ class ConfigReader:
self.config.read(SETTINGS) # default configuration self.config.read(SETTINGS) # default configuration
self.config.read(filenames) self.config.read(filenames)
def parse_output(self): # Fallback to None when attribute is missing
"""Parse graphics and sound configurations.""" def __getattr__(self, name): return None
def parse(self):
"""Parse configurations."""
self.server = self.config.getboolean('Server', 'Enable')
if self.server:
self.host = self.config.get('Server', 'Host')
self.port = self.config.getint('Server', 'Port')
self.headless = self.config.getboolean('Server', 'Headless')
else:
self.key, self.mouse = {}, {}
for cmd, alias in self.CONTROL_ALIASES:
i = self.config.get('Control', cmd)
if re.match('mouse[1-3]$', i.lower()):
if alias not in ('shot', 'slash'):
raise ValueError(self.WEIRD_MOUSE_ERR.format(cmd))
self.mouse[alias] = int(i[-1]) - 1
continue
if len(i) == 1:
self.key[alias] = ord(i.lower())
continue
try:
self.key[alias] = getattr(pygame, 'K_{}'.format(i.upper()))
except AttributeError:
raise ValueError(self.INVALID_CONTROL_ERR.format(cmd, i))
self.size = (self.config.getint('Graphics', 'Screen width'), self.size = (self.config.getint('Graphics', 'Screen width'),
self.config.getint('Graphics', 'Screen height')) self.config.getint('Graphics', 'Screen height'))
self.opengl = self.config.getboolean('Graphics', 'OpenGL') self.opengl = self.config.getboolean('Graphics', 'OpenGL')
@ -67,24 +95,6 @@ class ConfigReader:
self.muted = self.config.getboolean('Sound', 'Muted') self.muted = self.config.getboolean('Sound', 'Muted')
self.musicvol = self.config.getfloat('Sound', 'Music volume') self.musicvol = self.config.getfloat('Sound', 'Music volume')
def parse_control(self):
"""Parse control configurations."""
self.key, self.mouse = {}, {}
for cmd, alias in self.CONTROL_ALIASES:
i = self.config.get('Control', cmd)
if re.match('mouse[1-3]$', i.lower()):
if alias not in ('shot', 'slash'):
raise ValueError(self.WEIRD_MOUSE_ERR.format(cmd))
self.mouse[alias] = int(i[-1]) - 1
continue
if len(i) == 1:
self.key[alias] = ord(i.lower())
continue
try:
self.key[alias] = getattr(pygame, 'K_{}'.format(i.upper()))
except AttributeError:
raise ValueError(self.INVALID_CONTROL_ERR.format(cmd, i))
def read_args(self, arguments): def read_args(self, arguments):
"""Read and parse a ArgumentParser.Namespace.""" """Read and parse a ArgumentParser.Namespace."""
for option in 'size', 'opengl', 'max_fps', 'muted', 'musicvol': for option in 'size', 'opengl', 'max_fps', 'muted', 'musicvol':
@ -94,29 +104,108 @@ class ConfigReader:
class Game: class Game:
"""Object handling main loop and IO.""" """Object handling main loop and IO."""
def __init__(self, size, scrtype, max_fps, muted, musicvol, key, mouse): def __init__(self, config):
pygame.mixer.pre_init(frequency=44100) pygame.mixer.pre_init(frequency=44100)
pygame.init() pygame.init()
if muted: if config.muted or config.headless:
pygame.mixer.quit() pygame.mixer.quit()
else: else:
pygame.mixer.music.load(MUSIC) pygame.mixer.music.load(MUSIC)
pygame.mixer.music.set_volume(musicvol) pygame.mixer.music.set_volume(config.musicvol)
pygame.mixer.music.play(-1) pygame.mixer.music.play(-1)
pygame.display.set_icon(ICON) pygame.display.set_icon(ICON)
pygame.fastevent.init()
if config.server:
self.socket = socket()
self.socket.bind((config.host, config.port))
self.socket.listen()
else:
pygame.fastevent.init()
self.server, self.headless = config.server, config.headless
# self.fps is a float to make sure floordiv won't be used in Python 2 # self.fps is a float to make sure floordiv won't be used in Python 2
self.max_fps, self.fps = max_fps, float(max_fps) self.max_fps, self.fps = config.max_fps, float(config.max_fps)
self.musicvol = musicvol self.musicvol = config.musicvol
self.key, self.mouse = key, mouse self.key, self.mouse = config.key, config.mouse
self.maze = Maze(max_fps, size, scrtype) scrtype = (config.opengl and DOUBLEBUF|OPENGL) | RESIZABLE
self.maze = Maze(config.max_fps, config.size, scrtype, config.headless)
self.hero = self.maze.hero self.hero = self.maze.hero
self.clock, self.paused = Clock(), False self.clock, self.paused = Clock(), False
def __enter__(self): return self def __enter__(self): return self
def expos(self, x, y):
"""Return position of the given coordinates in rounded percent."""
cx = (x+self.maze.x-self.maze.centerx) / self.maze.distance * 100
cy = (y+self.maze.y-self.maze.centery) / self.maze.distance * 100
return round2(cx), round2(cy)
def export(self):
"""Export maze data to a bytes object."""
maze, hero, tick, ne = self.maze, self.hero, get_ticks(), 0
walls = [[1 if maze.map[x][y] == WALL else 0 for x in maze.rangex]
for y in maze.rangey]
x, y = self.expos(maze.x, maze.y)
lines = deque(['{} {} {:.0f} {:d} {:d} {:d}'.format(
x, y, hero.wound * 100, hero.next_strike <= tick,
hero.next_heal <= tick, maze.next_move <= tick)])
for enemy in maze.enemies:
if not enemy.awake:
walls[enemy.y-maze.rangey[0]][enemy.x-maze.rangex[0]] = WALL
continue
elif enemy.color == 'Chameleon' and maze.next_move <= tick:
continue
x, y = self.expos(*enemy.get_pos())
lines.append('{} {} {} {:.0f}'.format(COLORS[enemy.get_color()],
x, y, degrees(enemy.angle)))
ne += 1
if maze.next_move <= tick:
rows = (''.join(str(cell) for cell in row) for row in walls)
else:
rows = repeat('0' * len(maze.rangex), len(maze.rangey))
lines.appendleft('\n'.join(rows))
for bullet in maze.bullets:
x, y = self.expos(bullet.x, bullet.y)
lines.append('{} {} {} {:.0f}'.format(COLORS[bullet.get_color()],
x, y, degrees(bullet.angle)))
lines.appendleft('{} {} {}'.format(len(walls), ne, len(maze.bullets)))
return '\n'.join(lines).encode()
def meta(self):
"""Handle meta events on Pygame window.
Return False if QUIT event is captured, True otherwise.
"""
events = pygame.fastevent.get()
for event in events:
if event.type == QUIT:
return False
elif event.type == VIDEORESIZE:
self.maze.resize((event.w, event.h))
elif event.type == KEYDOWN and not self.server:
if event.key == self.key['new']:
self.maze.__init__(self.fps)
elif event.key == self.key['pause'] and not self.hero.dead:
self.paused ^= True
elif event.key == self.key['mute']:
if pygame.mixer.get_init() is None:
pygame.mixer.init(frequency=44100)
pygame.mixer.music.load(MUSIC)
pygame.mixer.music.set_volume(self.musicvol)
pygame.mixer.music.play(-1)
else:
pygame.mixer.quit()
if not self.headless: self.maze.draw()
return True
def move(self, x, y): def move(self, x, y):
"""Command the hero to move faster in the given direction.""" """Command the hero to move faster in the given direction."""
x, y = -x, -y # or move the maze in the reverse direction
stunned = pygame.time.get_ticks() < self.maze.next_move stunned = pygame.time.get_ticks() < self.maze.next_move
velocity = self.maze.distance * HERO_SPEED / self.fps velocity = self.maze.distance * HERO_SPEED / self.fps
accel = velocity * HERO_SPEED / self.fps accel = velocity * HERO_SPEED / self.fps
@ -139,42 +228,51 @@ class Game:
self.maze.vy += y * accel self.maze.vy += y * accel
if abs(self.maze.vy) > velocity: self.maze.vy = y * velocity if abs(self.maze.vy) > velocity: self.maze.vy = y * velocity
def loop(self): def control(self, x, y, angle, firing, slashing):
"""Start and handle main loop.""" """Control how the hero move and attack."""
events = pygame.fastevent.get() self.move(x, y)
for event in events: self.hero.update_angle(angle)
if event.type == QUIT: self.hero.firing = firing
return False self.hero.slashing = slashing
elif event.type == VIDEORESIZE:
self.maze.resize((event.w, event.h))
elif event.type == KEYDOWN:
if event.key == self.key['new']:
self.maze.__init__(self.fps)
elif event.key == self.key['pause'] and not self.hero.dead:
self.paused ^= True
elif event.key == self.key['mute']:
if pygame.mixer.get_init() is None:
pygame.mixer.init(frequency=44100)
pygame.mixer.music.load(MUSIC)
pygame.mixer.music.set_volume(self.musicvol)
pygame.mixer.music.play(-1)
else:
pygame.mixer.quit()
def remote_control(self, connection):
"""Handle remote control though socket server.
Return False if client disconnect, True otherwise.
"""
data = self.export()
connection.send('{:06}'.format(len(data)))
connection.send(data)
buf = connection.recv(8)
if not buf: return False
x, y, angle, attack = (int(i) for i in buf.decode().split())
self.control(x, y, angle, attack & 1, attack >> 1)
def user_control(self):
"""Handle direct control from user's mouse and keyboard."""
if not self.hero.dead: if not self.hero.dead:
keys = pygame.key.get_pressed() keys = pygame.key.get_pressed()
self.move(keys[self.key['left']] - keys[self.key['right']], right = keys[self.key['right']] - keys[self.key['left']]
keys[self.key['up']] - keys[self.key['down']]) down = keys[self.key['down']] - keys[self.key['up']]
# Follow the mouse cursor
x, y = pygame.mouse.get_pos()
angle = atan2(y - self.hero.y, x - self.hero.x)
buttons = pygame.mouse.get_pressed() buttons = pygame.mouse.get_pressed()
try: try:
self.hero.firing = keys[self.key['shot']] firing = keys[self.key['shot']]
except KeyError: except KeyError:
self.hero.firing = buttons[self.mouse['shot']] firing = buttons[self.mouse['shot']]
try: try:
self.hero.slashing = keys[self.key['slash']] slashing = keys[self.key['slash']]
except KeyError: except KeyError:
self.hero.slashing = buttons[self.mouse['slash']] slashing = buttons[self.mouse['slash']]
self.control(right, down, angle, firing, slashing)
def update(self):
"""Update fps and the maze."""
# Compare current FPS with the average of the last 10 frames # Compare current FPS with the average of the last 10 frames
new_fps = self.clock.get_fps() new_fps = self.clock.get_fps()
if new_fps < self.fps: if new_fps < self.fps:
@ -183,7 +281,6 @@ class Game:
self.fps += 5 self.fps += 5
if not self.paused: self.maze.update(self.fps) if not self.paused: self.maze.update(self.fps)
self.clock.tick(self.fps) self.clock.tick(self.fps)
return True
def __exit__(self, exc_type, exc_value, traceback): pygame.quit() def __exit__(self, exc_type, exc_value, traceback): pygame.quit()
@ -196,7 +293,7 @@ def main():
parents.append(dirs.user_config_dir) parents.append(dirs.user_config_dir)
filenames = [join(parent, 'settings.ini') for parent in parents] filenames = [join(parent, 'settings.ini') for parent in parents]
config = ConfigReader(filenames) config = ConfigReader(filenames)
config.parse_output() config.parse()
# Parse command-line arguments # Parse command-line arguments
parser = ArgumentParser(formatter_class=RawTextHelpFormatter) parser = ArgumentParser(formatter_class=RawTextHelpFormatter)
@ -238,11 +335,15 @@ def main():
# Manipulate config # Manipulate config
if args.config: config.config.read(args.config) if args.config: config.config.read(args.config)
config.read_args(args) config.read_args(args)
config.parse_output() config.parse()
config.parse_control()
# Main loop # Main loop
scrtype = (config.opengl and DOUBLEBUF|OPENGL) | RESIZABLE with Game(config) as game:
with Game(config.size, scrtype, config.max_fps, config.muted, if config.server:
config.musicvol, config.key, config.mouse) as game: while game.meta():
while game.loop(): pass game.remote_control()
game.update()
else:
while game.meta():
game.user_control()
game.update()

View file

@ -63,7 +63,7 @@ class Maze:
distance (float): distance between centers of grids (in px) distance (float): distance between centers of grids (in px)
x, y (int): coordinates of the center of the hero (in px) x, y (int): coordinates of the center of the hero (in px)
centerx, centery (float): center grid's center's coordinates (in px) centerx, centery (float): center grid's center's coordinates (in px)
rangex, rangey (range): range of the index of the grids on display rangex, rangey (list): range of the index of the grids on display
score (float): current score score (float): current score
map (deque of deque): map of grids representing objects on the maze map (deque of deque): map of grids representing objects on the maze
vx, vy (float): velocity of the maze movement (in pixels per frame) vx, vy (float): velocity of the maze movement (in pixels per frame)
@ -78,21 +78,22 @@ class Maze:
sfx_slash (pygame.mixer.Sound): sound effect of slashed enemy sfx_slash (pygame.mixer.Sound): sound effect of slashed enemy
sfx_lose (pygame.mixer.Sound): sound effect to be played when you lose sfx_lose (pygame.mixer.Sound): sound effect to be played when you lose
""" """
def __init__(self, fps, size=None, scrtype=None): def __init__(self, fps, size=None, scrtype=None, headless=False):
self.fps = fps self.fps = fps
if size is not None: if not headless:
self.w, self.h = size if size is not None:
else: self.w, self.h = size
size = self.w, self.h else:
if scrtype is not None: self.scrtype = scrtype size = self.w, self.h
if scrtype is not None: self.scrtype = scrtype
self.surface = pygame.display.set_mode(size, self.scrtype)
self.surface = pygame.display.set_mode(size, self.scrtype)
self.distance = (self.w * self.h / 416) ** 0.5 self.distance = (self.w * self.h / 416) ** 0.5
self.x, self.y = self.w // 2, self.h // 2 self.x, self.y = self.w // 2, self.h // 2
self.centerx, self.centery = self.w / 2.0, self.h / 2.0 self.centerx, self.centery = self.w / 2.0, self.h / 2.0
w, h = (int(i/self.distance/2 + 2) for i in size) w, h = (int(i/self.distance/2 + 2) for i in size)
self.rangex = range(MIDDLE - w, MIDDLE + w + 1) self.rangex = list(range(MIDDLE - w, MIDDLE + w + 1))
self.rangey = range(MIDDLE - h, MIDDLE + h + 1) self.rangey = list(range(MIDDLE - h, MIDDLE + h + 1))
self.score = INIT_SCORE self.score = INIT_SCORE
self.map = deque() self.map = deque()
@ -146,7 +147,7 @@ class Maze:
fill_aapolygon(self.surface, square, FG_COLOR) fill_aapolygon(self.surface, square, FG_COLOR)
for enemy in self.enemies: enemy.draw() for enemy in self.enemies: enemy.draw()
self.hero.draw() if not self.hero.dead: self.hero.draw()
bullet_radius = self.distance / 4 bullet_radius = self.distance / 4
for bullet in self.bullets: bullet.draw(bullet_radius) for bullet in self.bullets: bullet.draw(bullet_radius)
pygame.display.flip() pygame.display.flip()
@ -320,7 +321,6 @@ class Maze:
self.hero.update(fps) self.hero.update(fps)
self.slash() self.slash()
self.track_bullets() self.track_bullets()
self.draw()
def resize(self, size): def resize(self, size):
"""Resize the maze.""" """Resize the maze."""
@ -349,3 +349,4 @@ class Maze:
self.hero.slashing = self.hero.firing = False self.hero.slashing = self.hero.firing = False
self.vx = self.vy = 0.0 self.vx = self.vy = 0.0
play(self.sfx_lose) play(self.sfx_lose)
print('Game over. Your score: {}'.format(int(self.score - INIT_SCORE)))

View file

@ -1,3 +1,11 @@
[Server]
# Enabling remote control will disable control via keyboard and mouse.
Enable: no
Host: localhost
Port: 8089
# Disable graphics and sounds (only if socket server is enabled).
Headless: no
[Graphics] [Graphics]
Screen width: 640 Screen width: 640
Screen height: 480 Screen height: 480

View file

@ -55,14 +55,18 @@ class Bullet:
self.x += s * cos(self.angle) self.x += s * cos(self.angle)
self.y += s * sin(self.angle) self.y += s * sin(self.angle)
def get_color(self):
"""Return current color of the enemy."""
value = int((1-(self.fall_time-get_ticks())/BULLET_LIFETIME)*ENEMY_HP)
try:
return TANGO[self.color][value]
except IndexError:
return BG_COLOR
def draw(self, radius): def draw(self, radius):
"""Draw the bullet.""" """Draw the bullet."""
pentagon = regpoly(5, radius, self.angle, self.x, self.y) pentagon = regpoly(5, radius, self.angle, self.x, self.y)
value = int((1-(self.fall_time-get_ticks())/BULLET_LIFETIME)*ENEMY_HP) fill_aapolygon(self.surface, pentagon, self.get_color())
try:
fill_aapolygon(self.surface, pentagon, TANGO[self.color][value])
except IndexError:
pass
def place(self, x, y): def place(self, x, y):
"""Move the bullet by (x, y) (in pixels).""" """Move the bullet by (x, y) (in pixels)."""