Skip to content

Commit 072206f

Browse files
author
Ricardo Band
committed
Merge branch 'release/v0.2.0'
2 parents 0a4c9aa + 9ebf50f commit 072206f

15 files changed

+638
-412
lines changed

README.md

+102-17
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,114 @@
1-
# pymlgame
1+
pymlgame
2+
========
23

3-
pymlgame is an abstraction layer to easily build games for Mate Light inspired
4-
by pygame.
4+
pymlgame is an abstraction layer to easily build games for Mate Light inspired by pygame.
55

66
You need 3 parts to actually have a running game on Mate Light.
77

8-
## The game
8+
The game
9+
--------
910

10-
You can build a game using the pymlgame library. If you know the pygame
11-
library this should meen nothing new to you. Use the game_example.py to find
12-
out how to to so.
11+
You can build a game using the pymlgame library. If you know the pygame library this should be nothing new to you.
12+
Use the game_example.py to find out how to do so.
1313

14-
## A controller
14+
A controller
15+
------------
1516

16-
If you want players, your game needs a controller to have some inputs. You
17-
can use anything as a controller that can trigger the JSONRPC calls in your
18-
game. As an example you can use the controller_example.py and adapt it to
19-
your controller.
17+
If you want players, your game needs a controller to have some inputs. Controllers should work with every game made with
18+
pymlgame. There is a controller_example.py to play with. An Android based controller app is in the works.
2019

21-
## Mate Light
20+
Mate Light
21+
----------
22+
23+
To play pymlgames you need the awesome Mate Light. You could use the one on c-base space station, or build your own.
24+
If you're not near c-base and don't want to build your own, you can use the Mate Light emulator provided with pymlgame.
25+
26+
27+
Protocol
28+
========
29+
30+
This is the coitus protocol that handles the communication between the game and the controllers.
31+
32+
33+
Controller -> Game
34+
------------------
35+
36+
### /controller/new/<port>
37+
38+
Connect a new controller to the game. Game will answer with /uid/<uid>. This is also known as 'anpymln'.
39+
40+
### /controller/<uid>/ping/<port>
41+
42+
Tell the game that the controller is still in use and update it's address and port. Use this once a minute or the
43+
controller will get deleted.
44+
45+
### /controller/<uid>/kthxbye
46+
47+
Disconnect the controller properly. In theory you could just wait 60s but this is the cleaner way and most games would
48+
be very happy if you use this.
49+
50+
###/controller/<uid>/states/<states>
51+
52+
Send the states of your controller keys. Always send all 14 states, even if your controller doesn't have 14 buttons.
53+
The states should be an array with 0 for key not pressed and 1 for key pressed. So if you're pressing the Up button and
54+
X the states array should look like this: 10000010000000
55+
You can lookup all possible buttons and there location in the array in pymlgame/locals.py.
56+
57+
### /controller/<uid>/text/<text>
58+
59+
*Optional*
60+
61+
Send some text to the game. Maybe it asks you for a player name. Games should always give you the option to enter text
62+
without this function but you can use it if your controller is capable of text input.
63+
64+
65+
Game -> Controller
66+
------------------
67+
68+
### /uid/<uid>
69+
70+
Tell the controller its uid. This is ideally an integer.
71+
72+
### /rumble/<duration>
73+
74+
*Optional*
75+
76+
Tell the controller to rumble. Duration should be given in milliseconds. Not all controllers have the ability to rumble.
77+
Maybe they do it in an optical way.
78+
79+
### /message/<text>
80+
81+
*Optional*
82+
83+
Send some text to the controller. Be aware that not all controllers can display text, so don't send important things.
84+
85+
### /download/<url>
86+
87+
*Optional*
88+
89+
Tell the controller to download the media file under the given url. The game can then ask the controller to play the
90+
file so that the player can hear some ingame sounds. Use this function everytime the game starts because a controller
91+
could have deleted the files after playing. Controllers should ensure that files already downloaded gets downloaded
92+
again to reduce loading times for games that have been played before.
93+
94+
### /play/<file>
95+
96+
*Optional*
97+
98+
Tell the controller to play the file with that name. Use the download command to download media files to the controller
99+
before using this.
100+
101+
102+
Tipps
103+
-----
104+
105+
If your controller downloads media files for games it is good practice to delete the downloaded stuff from time to time
106+
or give the user the ability to cleanup the downloaded files.
107+
108+
If your game uses some optional features, tell the players that they can have an even better gaming experience when they
109+
use a controller with is capable of these features.
22110

23-
Last but not least you need Mate Light as your display. If you are not at
24-
c-base space station but still want to tinker around with this you can use
25-
the emulator.py to do so.
26111

27112
---
28113

29-
Have fun while playing pymlgames on Mate Light! :D
114+
Have fun while playing pymlgames on Mate Light! :D

controller_example.py

+163-75
Original file line numberDiff line numberDiff line change
@@ -5,95 +5,183 @@
55
pymlgame - controller example
66
=============================
77
8-
This example shows how you can use a controller connected to a laptop or any
9-
other machine capable of pygame to connect to a pymlgame instance.
8+
This example shows how you can use your notebooks keyboard to connect to a pymlgame instance.
109
"""
1110

12-
__author__ = 'Ricardo Band'
13-
__copyright__ = 'Copyright 2014, Ricardo Band'
14-
__credits__ = ['Ricardo Band']
15-
__license__ = 'MIT'
16-
__version__ = '0.1.1'
17-
__maintainer__ = 'Ricardo Band'
18-
__email__ = '[email protected]'
19-
__status__ = 'Development'
20-
2111
import sys
12+
import time
13+
import socket
14+
import logging
15+
logging.basicConfig(format='%(asctime)s | %(levelname)s | %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
16+
from datetime import datetime
17+
from threading import Thread
2218

2319
import pygame
24-
import jsonrpclib
2520

21+
# define some constants
22+
E_UID = pygame.USEREVENT + 1
23+
E_DOWNLOAD = pygame.USEREVENT + 2
24+
E_PLAY = pygame.USEREVENT + 3
25+
E_RUMBLE = pygame.USEREVENT + 4
2626

27-
class Controller(object):
28-
def __init__(self, host, port):
27+
28+
class ReceiverThread(Thread):
29+
"""
30+
This thread will listen on a UDP port for packets from the game.
31+
"""
32+
def __init__(self, host='0.0.0.0', port=11337):
33+
"""
34+
Creates the socket and binds it to the given host and port.
35+
"""
36+
super(ReceiverThread, self).__init__()
2937
self.host = host
3038
self.port = port
39+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
40+
self.sock.bind((self.host, self.port))
41+
42+
def run(self):
43+
while True:
44+
data, addr = self.sock.recvfrom(1024)
45+
data = data.decode('utf-8')
46+
47+
logging.info('example info')
48+
logging.warning('example warning')
49+
logging.error('example error')
50+
logging.critical('example critical')
51+
#print(datetime.now(), '<<< {}'.format(data))
52+
if data.startswith('/uid/'):
53+
#print(datetime.now(), '### uid', data[5:], 'received')
54+
e = pygame.event.Event(E_UID, {'uid': int(data[5:])})
55+
pygame.event.post(e)
56+
elif data.startswith('/download/'):
57+
e = pygame.event.Event(E_DOWNLOAD, {'url': str(data[10:])})
58+
pygame.event.post(e)
59+
elif data.startswith('/play/'):
60+
e = pygame.event.Event(E_PLAY, {'filename': str(data[6:])})
61+
pygame.event.post(e)
62+
elif data.startswith('/rumble/'):
63+
e = pygame.event.Event(E_RUMBLE, {'duration': float(data[8:].replace(',', '.'))})
64+
pygame.event.post(e)
65+
66+
67+
class Controller(object):
68+
def __init__(self, game_host='127.0.0.1', game_port=1338, host='0.0.0.0', port=11337):
69+
self.game_host = game_host # Host of Mate Light
70+
self.game_port = game_port # Port of Mate Light
71+
self.host = host # Host of ReceiverThread
72+
self.port = port # Port of ReceiverThread
73+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
74+
3175
pygame.init()
32-
self.joysticks = [[pygame.joystick.Joystick(j), None, None]
33-
for j in range(pygame.joystick.get_count())]
34-
self.screen = pygame.display.set_mode((100, 10))
35-
pygame.display.set_caption("pymlgame_ctlr")
36-
self.clock = pygame.time.Clock()
37-
self.server = jsonrpclib.Server('http://' + self.host + ':' +
38-
str(self.port))
39-
for joy in self.joysticks:
40-
joy[0].init()
41-
joy[1] = self.server.init()
42-
if joy[0].get_name() == 'Xbox 360 Wireless Receiver':
43-
joy[2] = {0: 'A',
44-
1: 'B',
45-
2: 'X',
46-
3: 'Y',
47-
4: 'L1',
48-
5: 'R1',
49-
6: 'Select',
50-
7: 'Start',
51-
11: 'Left',
52-
12: 'Right',
53-
13: 'Up',
54-
14: 'Down'}
55-
56-
def handle_events(self):
76+
self.screen = pygame.display.set_mode((128, 113), pygame.DOUBLEBUF, 32)
77+
bg = pygame.image.load('kbd.png')
78+
self.screen.blit(bg, pygame.rect.Rect(0, 0, 128, 113))
79+
pygame.display.flip()
80+
81+
self.keys = [0 for _ in range(14)]
82+
self.mapping = {pygame.K_UP: 0, # Up
83+
pygame.K_DOWN: 1, # Down
84+
pygame.K_LEFT: 2, # Left
85+
pygame.K_RIGHT: 3, # Right
86+
pygame.K_a: 4, # A
87+
pygame.K_w: 5, # B
88+
pygame.K_s: 6, # X
89+
pygame.K_d: 7, # Y
90+
pygame.K_RETURN: 8, # Start
91+
pygame.K_SPACE: 9, # Select
92+
pygame.K_q: 10, # L1
93+
pygame.K_1: 11, # L2
94+
pygame.K_e: 12, # R1
95+
pygame.K_3: 13} # R2
96+
97+
self.timeout = 0 # if the controller is in idle state a ping signal will be sent
98+
self.rumble_active = False
99+
self.uid = None
100+
101+
self._receiver = ReceiverThread(host, port)
102+
self._receiver.setDaemon(True)
103+
self._receiver.start()
104+
105+
def ping(self):
106+
if self.uid:
107+
msg = '/controller/{}/ping/{}'.format(self.uid, self._receiver.port)
108+
self.sock.sendto(msg.encode('utf-8'), (self.game_host, self.game_port))
109+
#print(datetime.now(), '>>>', msg)
110+
111+
def send_keys(self):
112+
# alternative states creation: [1 if k else 0 for k in self.keys]
113+
states = '/controller/{}/states/{}'.format(self.uid, ''.join([str(k) for k in self.keys]))
114+
self.sock.sendto(states.encode('utf-8'), (self.game_host, self.game_port))
115+
#print(datetime.now(), '>>>' + states)
116+
self.timeout = time.time()
117+
118+
def send_message(self, msg):
119+
pass
120+
121+
def disconnect(self):
122+
msg = '/controller/{}/kthxbye'.format(self.uid)
123+
self.sock.sendto(msg.encode('utf-8'), (self.game_host, self.game_port))
124+
125+
def connect(self):
126+
msg = '/controller/new/{}'.format(self.port)
127+
self.sock.sendto(msg.encode('utf-8'), (self.game_host, self.game_port))
128+
129+
def rumble(self, duration):
130+
pass
131+
132+
def download_sound(self, url):
133+
pass
134+
135+
def play_sound(self, filename):
136+
pass
137+
138+
def handle_inputs(self):
139+
if time.time() > self.timeout + 30:
140+
self.ping()
141+
self.timeout = time.time()
142+
57143
for event in pygame.event.get():
58144
if event.type == pygame.QUIT:
59-
pygame.joystick.quit()
60145
pygame.quit()
61-
sys.exit()
62-
if event.type == pygame.KEYDOWN:
63-
if event.key == pygame.K_ESCAPE:
64-
pygame.event.post(pygame.event.Event(pygame.QUIT))
65-
if event.type == pygame.JOYBUTTONDOWN:
66-
print('joy', self.joysticks[event.joy][0].get_name(),
67-
'(uid:', self.joysticks[event.joy][1], ')button pressed:',
68-
event.button)
69-
self.server.trigger_button(self.joysticks[event.joy][1],
70-
'KeyDown',
71-
self.joysticks[event.joy][2][event.button])
72-
if event.type == pygame.JOYBUTTONUP:
73-
print('joy', self.joysticks[event.joy][0].get_name(),
74-
'(uid:', self.joysticks[event.joy][1], ')button released:',
75-
event.button)
76-
self.server.trigger_button(self.joysticks[event.joy][1],
77-
'KeyUp',
78-
self.joysticks[event.joy][2][event.button])
79-
80-
def update(self):
81-
pass
146+
sys.exit(0)
147+
elif event.type == pygame.MOUSEBUTTONUP:
148+
pygame.event.post(pygame.event.Event(pygame.QUIT))
149+
elif event.type == E_UID:
150+
#print(datetime.now(), '### UID event received', event.uid)
151+
self.uid = event.uid
82152

83-
def render(self):
84-
pygame.display.update()
85-
pygame.display.flip()
86-
87-
def gameloop(self):
88-
try:
89-
while True:
90-
self.handle_events()
91-
self.update()
92-
self.render()
93-
except KeyboardInterrupt:
94-
pass
153+
if self.uid is not None:
154+
#print(datetime.now(), '### UID set. checking other events')
155+
if event.type == E_DOWNLOAD:
156+
self.download_sound(event.url)
157+
elif event.type == E_PLAY:
158+
self.play_sound(event.filename)
159+
elif event.type == E_RUMBLE:
160+
self.rumble(event.duration)
161+
elif event.type == pygame.KEYDOWN or event.type == pygame.KEYUP:
162+
try:
163+
button = self.mapping[event.key]
164+
if event.type == pygame.KEYDOWN:
165+
#print('{} | {}'.format(event.key, button))
166+
self.keys[button] = 1
167+
elif event.type == pygame.KEYUP:
168+
#print('{} | {}'.format(event.key, button))
169+
self.keys[button] = 0
170+
self.send_keys()
171+
except KeyError:
172+
break
173+
else:
174+
#print(datetime.now(), '### UID not set. connecting to game.')
175+
self.connect()
176+
time.sleep(1)
95177

96178

97179
if __name__ == '__main__':
98-
CTLR = Controller('127.0.0.1', 1338)
99-
CTLR.gameloop()
180+
ctlr = Controller('127.0.0.1', 1338, '0.0.0.0', 11337)
181+
try:
182+
while True:
183+
ctlr.handle_inputs()
184+
except KeyboardInterrupt:
185+
if ctlr.uid is not None:
186+
ctlr.disconnect()
187+
pass

0 commit comments

Comments
 (0)