Finnish first socket server protype
This commit is contained in:
parent
bc47fb3f30
commit
3e85f0c3a1
6 changed files with 213 additions and 88 deletions
|
@ -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."""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)."""
|
||||
|
|
Loading…
Reference in a new issue