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:
self.angle += sign(self.spin_queue) * pi / 2 / self.spin_speed
self.spin_queue -= sign(self.spin_queue)
else:
# Follow the mouse cursor
x, y = pygame.mouse.get_pos()
self.angle = atan2(y - self.y, x - self.x)
def update_angle(self, angle):
"""Turn to the given angle if the hero is not busy slashing."""
if abs(self.spin_queue) <= 0.5:
self.spin_queue = 0.0
self.angle = angle
def draw(self):
"""Draw the hero."""
@ -238,13 +239,18 @@ class Enemy:
x, y = self.get_pos()
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):
"""Draw the enemy."""
if get_ticks() < self.maze.next_move and not self.awake: return
radious = self.maze.distance/SQRT2 - self.awake*2
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, color)
fill_aapolygon(self.maze.surface, square, self.get_color())
def update(self):
"""Update the enemy."""

View File

@ -19,6 +19,8 @@
__doc__ = 'brutalmaze module for shared constants'
from string import ascii_lowercase
from pkg_resources import resource_filename as pkg_file
import pygame
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))}
ENEMIES = ['Butter', 'Orange', 'Chocolate', 'Chameleon',
'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
ENEMY_HP = 3
HERO_HP = 5

View File

@ -26,7 +26,10 @@ try: # Python 3
from configparser import ConfigParser
except ImportError: # Python 2
from ConfigParser import ConfigParser
from itertools import repeat
from math import atan2, degrees
from os.path import join, pathsep
from socket import socket
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 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 .misc import sign
from .misc import round2, sign
class ConfigReader:
@ -58,8 +61,33 @@ class ConfigReader:
self.config.read(SETTINGS) # default configuration
self.config.read(filenames)
def parse_output(self):
"""Parse graphics and sound configurations."""
# Fallback to None when attribute is missing
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.config.getint('Graphics', 'Screen height'))
self.opengl = self.config.getboolean('Graphics', 'OpenGL')
@ -67,24 +95,6 @@ class ConfigReader:
self.muted = self.config.getboolean('Sound', 'Muted')
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):
"""Read and parse a ArgumentParser.Namespace."""
for option in 'size', 'opengl', 'max_fps', 'muted', 'musicvol':
@ -94,29 +104,108 @@ class ConfigReader:
class Game:
"""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.init()
if muted:
if config.muted or config.headless:
pygame.mixer.quit()
else:
pygame.mixer.music.load(MUSIC)
pygame.mixer.music.set_volume(musicvol)
pygame.mixer.music.set_volume(config.musicvol)
pygame.mixer.music.play(-1)
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.max_fps, self.fps = max_fps, float(max_fps)
self.musicvol = musicvol
self.key, self.mouse = key, mouse
self.maze = Maze(max_fps, size, scrtype)
self.max_fps, self.fps = config.max_fps, float(config.max_fps)
self.musicvol = config.musicvol
self.key, self.mouse = config.key, config.mouse
scrtype = (config.opengl and DOUBLEBUF|OPENGL) | RESIZABLE
self.maze = Maze(config.max_fps, config.size, scrtype, config.headless)
self.hero = self.maze.hero
self.clock, self.paused = Clock(), False
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):
"""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
velocity = self.maze.distance * HERO_SPEED / self.fps
accel = velocity * HERO_SPEED / self.fps
@ -139,42 +228,51 @@ class Game:
self.maze.vy += y * accel
if abs(self.maze.vy) > velocity: self.maze.vy = y * velocity
def loop(self):
"""Start and handle main loop."""
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:
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 control(self, x, y, angle, firing, slashing):
"""Control how the hero move and attack."""
self.move(x, y)
self.hero.update_angle(angle)
self.hero.firing = firing
self.hero.slashing = slashing
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:
keys = pygame.key.get_pressed()
self.move(keys[self.key['left']] - keys[self.key['right']],
keys[self.key['up']] - keys[self.key['down']])
right = keys[self.key['right']] - keys[self.key['left']]
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()
try:
self.hero.firing = keys[self.key['shot']]
firing = keys[self.key['shot']]
except KeyError:
self.hero.firing = buttons[self.mouse['shot']]
firing = buttons[self.mouse['shot']]
try:
self.hero.slashing = keys[self.key['slash']]
slashing = keys[self.key['slash']]
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
new_fps = self.clock.get_fps()
if new_fps < self.fps:
@ -183,7 +281,6 @@ class Game:
self.fps += 5
if not self.paused: self.maze.update(self.fps)
self.clock.tick(self.fps)
return True
def __exit__(self, exc_type, exc_value, traceback): pygame.quit()
@ -196,7 +293,7 @@ def main():
parents.append(dirs.user_config_dir)
filenames = [join(parent, 'settings.ini') for parent in parents]
config = ConfigReader(filenames)
config.parse_output()
config.parse()
# Parse command-line arguments
parser = ArgumentParser(formatter_class=RawTextHelpFormatter)
@ -238,11 +335,15 @@ def main():
# Manipulate config
if args.config: config.config.read(args.config)
config.read_args(args)
config.parse_output()
config.parse_control()
config.parse()
# Main loop
scrtype = (config.opengl and DOUBLEBUF|OPENGL) | RESIZABLE
with Game(config.size, scrtype, config.max_fps, config.muted,
config.musicvol, config.key, config.mouse) as game:
while game.loop(): pass
with Game(config) as game:
if config.server:
while game.meta():
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)
x, y (int): coordinates of the center of the hero (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
map (deque of deque): map of grids representing objects on the maze
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_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
if size is not None:
self.w, self.h = size
else:
size = self.w, self.h
if scrtype is not None: self.scrtype = scrtype
if not headless:
if size is not None:
self.w, self.h = size
else:
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.x, self.y = self.w // 2, self.h // 2
self.centerx, self.centery = self.w / 2.0, self.h / 2.0
w, h = (int(i/self.distance/2 + 2) for i in size)
self.rangex = range(MIDDLE - w, MIDDLE + w + 1)
self.rangey = range(MIDDLE - h, MIDDLE + h + 1)
self.rangex = list(range(MIDDLE - w, MIDDLE + w + 1))
self.rangey = list(range(MIDDLE - h, MIDDLE + h + 1))
self.score = INIT_SCORE
self.map = deque()
@ -146,7 +147,7 @@ class Maze:
fill_aapolygon(self.surface, square, FG_COLOR)
for enemy in self.enemies: enemy.draw()
self.hero.draw()
if not self.hero.dead: self.hero.draw()
bullet_radius = self.distance / 4
for bullet in self.bullets: bullet.draw(bullet_radius)
pygame.display.flip()
@ -320,7 +321,6 @@ class Maze:
self.hero.update(fps)
self.slash()
self.track_bullets()
self.draw()
def resize(self, size):
"""Resize the maze."""
@ -349,3 +349,4 @@ class Maze:
self.hero.slashing = self.hero.firing = False
self.vx = self.vy = 0.0
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]
Screen width: 640
Screen height: 480

View File

@ -55,14 +55,18 @@ class Bullet:
self.x += s * cos(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):
"""Draw the bullet."""
pentagon = regpoly(5, radius, self.angle, self.x, self.y)
value = int((1-(self.fall_time-get_ticks())/BULLET_LIFETIME)*ENEMY_HP)
try:
fill_aapolygon(self.surface, pentagon, TANGO[self.color][value])
except IndexError:
pass
fill_aapolygon(self.surface, pentagon, self.get_color())
def place(self, x, y):
"""Move the bullet by (x, y) (in pixels)."""