aboutsummaryrefslogtreecommitdiff
path: root/schuessel.py
blob: 5f80fd55b0d64909a10a131d1d67ac7142d5c24d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
"""Implementation of a cyberschuessel"""
import json
import logging
import random

import aiohttp
import aiohttp_jinja2

log = logging.getLogger(__name__)


class Schuessel:
    """Class representing a Salatschüssel and providing an aiohttp handler"""

    def __init__(self):
        self.phase = 0
        self.words = []
        self.guessed = []
        self.draw_counter = 0

        self.session_counter = 0
        self.clients = {}

    def reset(self):
        """Reset the state of the schuessel"""
        self.phase = 0
        self.words = []
        self.guessed = []

    def schuessel_state(self):
        """Return the client facing state of the schuessel"""
        return {
            'words': len(self.words),
            'guessed': len(self.guessed),
            'draw_counter': self.draw_counter,
            'phase': self.phase
        }

    def add(self, word):
        """Add a word to the schuessel"""
        self.words.append(word)

    def draw(self, last_word=None):
        """Draw a word from the schuessel

        If a last_word is provided which was the last drawn word this
        word will be counted as guessed and moved to the guessed word list.

        If the word list is empty transfer the guessed list into the words list
        and start the next phase"""
        phase_change = False

        # first draw -> start game
        if self.phase == 0:
            self.phase = 1

        # we ignore invalid or outdated last_words
        elif last_word and last_word['word'] in self.words and last_word[
                'draw_counter'] == self.draw_counter:
            self.guessed.append(last_word['word'])
            del self.words[self.words.index(last_word['word'])]

        if len(self.words) == 0:
            # empty schuessel
            if len(self.guessed) == 0:
                return None, False

            # move guessed words back into the schuessel
            tmp = self.guessed
            self.guessed = self.words
            self.words = tmp
            self.phase = self.phase + 1
            phase_change = True

        self.draw_counter += 1

        word = random.choice(self.words)
        return word, phase_change

    async def broadcast(self, msg):
        """broadcast any json encodable object to all open websockets"""
        log.info('broadcasting %s to %d', msg, len(self.clients.values()))
        broken_sessions = []
        for session_id, ws_conn in self.clients.items():
            try:
                await ws_conn.send_json(msg)
            except ConnectionResetError:
                broken_sessions.append(session_id)

        for session_id in broken_sessions:
            log.info('remove broken session_id %d', session_id)
            del self.clients[session_id]

    async def close_all(self):
        """close all open websockets"""
        for ws_conn in list(self.clients.values()):
            await ws_conn.close()

    async def handler(self, request):
        """aiohttp handler

        This function delivers the webpage or upgrades the connection to a websocket.
        Then it listens for events on the socket and executes the
        commands received from the socket.

        The server logic is implemented in this fnction."""
        ws_conn = aiohttp.web.WebSocketResponse()
        ws_ready = ws_conn.can_prepare(request)
        if not ws_ready.ok:
            return aiohttp_jinja2.render_template('index.html', request, {})

        await ws_conn.prepare(request)

        session_id = self.session_counter
        self.session_counter += 1

        self.clients[session_id] = ws_conn

        init_msg = {'action': 'init'}
        init_msg.update(self.schuessel_state())
        log.info('send init action with %s to %d', init_msg, session_id)
        try:
            await ws_conn.send_json(init_msg)
        except ConnectionResetError:
            del self.clients[session_id]
            return ws_conn

        async for msg in ws_conn:
            log.info('received msg: %s from %d', msg, session_id)
            if msg.type == aiohttp.WSMsgType.TEXT:
                data = json.loads(msg.data)

                if data['action'] == 'draw':
                    last_word = data['last_word']
                    new_word, phase_change = self.draw(last_word)

                    drawn_msg = {'action': 'drawn'}
                    drawn_msg.update(self.schuessel_state())
                    await self.broadcast(drawn_msg)

                    # Make sure to always send the 'draw' msg after the 'drawn'
                    # msg the javascript part expects this behavior
                    log.info('send draw action with %s to %d', new_word,
                             session_id)
                    try:
                        await ws_conn.send_json({
                            'action':
                            'draw',
                            'drawn_word':
                            new_word,
                            'phase_change':
                            phase_change,
                            'draw_counter':
                            self.draw_counter,
                        })
                    except ConnectionResetError:
                        del self.clients[session_id]
                        return ws_conn

                elif data['action'] == 'add':
                    new_word = data['word']
                    self.add(new_word)
                    log.info('words in schuessel %s', self.words)
                    await self.broadcast({'action': 'add'})

                elif data['action'] == 'reset':
                    self.reset()
                    reset_msg = {'action': 'init'}
                    reset_msg.update(self.schuessel_state())
                    await self.broadcast(reset_msg)

                else:
                    log.error('unknown action received: %s from %d', data,
                              session_id)
            elif msg.type == aiohttp.WSMsgType.ERROR:
                del self.clients[session_id]
                print(
                    f'{session_id} connection closed with exception {ws_conn.exception()}'
                )

        del self.clients[session_id]
        log.info('websocket connection %d closed', session_id)

        return ws_conn