feat: add mastermind
This commit is contained in:
parent
2bc089d988
commit
4b5650555b
3 changed files with 296 additions and 0 deletions
|
@ -12,6 +12,7 @@
|
||||||
<option value="set">Set</option>
|
<option value="set">Set</option>
|
||||||
<option value="carcassonne">Carcassonne</option>
|
<option value="carcassonne">Carcassonne</option>
|
||||||
<option value="go">Go</option>
|
<option value="go">Go</option>
|
||||||
|
<option value="mastermind">Mastermind</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="text" name="game" placeholder="empty: new game"/><input type="submit" value="Go!"/></li>
|
<input type="text" name="game" placeholder="empty: new game"/><input type="submit" value="Go!"/></li>
|
||||||
<li>Invite other players</li>
|
<li>Invite other players</li>
|
||||||
|
|
135
templates/mastermind/mastermind.html.j2
Normal file
135
templates/mastermind/mastermind.html.j2
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
{% if not done and player in played %}
|
||||||
|
<meta http-equiv="refresh" content="3" />
|
||||||
|
{% endif %}
|
||||||
|
<style>
|
||||||
|
td {
|
||||||
|
background: #444444;
|
||||||
|
padding: 7pt;
|
||||||
|
}
|
||||||
|
.attempt {
|
||||||
|
font-size: 14pt;
|
||||||
|
}
|
||||||
|
.response {
|
||||||
|
font-size: 7pt;
|
||||||
|
}
|
||||||
|
.other {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
.clickselect {
|
||||||
|
text-shadow: 0 0 5pt #4030ff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
{% macro attempt(a) %}
|
||||||
|
<div class="attempt">
|
||||||
|
{% for i in range(4) %}
|
||||||
|
<span style="color: {{ a[i].value.html }};">⬤</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro response(r, other) %}
|
||||||
|
<div class="response {% if other %}other{% endif %}">
|
||||||
|
{% for i in range(4) %}
|
||||||
|
<span style="color: {{ r[i].value.html }};">⬤</span>
|
||||||
|
{#{% if i == 1 %}<br>{% endif %}#}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
<table border="0">
|
||||||
|
{% if done %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ attempt(solution) }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<th>Turn</th>
|
||||||
|
<th>Your guess</th>
|
||||||
|
<th>{{ players[player].name }}</th>
|
||||||
|
{% for p in players.values() %}
|
||||||
|
{% if p.uuid == player %}{% continue %}{% endif %}
|
||||||
|
<th>{{ p.name }}</th>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% for i in range(turn) %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ i+1 }}</td>
|
||||||
|
<td>{% if attempts | length >= i+1 %}{{ attempt(attempts[i]) }}{% endif %}</td>
|
||||||
|
<td>{% if responses[player] | length >= i+1 %}{{ response(responses[player][i], false) }}{% endif %}</td>
|
||||||
|
{% for p in players.values() %}
|
||||||
|
{% if p.uuid == player %}{% continue %}{% endif %}
|
||||||
|
<td>{% if responses[p.uuid] | length >= i+1 %}{{ response(responses[p.uuid][i], true) }}{% endif %}</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% if done %}
|
||||||
|
{% if winners | length == 0 %}
|
||||||
|
Nobody won
|
||||||
|
{% else %}
|
||||||
|
{{ winners | join(', ') }} won!
|
||||||
|
{% endif %}
|
||||||
|
Game Over! <a href="{{ baseurl }}/{{ player }}">Return to lobby</a>
|
||||||
|
{% elif player not in played %}
|
||||||
|
<form name="controls" method="POST">
|
||||||
|
{% for i in range(4) %}
|
||||||
|
<label ondragover="allowDrop(event)" ondrop="drop(event)" for="mastermind-controls-attempt{{ i+1 }}">⬤</label>
|
||||||
|
<select name="attempt{{ i+1 }}" id="mastermind-controls-attempt{{ i+1 }}">
|
||||||
|
{% for c in colors %}
|
||||||
|
<option name="{{ c.name }}">{{ c.value.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
{% endfor %}
|
||||||
|
<input type="submit" value="Submit" name="action" />
|
||||||
|
</form>
|
||||||
|
<div id="colorlist">
|
||||||
|
{% for c in colors %}
|
||||||
|
<span draggable="true" clickable="true" onclick="click(event, '{{ c.value.name }}', '{{ c.value.html }}')" ondragstart="drag(event, '{{ c.value.name }}', '{{ c.value.html }}')" style="color: {{ c.value.html }};">⬤</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
let labels = document.getElementsByTagName('label');
|
||||||
|
for (let select of document.getElementsByTagName('select')) {
|
||||||
|
select.style.display = 'none';
|
||||||
|
}
|
||||||
|
labels[0].classList.add('clickselect');
|
||||||
|
i = 0;
|
||||||
|
|
||||||
|
function allowDrop(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drag(e, name, color) {
|
||||||
|
e.dataTransfer.setData('name', name);
|
||||||
|
e.dataTransfer.setData('color', color);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drop(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
console.log(e.dataTransfer.getData('color'));
|
||||||
|
console.log(e.dataTransfer.getData('name'));
|
||||||
|
e.target.style.color = e.dataTransfer.getData('color');
|
||||||
|
document.getElementById(e.target.getAttribute('for')).value = e.dataTransfer.getData('name');
|
||||||
|
console.log(e.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
function click(e, name, color) {
|
||||||
|
labels[i].style.color = color;
|
||||||
|
document.getElementById(labels[i].getAttribute('for')).value = name;
|
||||||
|
labels[i].classList.remove('clickselect');
|
||||||
|
i = (i+1) % 4;
|
||||||
|
labels[i].classList.add('clickselect');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% else %}
|
||||||
|
Please wait for the other players to finish their turn!
|
||||||
|
{% endif %}
|
||||||
|
</body>
|
||||||
|
</html>
|
160
webgames/mastermind.py
Normal file
160
webgames/mastermind.py
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
import random
|
||||||
|
import enum
|
||||||
|
from uuid import uuid4, UUID
|
||||||
|
|
||||||
|
from webgames.puzzle import Puzzle
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class _Color:
|
||||||
|
|
||||||
|
def __init__(self, name, html, ansi):
|
||||||
|
self.name = name
|
||||||
|
self.html = html
|
||||||
|
self.ansi = ansi
|
||||||
|
|
||||||
|
|
||||||
|
class Color(enum.Enum):
|
||||||
|
NONE = _Color('none', '#000000', '\033[30m')
|
||||||
|
RED = _Color('red', '#ff0000', '\033[31m')
|
||||||
|
GREEN = _Color('green', '#00aa00', '\033[32m')
|
||||||
|
BLUE = _Color('blue', '#0000ff', '\033[34m')
|
||||||
|
YELLOW = _Color('yellow', '#ffff00', '\093[33m')
|
||||||
|
WHITE = _Color('white', '#dddddd', '\033[97m')
|
||||||
|
GREY = _Color('grey', '#888888', '\033[90m')
|
||||||
|
PINK = _Color('pink', '#ff0088', '\033[95m')
|
||||||
|
ORANGE = _Color('orange', '#ff8800', '\033[95m')
|
||||||
|
|
||||||
|
|
||||||
|
class ResponseColor(enum.Enum):
|
||||||
|
RED = _Color('red', '#ff0000', '\033[31m')
|
||||||
|
WHITE = _Color('white', '#dddddd', '\033[97m')
|
||||||
|
BLACK = _Color('black', '#000000', '\033[30m')
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
if self == ResponseColor.RED and other != ResponseColor.RED:
|
||||||
|
return True
|
||||||
|
if self == ResponseColor.WHITE and other == ResponseColor.BLACK:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class Mastermind(Puzzle):
|
||||||
|
|
||||||
|
def __init__(self, pargs = None):
|
||||||
|
super().__init__('mastermind', self.__class__)
|
||||||
|
if not pargs:
|
||||||
|
pargs = {}
|
||||||
|
self._solution = []
|
||||||
|
self._attempts = {}
|
||||||
|
self._responses = {}
|
||||||
|
self._winners = []
|
||||||
|
self._played = set()
|
||||||
|
self._turn = 0
|
||||||
|
self._max_turn = 12
|
||||||
|
self._done = False
|
||||||
|
self._urlbase = '/'
|
||||||
|
|
||||||
|
def add_player(self, uuid: UUID) -> None:
|
||||||
|
self._attempts[uuid] = []
|
||||||
|
self._responses[uuid] = []
|
||||||
|
|
||||||
|
def get_extra_options_html(self) -> str:
|
||||||
|
return '''
|
||||||
|
<h3>Rule Options</h3>
|
||||||
|
<input type=checkbox name="rules" value="repeat" id="mastermind-rule-repeat" />
|
||||||
|
<label for="mastermind-rule-repeat">Colors may repeat</label><br/>
|
||||||
|
<input type=checkbox name="rules" value="empty" id="mastermind-rule-empty" />
|
||||||
|
<label for="mastermind-rule-empty">Empty slots</label><br/>
|
||||||
|
'''
|
||||||
|
|
||||||
|
def begin(self, options, urlbase):
|
||||||
|
self._urlbase = urlbase
|
||||||
|
colors = list(Color)
|
||||||
|
if 'empty' not in options.getall('rules'):
|
||||||
|
colors = colors[1:]
|
||||||
|
if 'repeat' in options.getall('rules'):
|
||||||
|
self._solution = random.choices(colors, 4)
|
||||||
|
else:
|
||||||
|
self._solution = random.sample(colors, 4)
|
||||||
|
self._turn = 1
|
||||||
|
|
||||||
|
def next_turn(self):
|
||||||
|
for p, _a in self._attempts.items():
|
||||||
|
attempt = _a[-1]
|
||||||
|
if attempt == self._solution:
|
||||||
|
self._winners.append(p)
|
||||||
|
self._done = True
|
||||||
|
# Need a separate list that we can remove correct attempts from so the same color doesn't
|
||||||
|
# get counted as correct a second time.
|
||||||
|
solution = [x for x in self._solution]
|
||||||
|
response = []
|
||||||
|
for i, a in enumerate(attempt):
|
||||||
|
if a == self._solution[i]:
|
||||||
|
response.append(ResponseColor.RED)
|
||||||
|
solution.remove(a)
|
||||||
|
elif a in solution:
|
||||||
|
response.append(ResponseColor.WHITE)
|
||||||
|
solution.remove(a)
|
||||||
|
else:
|
||||||
|
response.append(ResponseColor.BLACK)
|
||||||
|
response.sort()
|
||||||
|
self._responses[p].append(response)
|
||||||
|
self._played.clear()
|
||||||
|
if self._turn + 1 > self._max_turn:
|
||||||
|
self._done = True
|
||||||
|
if not self._done:
|
||||||
|
self._turn += 1
|
||||||
|
|
||||||
|
def process_action(self, puuid, action) -> None:
|
||||||
|
if action.get('action', 'none') != 'Submit':
|
||||||
|
return
|
||||||
|
if puuid in self._played:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
a1 = Color[action.get('attempt1', 'NONE').upper()]
|
||||||
|
a2 = Color[action.get('attempt2', 'NONE').upper()]
|
||||||
|
a3 = Color[action.get('attempt3', 'NONE').upper()]
|
||||||
|
a4 = Color[action.get('attempt4', 'NONE').upper()]
|
||||||
|
self._attempts[puuid].append([a1, a2, a3, a4])
|
||||||
|
self._played.add(puuid)
|
||||||
|
except BaseException as e:
|
||||||
|
print(e)
|
||||||
|
if len(list(filter(lambda x: len(x) < self._turn, self._attempts.values()))) == 0:
|
||||||
|
self.next_turn()
|
||||||
|
|
||||||
|
def serialize(self, player):
|
||||||
|
# todo
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def render(self, env, game, player, duration):
|
||||||
|
tmpl = env.get_template('mastermind/mastermind.html.j2')
|
||||||
|
return tmpl.render(players=game._players,
|
||||||
|
player=player.uuid,
|
||||||
|
played=self._played,
|
||||||
|
attempts=self._attempts[player.uuid],
|
||||||
|
responses=self._responses,
|
||||||
|
solution=self._solution,
|
||||||
|
winners=self._winners,
|
||||||
|
done=self._done,
|
||||||
|
turn=self._turn,
|
||||||
|
colors=Color,
|
||||||
|
rcolors=ResponseColor,
|
||||||
|
baseurl=self._urlbase)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def human_name(self) -> str:
|
||||||
|
return 'Mastermind'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scores(self):
|
||||||
|
return sorted(
|
||||||
|
[{'player': uuid, 'score': self._max_turn - len(self._attempts[uuid])} for uuid, player in self._players.items()],
|
||||||
|
key=lambda x: x['score'], reverse=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_completed(self) -> bool:
|
||||||
|
return self._done
|
||||||
|
|
||||||
|
|
||||||
|
Mastermind()
|
Loading…
Reference in a new issue