Initial commit

This commit is contained in:
s3lph 2021-10-31 14:44:14 +01:00
commit a1e6661249
91 changed files with 3767 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
**/__pycache__/
*.pyc
**/*.egg-info/
*.coverage
**/.mypy_cache/

5
requirements.txt Normal file
View file

@ -0,0 +1,5 @@
jinja3
bottle
sudoku-manager
sudokugen
namesgenerator

39
setup.py Executable file
View file

@ -0,0 +1,39 @@
#!/usr/bin/env python3
from setuptools import setup, find_packages
from webgames import __version__
setup(
name='webgames',
version=__version__,
author='s3lph',
author_email='',
description='',
license='MIT',
keywords='games,multiplayer,web',
url='',
packages=find_packages(exclude=['*.test']),
long_description='',
python_requires='>=3.6',
install_requires=[
'jinja2==2.11.1',
'bottle==0.12.18',
'sudoku-manager==1.0.6',
'sudokugen==0.2.1',
'namesgenerator==0.3'
],
entry_points={
'console_scripts': [
'webgames = webgames:main'
]
},
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Web Environment',
'Framework :: Bottle',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: MIT License',
'Topic :: Games/Entertainment :: Puzzle Games'
],
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

17
static/script.js Normal file
View file

@ -0,0 +1,17 @@
document.getElementById('joinurl').addEventListener('click',(e) => {
var url = document.getElementById('joinurl');
var sel = window.getSelection();
var range = document.createRange();
range.selectNodeContents(url);
sel.removeAllRanges();
sel.addRange(range);
if (document.execCommand('copy') === true) {
url.classList.add('copied');
setTimeout(() => {
url.classList.remove('copied');
}, 0);
}
sel.removeAllRanges();
});

60
static/set.js Normal file
View file

@ -0,0 +1,60 @@
function cardsEqual(a, b) {
return a.getAttribute('data-color') === b.getAttribute('data-color')
&& a.getAttribute('data-shape') === b.getAttribute('data-shape')
&& a.getAttribute('data-count') === b.getAttribute('data-count')
&& a.getAttribute('data-infill') === b.getAttribute('data-infill');
}
function replaceCard(a, b) {
a.innerHTML = b.innerHTML;
a.setAttribute('data-color', b.getAttribute('data-color'));
a.setAttribute('data-shape', b.getAttribute('data-shape'));
a.setAttribute('data-count', b.getAttribute('data-count'));
a.setAttribute('data-infill', b.getAttribute('data-infill'));
}
function markNewCard(card) {
card.classList.add('new');
setTimeout(() => {
card.classList.remove('new');
}, 3000);
}
function update() {
var req = new XMLHttpRequest();
req.addEventListener('load', (ev) => {
var parser = new DOMParser();
var doc = parser.parseFromString(req.responseText, 'text/html');
// Replace playing field cards
var field = document.getElementById('playingfield');
var aNodes = Array.from(field.children);
var bNodes = Array.from(doc.getElementById('playingfield').children);
for (var i = 0; i < Math.min(aNodes.length, bNodes.length); ++i) {
if (!cardsEqual(aNodes[i], bNodes[i])) {
replaceCard(aNodes[i], bNodes[i]);
markNewCard(aNodes[i]);
}
}
if (bNodes.length < aNodes.length) {
for (var i = bNodes.length; i < aNodes.length; ++i) {
aNodes[i].remove();
}
}
if (bNodes.length > aNodes.length) {
for (var i = aNodes.length; i < bNodes.length; ++i) {
field.appendChild(bNodes[i]);
markNewCard(bNodes[i]);
}
}
// Replace gamestats and scores
document.getElementById('gamestats').innerHTML =
doc.getElementById('gamestats').innerHTML;
document.getElementById('scorelist').innerHTML =
doc.getElementById('scorelist').innerHTML;
});
req.open('GET', window.location.href);
req.send();
}
setInterval(update, 500);

144
static/style.css Normal file
View file

@ -0,0 +1,144 @@
html, body {
padding: 0;
margin: 0;
background: #999999;
font-family: sans-serif;
}
main {
width: 1200px;
left: 0;
right: 0;
margin: auto;
}
code#joinurl {
cursor: pointer;
position: relative;
}
code#joinurl::after {
content: "Copied!";
text-align: center;
font-family: sans-serif;
font-weight: bold;
pointer-events: none;
position: absolute;
top: -1px;
right: -1px;
bottom: -1px;
left: -1px;
background: #ffffff;
opacity: 0;
transition: opacity 1s;
}
code#joinurl.copied::after {
opacity: 1;
transition: opacity 0s;
}
/*
* SUDOKU
*/
table, td {
border: 1px solid black;
border-collapse: collapse;
padding: 0;
background: white;
text-align: center;
}
tr.row-1 > td {
border-top: 3px solid black;
}
tr.row-3 > td, tr.row-6 > td, tr.row-9 > td {
border-bottom: 3px solid black;
}
td.col-1 {
border-left: 3px solid black;
}
td.col-3, td.col-6, td.col-9 {
border-right: 3px solid black;
}
input.sudoku-field:checked + label.sudoku-field {
background: #aaaaff;
}
input.sudoku-field {
display: none;
}
label.sudoku-field {
display: inline-block;
width: 45px;
height: 45px;
font-size: 2em;
margin: 0;
line-height: 45px;
}
label.sudoku-field-original {
background: #aaffaa;
}
input.fill {
width: 100% !important;
}
#sudoku-input input {
width: 45px;
height: 45px;
font-size: 2em;
}
/*
* SET
*/
#playingfield {
display: flex;
flex-direction: column;
flex-wrap: wrap;
align-content: flex-start;
max-height: 700px;
}
#playingfield .card > input {
display: none;
}
#playingfield .card.new > label #card {
filter: url(#new);
}
#playingfield .card > input:checked ~ label #card {
filter: url(#drop);
}
#playingfield .card > input:checked ~ label .svgcard {
transform: rotate(5deg);
transform-origin: 70px 100px;
}
.playercardlist .card {
display: inline-block;
margin-right: -120px;
}
.playercardlist .playercard-0 {
transform: rotate(-10deg) scale(0.5);
}
.playercardlist .playercard-1 {
transform: scale(0.5);
}
.playercardlist .playercard-2 {
transform: rotate(10deg) scale(0.5) translate(0px, 10px);
}

0
static/sudoku.js Normal file
View file

View file

@ -0,0 +1,6 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 0 H 100 V 100 H 0 Z" />
</g>
<path class="monastery{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 35 65 V 35 L 50 20 L 65 35 V 65 Z" />
</g>

View file

@ -0,0 +1,7 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 0 H 100 V 100 H 0 Z" />
<path class="road{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 45 100 V 60 H 55 V 100 Z" />
</g>
<path class="monastery{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 35 65 V 35 L 50 20 L 65 35 V 65 Z" />
</g>

View file

@ -0,0 +1,5 @@
<g transform="translate({{ x }} {{ y }}) rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 0 H 100 V 45 H 0 Z" />
<path class="pasture{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 100 H 100 V 55 H 0 Z" />
<path class="road{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 45 H 100 V 55 H 0 Z" />
</g>

View file

@ -0,0 +1,5 @@
<g transform="translate({{ x }} {{ y }}) rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 0 H 100 V 100 H 55 C 55 45, 55 45, 0 45 Z" />
<path class="pasture{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 100 H 45 C 45 55, 45 55, 0 55 Z" />
<path class="road{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 45 C 55 45, 55 45, 55 100 H 45 C 45 55, 45 55, 0 55 Z" />
</g>

View file

@ -0,0 +1,9 @@
<g transform="translate({{ x }} {{ y }}) rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 H 100 V 45 H 0 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 100 100 H 55 V 55 H 100 Z" />
<path class="pasture{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 100 H 45 V 55 H 0 Z" />
<path class="road{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 45 100 V 60 H 55 V 100 Z" />
<path class="road{% if resources[5].uuid() in claimable %} claimable{% endif %}" id="resource-5" data-uuid="{{ resources[5].uuid() }}" d="M 0 45 H 40 V 55 H 0 Z" />
<path class="road{% if resources[6].uuid() in claimable %} claimable{% endif %}" id="resource-6" data-uuid="{{ resources[6].uuid() }}" d="M 100 45 H 60 V 55 H 100 Z" />
<path class="road" d="M 40 45 L 45 40 H 55 L 60 45 V 55 L 55 60 H 45 L 40 55 Z" />
</g>

View file

@ -0,0 +1,11 @@
<g transform="translate({{ x }} {{ y }}) rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 H 45 V 45 H 0 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 55 0 H 100 V 45 H 55 Z" />
<path class="pasture{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 100 100 H 55 V 55 H 100 Z" />
<path class="pasture{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 0 100 H 45 V 55 H 0 Z" />
<path class="road{% if resources[5].uuid() in claimable %} claimable{% endif %}" id="resource-5" data-uuid="{{ resources[5].uuid() }}" d="M 45 100 V 60 H 55 V 100 Z" />
<path class="road{% if resources[6].uuid() in claimable %} claimable{% endif %}" id="resource-6" data-uuid="{{ resources[6].uuid() }}" d="M 0 45 H 40 V 55 H 0 Z" />
<path class="road{% if resources[7].uuid() in claimable %} claimable{% endif %}" id="resource-7" data-uuid="{{ resources[7].uuid() }}" d="M 100 45 H 60 V 55 H 100 Z" />
<path class="road{% if resources[8].uuid() in claimable %} claimable{% endif %}" id="resource-8" data-uuid="{{ resources[8].uuid() }}" d="M 45 0 V 40 H 55 V 0 Z" />
<path class="road" d="M 40 45 L 45 40 H 55 L 60 45 V 55 L 55 60 H 45 L 40 55 Z" />
</g>

View file

@ -0,0 +1,5 @@
<g transform="translate({{ x }} {{ y }}) rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 100 H 100 V 0 C 70 30, 30 30, 0 0 Z" />
<path class="city{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 Z" />
<!--<path class="citywall" d="M 0 0 C 30 30, 70 30, 100 0" />-->
</g>

View file

@ -0,0 +1,6 @@
<g transform="translate({{ x }} {{ y }}) rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 V 45 H 0 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 100 H 100 V 55 H 0 Z" />
<path class="road{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 45 H 100 V 55 H 0 Z" />
<path class="city{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 Z" />
</g>

View file

@ -0,0 +1,6 @@
<g transform="translate({{ x }} {{ y }}) rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 V 100 H 55 C 55 45, 55 45, 0 45 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 100 H 45 C 45 55, 45 55, 0 55 Z" />
<path class="road{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 45 C 55 45, 55 45, 55 100 H 45 C 45 55, 45 55, 0 55 Z" />
<path class="city{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 Z" />
</g>

View file

@ -0,0 +1,6 @@
<g transform="translate({{ x }} {{ y }}) rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 V 45 C 45 45, 45 45, 45 100 H 0 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 100 100 V 55 C 55 55, 55 55, 55 100 Z" />
<path class="road{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 45 100 C 45 45, 45 45, 100 45 V 55 C 55 55, 55 55, 55 100 Z" />
<path class="city{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 Z" />
</g>

View file

@ -0,0 +1,10 @@
<g transform="translate({{ x }} {{ y }}) rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 V 45 H 0 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 100 100 H 55 V 55 H 100 Z" />
<path class="pasture{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 100 H 45 V 55 H 0 Z" />
<path class="road{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 45 100 V 60 H 55 V 100 Z" />
<path class="road{% if resources[5].uuid() in claimable %} claimable{% endif %}" id="resource-5" data-uuid="{{ resources[5].uuid() }}" d="M 0 45 H 40 V 55 H 0 Z" />
<path class="road{% if resources[6].uuid() in claimable %} claimable{% endif %}" id="resource-6" data-uuid="{{ resources[6].uuid() }}" d="M 100 45 H 60 V 55 H 100 Z" />
<path class="city{% if resources[7].uuid() in claimable %} claimable{% endif %}" id="resource-7" data-uuid="{{ resources[7].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 Z" />
<path class="road" d="M 40 45 L 45 40 H 55 L 60 45 V 55 L 55 60 H 45 L 40 55 Z" />
</g>

View file

@ -0,0 +1,5 @@
<g transform="translate({{ x }} {{ y }}) rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 100 C 30 70, 70 70, 100 100 Z" />
<path class="city{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 V 100 C 70 70, 30 70, 0 100 Z" />
</g>

View file

@ -0,0 +1,8 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{rotation}} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 100 C 30 70, 70 70, 100 100 Z" />
<path class="city{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 V 100 C 70 70, 30 70, 0 100 Z" />
{% include "carcassonne/defense.svg" %}
</g>
</g>

View file

@ -0,0 +1,6 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{rotation}} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 100 H 100 C 100 70, 30 0, 0 0 Z" />
<path class="city{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 100 0 V 100 C 100 70, 30 0, 0 0 Z" />
</g>
</g>

View file

@ -0,0 +1,7 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{rotation}} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 100 H 100 C 100 70, 30 0, 0 0 Z" />
<path class="city{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 100 0 V 100 C 100 70, 30 0, 0 0 Z" />
{% include "carcassonne/defense.svg" %}
</g>
</g>

View file

@ -0,0 +1,7 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{rotation}} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 V 100 C 70 70, 30 70, 0 100 Z" />
<path class="city{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 Z" />
<path class="city{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 100 C 30 70, 70 70, 100 100 Z" />
</g>
</g>

View file

@ -0,0 +1,7 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{rotation}} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 C 70 30, 70 70, 100 100 H 0 Z" />
<path class="city{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 Z" />
<path class="city{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 100 0 C 70 30, 70 70, 100 100 Z" />
</g>
</g>

View file

@ -0,0 +1,8 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{rotation}} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 C 30 0, 100 70, 100 100 H 55 C 55 45, 55 45, 0 45 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 100 H 45 C 45 55, 45 55, 0 55 Z" />
<path class="road{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 45 C 55 45, 55 45, 55 100 H 45 C 45 55, 45 55, 0 55 Z" />
<path class="city{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 100 0 V 100 C 100 70, 30 0, 0 0 Z" />
</g>
</g>

View file

@ -0,0 +1,9 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{rotation}} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 C 30 0, 100 70, 100 100 H 55 C 55 45, 55 45, 0 45 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 100 H 45 C 45 55, 45 55, 0 55 Z" />
<path class="road{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 45 C 55 45, 55 45, 55 100 H 45 C 45 55, 45 55, 0 55 Z" />
<path class="city{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 100 0 V 100 C 100 70, 30 0, 0 0 Z" />
{% include "carcassonne/defense.svg" %}
</g>
</g>

View file

@ -0,0 +1,6 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{rotation}} 50 50)">
<path class="city{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 100 C 30 70, 70 70, 100 100 V 0 H 0 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 100 C 30 70, 70 70, 100 100 Z" />
</g>
</g>

View file

@ -0,0 +1,7 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{rotation}} 50 50)">
<path class="city{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 100 C 30 70, 70 70, 100 100 V 0 H 0 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 100 C 30 70, 70 70, 100 100 Z" />
{% include "carcassonne/defense.svg" %}
</g>
</g>

View file

@ -0,0 +1,8 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{rotation}} 50 50)">
<path class="city{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 100 C 30 70, 70 70, 100 100 V 0 H 0 Z" />
<path class="road{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 45 100 V 70 H 55 V 100 Z" />
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 100 C 30 70, 30 70, 45 70 V 100 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 100 100 C 70 70, 70 70, 55 70 V 100 Z" />
</g>
</g>

View file

@ -0,0 +1,9 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{rotation}} 50 50)">
<path class="city{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 100 C 30 70, 70 70, 100 100 V 0 H 0 Z" />
<path class="road{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 45 100 V 70 H 55 V 100 Z" />
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 100 C 30 70, 30 70, 45 70 V 100 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 100 100 C 70 70, 70 70, 55 70 V 100 Z" />
{% include "carcassonne/defense.svg" %}
</g>
</g>

View file

@ -0,0 +1,6 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{rotation}} 50 50)">
<path class="city{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 H 100 V 100 H 0 Z" />
{% include "carcassonne/defense.svg" %}
</g>
</g>

View file

@ -0,0 +1,161 @@
<!DOCTYPE html>
<html>
<head>
{% if not done %}
<meta http-equiv="refresh" content="3" />
{% endif %}
<style>
body > svg {
min-width: 50%;
max-width: 100%;
max-height: 70vh;
display: block;
}
svg .highlight {
stroke: black;
stroke-width: 1px;
fill: none;
}
svg .claimable:hover {
stroke: cyan;
stroke-width: 3px;
}
svg .empty {
fill: transparent;
stroke: grey;
stroke-width: 1px;
}
svg .plus {
fill: grey;
}
svg .defense {
fill: blue;
stroke-width: 3px;
stroke: white;
pointer-events: none;
}
svg .road {
fill: olive;
}
svg .city {
fill: brown;
}
svg .citywall {
stroke: #000000;
stroke-width: 5px;
fill: none;
}
svg .pasture {
fill: green;
}
{% for p in completed_pastures %}
svg .pasture[data-uuid="{{ p }}"] {
fill: #{{ p.__str__()[:6] }};
}
{% endfor %}
svg .monastery {
fill: #713c32;
}
svg .cathedral {
fill: #713c32;
pointer-events: none;
}
svg .inn {
fill: white;
pointer-events: none;
}
svg .shrine {
fill: #713c32;
}
svg .shrine-overlay {
fill: #713c32;
pointer-events: none;
}
svg .river {
fill: #86b4bc;
}
svg .bridgeshadow {
fill: #00000066;
pointer-events: none;
}
input[type="number"], input[value="Place Tile"], select {
display: none;
}
</style>
</head>
<body>
{{ field }}
{% if done %}
Game Over! <a href="{{ baseurl }}/{{ me.uuid }}">Return to lobby</a>
{% else %}
{% if current_player.uuid == me.uuid %}
It is your turn!
{% if phase == 'place' %}Place your tile{% else %}Claim your resource{% endif %}
<form name="controls" method="POST">
{% if phase == 'place' %}
<input type="submit" value="Rotate CCW" name="action" />
{{ card }}
<input type="submit" value="Rotate CW" name="action" />
<input type="number" value="0" name="x" />
<input type="number" value="0" name="y" />
<input type="submit" value="Place Tile" name="action" id="btn-place" />
{% endif %}
{% if phase == 'claim' %}
<select name="claim">
{{ claimable }}
</select>
<input type="submit" value="Claim" name="action" id="btn-claim" />
{% endif %}
</form>
<script>
let c = document.forms.controls;
let empties = document.getElementsByClassName('empty');
for (let i = 0; i < empties.length; ++i) {
empties[i].onclick = (e) => {
c.elements.x.value = e.target.getAttribute('data-x');
c.elements.y.value = e.target.getAttribute('data-y');
document.getElementById('btn-place').click();
};
};
let claimables = document.getElementsByClassName('claimable');
for (let i = 0; i < claimables.length; ++i) {
claimables[i].onclick = (e) => {
c.elements.claim.value = e.target.getAttribute('data-uuid');
document.getElementById('btn-claim').click();
};
};
</script>
{% else %}
It is <font color="{{ current_player.color.value.html }}">{{ game._players[current_player.uuid].name }}'s</font> turn.
{% endif %}
{% endif %}
<table border="1">
<tr><td>Name</td><td>Followers</td><td>Score</td></tr>
{% for player in players | sort(attribute='score', reverse=true) %}
<tr>
<td><font color="{{ player.color.value.html }}">{{ game._players[player.uuid].name }}</font></td>
<td>{{ player.followers | selectattr('resource', 'none') | list | length }}</td>
<td>{{ player.score }}</td>
</tr>
{% endfor %}
</table>
Deck: {{ deck | length }}
</body>
</html>

View file

@ -0,0 +1,3 @@
<g transform="translate(80 30)">
<path class="defense " transform="rotate(-{{rotation}})" d="M -7 -7 C -7 0, -7 0, 0 7 C 7 0, 7 0, 7 -7 Z" />
</g>

View file

@ -0,0 +1,4 @@
<g transform="translate({{ x }} {{ y }})">
<path class="plus" data-x="{{ cx }}" data-y="{{ cy }}" d="M 30 45 H 45 V 30 H 55 V 45 H 70 V 55 H 55 V 70 H 45 V 55 H 30 Z" />
<path class="empty" data-x="{{ cx }}" data-y="{{ cy }}" d="M 0 0 H 100 V 100 H 0 Z" />
</g>

View file

@ -0,0 +1,14 @@
<svg viewBox="0 0 {{ width*100 }} {{ height*100 }}">
{% for card in cards %}
{{ card }}
{% endfor %}
{% for empty in empties %}
<g transform="translate({{ empty[2] }} {{ empty[3] }})">
<path class="plus" data-x="{{ empty[0] }}" data-y="{{ empty[1] }}" d="M 30 45 H 45 V 30 H 55 V 45 H 70 V 55 H 55 V 70 H 45 V 55 H 30 Z" />
<path class="empty" data-x="{{ empty[0] }}" data-y="{{ empty[1] }}" d="M 0 0 H 100 V 100 H 0 Z" />
</g>
{% endfor %}
{% for follower in follows %}
{{ follower }}
{% endfor %}
</svg>

After

Width:  |  Height:  |  Size: 533 B

View file

@ -0,0 +1,3 @@
<g transform="translate({{ x }} {{ y }})">
<path class="follower" fill="{{ color }}" transform="translate({{ lx }} {{ ly }})" d="M -5 -5 C -20 -20, 20 -20, 5 -5 L 10 0 L 10 5 L 5 0 L 5 10 L 10 15 L 5 15 L 0 10 L -5 15 L -10 15 L -5 10 L -5 0 L -10 5 L -10 0 Z" />
</g>

View file

@ -0,0 +1,3 @@
<g transform="translate({{ x }} {{ y }})">
<path class="follower" fill="{{ color }}" transform="translate({{ lx }} {{ ly }}) rotate(80)" d="M -5 -5 C -20 -20, 20 -20, 5 -5 L 10 0 L 10 5 L 5 0 L 5 10 L 10 15 L 5 15 L 0 10 L -5 15 L -10 15 L -5 10 L -5 0 L -10 5 L -10 0 Z" />
</g>

View file

@ -0,0 +1,7 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 0 H 100 V 100 H 0 Z" />
</g>
<path class="shrine{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 30 75 V 25 H 35 V 75 Z M 65 75 V 25 H 70 V 75 Z M 25 20 C 50 25, 50 25, 75 20 V 25 C 50 30, 50 30, 25 25 Z M 27 30 H 73 V 33 H 27 Z" />
<path class="shrine-overlay" d="M 30 75 V 25 H 35 V 75 Z M 65 75 V 25 H 70 V 75 Z M 25 20 C 50 25, 50 25, 75 20 V 25 C 50 30, 50 30, 25 25 Z M 27 30 H 73 V 33 H 27 Z" />
</g>

View file

@ -0,0 +1,8 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 0 H 100 V 100 H 0 Z" />
<path class="road{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 45 100 V 70 H 55 V 100 Z" />
</g>
<path class="shrine{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 30 75 V 25 H 35 V 75 Z M 65 75 V 25 H 70 V 75 Z M 25 20 C 50 25, 50 25, 75 20 V 25 C 50 30, 50 30, 25 25 Z M 27 30 H 73 V 33 H 27 Z" />
<path class="shrine-overlay" d="M 30 75 V 25 H 35 V 75 Z M 65 75 V 25 H 70 V 75 Z M 25 20 C 50 25, 50 25, 75 20 V 25 C 50 30, 50 30, 25 25 Z M 27 30 H 73 V 33 H 27 Z" />
</g>

View file

@ -0,0 +1,11 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{ rotation }} 50 50)">
<path class="pasture" d="M 0 0 H 100 V 100 H 0 Z" />
<path class="pasture{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 0 0 H 45 V 100 H 0 Z" />
<path class="pasture{% if resources[5].uuid() in claimable %} claimable{% endif %}" id="resource-5" data-uuid="{{ resources[5].uuid() }}" d="M 55 0 H 100 V 100 H 55 Z" />
<path class="road{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 45 0 V 30 H 55 V 0 Z" />
<path class="road{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 45 100 V 70 H 55 V 100 Z" />
</g>
<path class="shrine{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 30 75 V 25 H 35 V 75 Z M 65 75 V 25 H 70 V 75 Z M 25 20 C 50 25, 50 25, 75 20 V 25 C 50 30, 50 30, 25 25 Z M 27 30 H 73 V 33 H 27 Z" />
<path class="shrine-overlay" d="M 30 75 V 25 H 35 V 75 Z M 65 75 V 25 H 70 V 75 Z M 25 20 C 50 25, 50 25, 75 20 V 25 C 50 30, 50 30, 25 25 Z M 27 30 H 73 V 33 H 27 Z" />
</g>

View file

@ -0,0 +1,8 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 100 H 100 V 0 C 70 30, 30 30, 0 0 Z" />
<path class="city{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 Z" />
</g>
<path class="shrine{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 30 75 V 25 H 35 V 75 Z M 65 75 V 25 H 70 V 75 Z M 25 20 C 50 25, 50 25, 75 20 V 25 C 50 30, 50 30, 25 25 Z M 27 30 H 73 V 33 H 27 Z" />
<path class="shrine-overlay" d="M 30 75 V 25 H 35 V 75 Z M 65 75 V 25 H 70 V 75 Z M 25 20 C 50 25, 50 25, 75 20 V 25 C 50 30, 50 30, 25 25 Z M 27 30 H 73 V 33 H 27 Z" />
</g>

View file

@ -0,0 +1,9 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 100 H 100 V 0 C 70 30, 30 30, 0 0 Z" />
<path class="city{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 Z" />
<path class="road{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 45 100 V 70 H 55 V 100 Z" />
</g>
<path class="shrine{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 30 75 V 25 H 35 V 75 Z M 65 75 V 25 H 70 V 75 Z M 25 20 C 50 25, 50 25, 75 20 V 25 C 50 30, 50 30, 25 25 Z M 27 30 H 73 V 33 H 27 Z" />
<path class="shrine-overlay" d="M 30 75 V 25 H 35 V 75 Z M 65 75 V 25 H 70 V 75 Z M 25 20 C 50 25, 50 25, 75 20 V 25 C 50 30, 50 30, 25 25 Z M 27 30 H 73 V 33 H 27 Z" />
</g>

View file

@ -0,0 +1,8 @@
<g transform="translate({{ x }} {{ y }}) rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 0 H 100 V 45 H 0 Z" />
<path class="pasture{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 100 H 100 V 55 H 0 Z" />
<path class="road{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 45 H 100 V 55 H 0 Z" />
<g transform="translate(50 30)">
<path class="inn" transform="rotate(-{{rotation}})" d="M -10 10 V -10 L 0 -20 L 10 -10 V 10 Z" />
</g>
</g>

View file

@ -0,0 +1,9 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 0 H 100 V 45 H 0 Z" />
<path class="pasture{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 100 H 100 V 55 H 0 Z" />
<path class="road{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 45 H 35 V 55 H 0 Z" />
<path class="road{% if resources[5].uuid() in claimable %} claimable{% endif %}" id="resource-5" data-uuid="{{ resources[5].uuid() }}" d="M 65 45 H 100 V 55 H 65 Z" />
</g>
<path class="monastery{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 35 65 V 35 L 50 20 L 65 35 V 65 Z" />
</g>

View file

@ -0,0 +1,8 @@
<g transform="translate({{ x }} {{ y }}) rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 0 H 100 V 100 H 55 C 55 45, 55 45, 0 45 Z" />
<path class="pasture{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 100 H 45 C 45 55, 45 55, 0 55 Z" />
<path class="road{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 45 C 55 45, 55 45, 55 100 H 45 C 45 55, 45 55, 0 55 Z" />
<g transform="translate(70 30)">
<path class="inn" transform="rotate(-{{rotation}})" d="M -10 10 V -10 L 0 -20 L 10 -10 V 10 Z" />
</g>
</g>

View file

@ -0,0 +1,12 @@
<g transform="translate({{ x }} {{ y }}) rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 H 100 V 45 H 0 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 100 100 H 55 V 55 H 100 Z" />
<path class="pasture{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 100 H 45 V 55 H 0 Z" />
<path class="road{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 45 100 V 60 H 55 V 100 Z" />
<path class="road{% if resources[5].uuid() in claimable %} claimable{% endif %}" id="resource-5" data-uuid="{{ resources[5].uuid() }}" d="M 0 45 H 40 V 55 H 0 Z" />
<path class="road{% if resources[6].uuid() in claimable %} claimable{% endif %}" id="resource-6" data-uuid="{{ resources[6].uuid() }}" d="M 100 45 H 60 V 55 H 100 Z" />
<path class="road" d="M 40 45 L 45 40 H 55 L 60 45 V 55 L 55 60 H 45 L 40 55 Z" />
<g transform="translate(80 30)">
<path class="inn" transform="rotate(-{{rotation}})" d="M -10 10 V -10 L 0 -20 L 10 -10 V 10 Z" />
</g>
</g>

View file

@ -0,0 +1,7 @@
<g transform="translate({{ x }} {{ y }}) rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 0 H 45 C 45 35, 35 45, 0 45 Z" />
<path class="pasture{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 0 100 V 55 C 45 55, 55 45, 55 0 H 100 V 45 C 55 45, 45 55, 45 100 Z" />
<path class="pasture{% if resources[5].uuid() in claimable %} claimable{% endif %}" id="resource-5" data-uuid="{{ resources[5].uuid() }}" d="M 100 100 H 55 C 55 65, 65 55, 100 55 Z" />
<path class="road{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 45 100 C 45 55, 55 45, 100 45 V 55 C 65 55, 55 65, 55 100 Z" />
<path class="road{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 45 C 35 45, 45 35, 45 0 H 55 C 55 45, 45 55, 0 55 Z" />
</g>

View file

@ -0,0 +1,5 @@
<g transform="translate({{ x }} {{ y }}) rotate({{ rotation }} 50 50)">
<path class="city{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 C 0 30, 70 100, 100 100 C 70 70, 70 30, 100 0 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 100 0 C 70 30, 70 70, 100 100 Z" />
<path class="pasture{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 100 H 100 C 70 100, 0 30, 0 0 Z" />
</g>

View file

@ -0,0 +1,6 @@
<g transform="translate({{ x }} {{ y }}) rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 55 100 V 30 C 70 30, 70 30, 100 0 V 100 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 45 100 V 30 C 30 30, 30 30, 0 0 V 100 Z" />
<path class="road{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 45 100 V 30 H 55 V 100 Z" />
<path class="city{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 0 0 C 30 30, 30 30, 45 30 H 55 C 70 30, 70 30, 100 0 Z" />
</g>

View file

@ -0,0 +1,11 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 V 100 H 55 C 55 45, 55 45, 0 45 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 100 H 45 C 45 55, 45 55, 0 55 Z" />
<path class="road{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 45 C 55 45, 55 45, 55 100 H 45 C 45 55, 45 55, 0 55 Z" />
<path class="city{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 Z" />
<g transform="translate(70 70)">
<path class="inn" transform="rotate(-{{rotation}})" d="M -10 10 V -10 L 0 -20 L 10 -10 V 10 Z" />
</g>
</g>
</g>

View file

@ -0,0 +1,12 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{rotation}} 50 50)">
<path class="city{% if resources[5].uuid() in claimable %} claimable{% endif %}" id="resource-5" data-uuid="{{ resources[5].uuid() }}" d="M 0 100 C 30 70, 30 70, 45 70 H 55 C 70 70, 70 70, 100 100 V 0 C 70 30, 70 30, 55 30 H 45 C 30 30, 30 30, 0 0 Z" />
<path class="road{% if resources[6].uuid() in claimable %} claimable{% endif %}" id="resource-6" data-uuid="{{ resources[6].uuid() }}" d="M 45 0 V 30 H 55 V 0 Z" />
<path class="road{% if resources[7].uuid() in claimable %} claimable{% endif %}" id="resource-7" data-uuid="{{ resources[7].uuid() }}" d="M 45 100 V 70 H 55 V 100 Z" />
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 C 30 30, 30 30, 45 30 V 0 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 100 0 C 70 30, 70 30, 55 30 V 0 Z" />
<path class="pasture{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 100 C 30 70, 30 70, 45 70 V 100 Z" />
<path class="pasture{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 100 100 C 70 70, 70 70, 55 70 V 100 Z" />
{% include "carcassonne/defense.svg" %}
</g>
</g>

View file

@ -0,0 +1,11 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{rotation}} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 100 100 H 55 V 25 C 55 0, 55 0, 100 0 Z" />
<path class="pasture{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[4].uuid() }}" d="M 45 100 H 0 C 0 70, 70 0, 45 30 Z" />
<path class="road{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 55 100 V 25 H 45 V 100" />
<path class="city{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 0 V 100 C 0 70, 70 0, 100 0 Z" />
<g transform="translate(70 70)">
<path class="inn" transform="rotate(-{{rotation}})" d="M -10 10 V -10 L 0 -20 L 10 -10 V 10 Z" />
</g>
</g>
</g>

View file

@ -0,0 +1,8 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{rotation}} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 100 H 45 V 25 C 45 0, 45 0, 0 0 Z" />
<path class="pasture{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 55 100 H 100 C 100 70, 30 0, 55 30 Z" />
<path class="road{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 45 100 V 25 H 55 V 100" />
<path class="city{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 100 0 V 100 C 100 70, 30 0, 0 0 Z" />
</g>
</g>

View file

@ -0,0 +1,12 @@
<g transform="translate({{ x }} {{ y }}) rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 C 30 30, 30 30, 45 30 V 45 H 0 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 100 0 C 70 30, 70 30, 55 30 V 45 H 100 Z" />
<path class="pasture{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 100 C 30 70, 30 70, 45 70 V 55 H 0 Z" />
<path class="pasture{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 100 100 C 70 70, 70 70, 45 70 V 55 H 100 Z" />
<path class="road{% if resources[5].uuid() in claimable %} claimable{% endif %}" id="resource-5" data-uuid="{{ resources[5].uuid() }}" d="M 0 45 H 40 V 55 H 0 Z" />
<path class="road{% if resources[6].uuid() in claimable %} claimable{% endif %}" id="resource-6" data-uuid="{{ resources[6].uuid() }}" d="M 100 45 H 60 V 55 H 100 Z" />
<path class="city{% if resources[7].uuid() in claimable %} claimable{% endif %}" id="resource-7" data-uuid="{{ resources[7].uuid() }}" d="M 0 0 C 30 30, 30 30, 45 30 H 55 C 70 30, 70 30, 100 0 Z" />
<path class="city{% if resources[8].uuid() in claimable %} claimable{% endif %}" id="resource-8" data-uuid="{{ resources[8].uuid() }}" d="M 0 100 C 30 70, 30 70, 45 70 H 55 C 70 70, 70 70, 100 100 Z" />
<path class="road" d="M 40 45 L 45 40 H 55 L 60 45 V 55 L 55 60 H 45 L 40 55 Z" />
<path class="road" d="M 45 30 H 55 V 70 H 45 Z" />
</g>

View file

@ -0,0 +1,12 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{rotation}} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 C 30 0, 100 70, 100 100 H 55 C 55 45, 55 45, 0 45 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 100 H 45 C 45 55, 45 55, 0 55 Z" />
<path class="road{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 45 C 55 45, 55 45, 55 100 H 45 C 45 55, 45 55, 0 55 Z" />
<path class="city{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 100 0 V 100 C 100 70, 30 0, 0 0 Z" />
<g transform="translate(30 70)">
<path class="inn" transform="rotate(-{{rotation}})" d="M -10 10 V -10 L 0 -20 L 10 -10 V 10 Z" />
</g>
{% include "carcassonne/defense.svg" %}
</g>
</g>

View file

@ -0,0 +1,6 @@
<g transform="translate({{ x }} {{ y }}) rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 C 70 30, 70 70, 100 100 H 0 C 30 70, 30 30, 0 0 Z" />
<path class="city{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 100 C 30 70, 30 30, 0 0 Z" />
<path class="city{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[3].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 Z" />
<path class="city{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[4].uuid() }}" d="M 100 0 C 70 30, 70 70, 100 100 Z" />
</g>

View file

@ -0,0 +1,6 @@
<g transform="translate({{ x }} {{ y }}) rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 C 0 30, 70 100, 100 100 C 70 70, 70 30, 100 0 Z" />
<path class="city{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 100 0 C 70 30, 70 70, 100 100 Z" />
<path class="city{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 100 H 100 C 70 100, 0 30, 0 0 Z" />
{% include "carcassonne/defense.svg" %}
</g>

View file

@ -0,0 +1,6 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{rotation}} 50 50)">
<path class="city{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 H 100 V 100 H 0 Z" />
</g>
<path class="cathedral" d="M 5 75 V 45 L 20 30 L 35 45 V 35 L 50 20 L 65 35 V 45 L 80 30 L 95 45 V 75 Z" />
</g>

View file

@ -0,0 +1,7 @@
<g transform="translate({{ x }} {{ y }}) rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[5].uuid() in claimable %} claimable{% endif %}" id="resource-5" data-uuid="{{ resources[5].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 C 70 30, 70 70, 100 100 C 70 70, 30 70, 0 100 C 30 70, 30 30, 0 0 Z" />
<path class="city{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 C 30 30, 70 30, 100 0 Z" />
<path class="city{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 100 0 C 70 30, 70 70, 100 100 Z" />
<path class="city{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 100 100 C 70 70, 30 70, 0 100 Z" />
<path class="city{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 0 100 C 30 70, 30 30, 0 0 Z" />
</g>

View file

@ -0,0 +1,8 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 100 C 0 70, 70 0, 100 0 V 40 C 40 40, 40 40, 40 100 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 100 100 V 60 C 60 60, 60 60, 60 100 Z" />
<path class="city{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 0 V 100 C 0 70, 70 0, 100 0 Z" />
<path class="river" d="M 40 100 C 40 40, 40 40, 100 40 V 60 C 60 60, 60 60, 60 100 Z" />
</g>
</g>

View file

@ -0,0 +1,9 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 C 30 30, 30 30, 30 45 V 55 C 30 70, 30 70, 0 100 H 40 V 0 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 100 0 C 70 30, 70 30, 70 45 V 55 C 70 70, 70 70, 100 100 H 60 V 0 Z" />
<path class="river" d="M 40 0 H 60 V 100 H 40 Z" />
<path class="bridgeshadow" d="M 30 45 H 70 V 55 H 30 Z" />
<path class="city{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 0 C 30 30, 30 30, 30 45 C 50 40, 50 40, 70 45 C 70 30, 70 30, 100 0 V 100 C 70 70, 70 70, 70 55 C 50 50, 50 50, 30 55 C 30 70, 30 70, 0 100 Z" />
</g>
</g>

View file

@ -0,0 +1,7 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 V 40 C 60 40, 60 40, 60 100 H 100 V 0 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 40 100 C 40 60, 40 60, 0 60 V 100 Z" />
<path class="river" d="M 0 40 C 60 40, 60 40, 60 100 H 40 C 40 60, 40 60, 0 60 Z" />
</g>
</g>

View file

@ -0,0 +1,8 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 H 40 V 100 H 0 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 60 0 H 100 V 40 H 60 Z" />
<path class="pasture{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 60 60 H 100 V 100 H 60 Z" />
<path class="river" d="M 40 0 H 60 V 40 H 100 V 60 H 60 V 100 H 40 Z" />
</g>
</g>

View file

@ -0,0 +1,7 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 H 100 V 100 H 0 Z" />
<path class="river" d="M 40 0 V 50 H 60 V 0 Z" />
<circle class="river" cx="50", cy="50", r="30" />
</g>
</g>

View file

@ -0,0 +1,9 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 C 30 30, 30 30, 40 30 V 100 H 0 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 100 0 C 70 30, 70 30, 60 30 V 100 H 100 Z" />
<path class="river" d="M 40 100 V 50 H 60 V 100 Z" />
<circle class="river" cx="50", cy="50", r="30" />
<path class="city{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 0 0 C 30 30, 30 30, 40 30 H 60 C 70 30, 70 30, 100 0 Z" />
</g>
</g>

View file

@ -0,0 +1,8 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 100 100 V 60 C 75 60, 75 90, 50 90 C 25 90, 25 60, 0 60 V 100 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 0 V 40 C 25 40, 25 70, 50 70 C 75 70, 75 40, 100 40 V 0 Z" />
<path class="river" d="M 0 40 C 25 40, 25 70, 50 70 C 75 70, 75 40, 100 40 V 60 C 75 60, 75 90, 50 90 C 25 90, 25 60, 0 60 Z"/>
</g>
<path class="monastery{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 35 65 V 35 L 50 20 L 65 35 V 65 Z" />
</g>

View file

@ -0,0 +1,15 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{ rotation }} 50 50)">
<path class="pasture" d="M 30 45 H 70 V 55 H 30 Z" />
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 100 V 55 H 40 V 100 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 0 V 45 H 40 V 0 Z" />
<path class="pasture{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 60 0 V 45 H 100 V 0 Z" />
<path class="pasture{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 60 55 V 100 H 100 V 55 Z" />
<path class="river" d="M 40 0 H 60 V 100 H 40 Z" />
<path class="bridgeshadow" d="M 30 45 H 70 V 55 H 30 Z" />
<path class="road{% if resources[5].uuid() in claimable %} claimable{% endif %}" id="resource-5" data-uuid="{{ resources[5].uuid() }}" d="M 0 45 H 30 C 50 40, 50 40, 70 45 H 100 V 55 H 70 C 50 50, 50 50, 30 55 H 0 Z" />
<g transform="translate(85 25)">
<path class="inn" transform="rotate(-{{rotation}})" d="M -10 10 V -10 L 0 -20 L 10 -10 V 10 Z" />
</g>
</g>
</g>

View file

@ -0,0 +1,9 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 0 V 40 C 60 40, 60 40, 60 100 H 100 V 55 C 55 55, 45 45, 45 0 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 100 V 60 C 40 60, 40 60, 40 100 Z" />
<path class="pasture{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 100 0 V 45 C 45 45, 45 45, 55 0 Z" />
<path class="river" d="M 0 40 C 60 40, 60 40, 60 100 H 40 C 40 60, 40 60, 0 60 Z" />
<path class="road{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 45 0 C 45 45, 55 55, 100 55 V 45 C 65 45, 55 35, 55 0 Z" />
</g>
</g>

View file

@ -0,0 +1,13 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{ rotation }} 50 50)">
<path class="pasture" d="M 30 45 H 70 V 55 H 30 Z" />
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 0 100 V 55 H 40 V 100 Z" />
<path class="pasture{% if resources[2].uuid() in claimable %} claimable{% endif %}" id="resource-2" data-uuid="{{ resources[2].uuid() }}" d="M 0 0 V 45 H 40 V 0 Z" />
<path class="pasture{% if resources[3].uuid() in claimable %} claimable{% endif %}" id="resource-3" data-uuid="{{ resources[3].uuid() }}" d="M 60 0 H 100 C 70 30, 70 30, 70 45 H 60 Z" />
<path class="pasture{% if resources[4].uuid() in claimable %} claimable{% endif %}" id="resource-4" data-uuid="{{ resources[4].uuid() }}" d="M 60 55 V 100 H 100 C 70 70, 70 70, 70 55 H 60 Z" />
<path class="river" d="M 40 0 H 60 V 100 H 40 Z" />
<path class="bridgeshadow" d="M 30 45 H 70 V 55 H 30 Z" />
<path class="road{% if resources[5].uuid() in claimable %} claimable{% endif %}" id="resource-5" data-uuid="{{ resources[5].uuid() }}" d="M 0 45 H 30 C 50 40, 50 40, 70 45 V 55 C 50 50, 50 50, 30 55 H 0 Z" />
<path class="city{% if resources[6].uuid() in claimable %} claimable{% endif %}" id="resource-6" data-uuid="{{ resources[6].uuid() }}" d="M 100 0 C 70 30, 70 30, 70 45 V 55 C 70 70, 70 70, 100 100 Z" />
</g>
</g>

View file

@ -0,0 +1,6 @@
<g transform="translate({{ x }} {{ y }})">
<g transform="rotate({{ rotation }} 50 50)">
<path class="pasture{% if resources[1].uuid() in claimable %} claimable{% endif %}" id="resource-1" data-uuid="{{ resources[1].uuid() }}" d="M 40 100 C 40 80, 40 80, 30 70 S 40 60, 60 50 S 60 40, 50 30 C 80 40, 80 40, 70 50 S 40 60, 50 70, S 60 80 60 100 Z H 100 V 0 H 0 V 100 Z" />
<path class="river" d="M 40 100 C 40 80, 40 80, 30 70 S 40 60, 60 50 S 60 40, 50 30 C 80 40, 80 40, 70 50 S 40 60, 50 70, S 60 80 60 100 Z" />
</g>
</g>

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Game</title>
<link rel="stylesheet" type="text/css" href="{{ baseurl }}static/style.css" />
</head>
<body>
{%- block body %}
{%- endblock %}
</body>
</html>

58
templates/cwa/set.html.j2 Normal file
View file

@ -0,0 +1,58 @@
{% extends 'cwa/base.html.j2' %}
{%- block body %}
<h1>Set: {{ game.human_id }}</h1>
<form method="POST">
<div id="playingfield">
{%- for card in cards %}
<div class="card"
data-count="{{ card.count.name }}"
data-shape="{{ card.shape.name }}"
data-color="{{ card.color.name }}"
data-infill="{{ card.infill.name }}">
<input type="checkbox" id="card-{{ loop.index0 }}" name="set" value="{{ loop.index0 }}" />
<label for="card-{{ loop.index0 }}">
{%- set symbol = card %}
{%- include 'cwa/setcard.svg.j2' %}
</label>
</div>
{%- endfor %}
</div>
<input type="submit" value="Set!" name="action" />
<input type="submit" value="Draw 3" name="action" />
<p id="gamestats">
{%- if game.puzzle.is_completed %}
<b>Game Over</b> <a href="..">Return to lobby</a>
{%- else %}
Draw votes: <b>{{ draw_votes }}</b> / {{ draw_majority }}
Deck: <b>{{ deck | length }}</b>
{% endif %}
</p>
</form>
<h2>Scores</h2>
<ol id="scorelist">
{%- for score in game.scoreboard %}
<li>
<b>{{ game.players[score['player']].name }}</b>
{{ score['score'] }}
<div class="playercardlist">
{%- for symbol in lastset[score['player']] %}
<div class="card playercard-{{ loop.index0 }}">
{%- include 'cwa/setcard.svg.j2' %}
</div>
{%- endfor %}
</div>
</li>
{%- endfor %}
</ol>
<a href="{{ baseurl }}/{{ player.uuid }}/{{ game.uuid }}/play">Refresh</a>
<script src="/static/set.js"></script>
{%- endblock %}

View file

@ -0,0 +1,50 @@
{%- macro fill(symbol) -%}
{%- if symbol.infill == Infill.NONE -%}
none
{%- elif symbol.infill == Infill.SEMI -%}
url(#semifill-{{ symbol.color.value.name }})
{%- elif symbol.infill == Infill.FULL -%}
{{- symbol.color.value.html -}}
{%- endif -%}
{%- endmacro -%}
<svg xmlns="http://www.w3.org/2000/svg" width="145" height="220" version="2.0">
<title>{{ symbol.count }} {{ symbol.shape }} {{ symbol.color }} {{ symbol.infill }}</title>
<defs>
{%- for color in Color %}
<pattern id="semifill-{{ color.value.name }}" width="20" height="9" fill="none" patternUnits="userSpaceOnUse">
<path stroke="{{ color.value.html }}" d="M 0 2 C 5 2, 5 -3, 10 -3 C 15 -3, 15 2, 20 2 M 0 5 C 5 5, 5 0, 10 0 C 15 0, 15 5, 20 5 M 0 8 C 5 8, 5 3, 10 3 C 15 3, 15 8, 20 8 M 0 11 C 5 11, 5 6, 10 6 C 15 6, 15 11, 20 11 M 0 14 C 5 14, 5 9, 10 9 C 15 9, 15 14, 20 14" />
</pattern>
{%- endfor %}
<filter id="drop" x="0" y="0">
<feDropShadow dx="10" dy="10" stdDeviation="5" />
</filter>
<filter id="new" x="0" y="0">
<feDropShadow dx="10" dy="10" stdDeviation="5" flood-color="gold" />
</filter>
</defs>
<g id="card" stroke-width="2" transform="translate(20, 20)">
<rect class="svgcard" width="100" height="180" rx="10" stroke="#000000" fill="#ffffff" />
<g class="svgcard" id="symbols">
{%- for i in range(symbol.count.value) %}
{%- set ybase = 80 - 25*(symbol.count.value - 1) + 50*i %}
{%- if symbol.shape == Shape.RECTANGLE %}
{#- rect #}
<rect x="20" width="60" height="20" y="{{ ybase }}" stroke="{{ symbol.color.value.html }}" fill="{{ fill(symbol) }}" />
{%- elif symbol.shape == Shape.TILDE %}
{#- tilde #}
<path width="60" height="20" transform="translate(20, {{ ybase }})" stroke="{{ symbol.color.value.html }}" fill="{{ fill(symbol) }}"
d="M 0 15 C 0 5, 5 0, 15 0 C 25 0, 35 5, 45 5 C 50 5, 53 0, 55 0 C 57 0, 60 0, 60 5 C 60 15, 55 20, 45 20 C 35 20, 25 15, 15 15 C 10 15, 7 20, 5 20 C 3 20, 0 20,0 15 Z"/>
{%- elif symbol.shape == Shape.ELLIPSE %}
{#- ellipse #}
<ellipse cx="50" cy="{{ ybase + 10 }}" rx="30" ry="10" stroke="{{ symbol.color.value.html }}" fill="{{ fill(symbol) }}" />
{%- endif %}
{%- endfor %}
</g>
</g>
</g>
</svg>

View file

@ -0,0 +1,68 @@
{% extends 'cwa/base.html.j2' %}
{%- block body %}
<h1>Sudoku: {{ game.human_id }} (<span id="game-timer">{{ duration }}</span>)</h1>
<main>
<form method="POST">
<div id="sudoku-field">
<table>
{%- for row in range(1, 10) %}
<tr class="{{ loop.cycle('odd', 'even') }} row-{{ row }}">
{%- for col in range(1, 10) %}
<td class="{{ loop.cycle('odd', 'even') }} col-{{ col }}">
{%- if puzzle.original[row-1][col-1] %}
<label class="sudoku-field sudoku-field-original">
{{ puzzle.grid[row-1][col-1] | default('', true) }}
</label>
{%- else %}
<input type="radio" id="sudoku-field-{{ row }}{{ col }}" name="sudoku-field" value="{{ row }}{{ col }}" class="sudoku-field"/>
<label for="sudoku-field-{{ row }}{{ col }}" id="sudoku-field-{{ row }}{{ col }}-label" class="sudoku-field">
{{ puzzle.grid[row-1][col-1] | default('', true) }}
</label>
{%- endif %}
</td>
{%- endfor %}
</tr>
{%- endfor %}
</table>
</div>
<div id="sudoku-input">
<table>
{%- for row in range(0, 3) %}
<tr>
{%- for col in range(1, 4) %}
<td>
<input type="submit" value="{{ row * 3 + col }}" name="number" />
</td>
{%- endfor %}
</tr>
{%- endfor %}
<tr>
<td colspan="3">
<input type="submit" value="Delete" name="number" class="fill" />
</td>
</tr>
</table>
</div>
</form>
</main>
<h2>Players in Game</h2>
<ul>
{%- for pkey in game.players.keys() %}
<li>
<b>{{ game.players[pkey].name }}</b>
(Progress: {{ (game.puzzle.puzzles[pkey].progress * 100) | int }} %)
</li>
{%- endfor %}
</ul>
<h2>Scores</h2>
<ol>
{%- for score in game.scoreboard %}
<li>
<b>{{ game.players[score['player']].name }}</b>
{{ score['duration'] }}
</li>
{%- endfor %}
</ol>
<a href="{{ baseurl }}/{{ player.uuid }}/{{ game.uuid }}/play">Refresh</a>
{%- endblock %}

View file

@ -0,0 +1,15 @@
{% extends 'cwa/base.html.j2' %}
{% block body %}
<h1>New Game</h1>
<form action="{{ baseurl }}" method="POST">
<ol>
<li>Choose a name: <input type="text" name="name" placeholder="{{ placeholder }}" /><input type="submit" value="Go!"/></li>
<li>Join or start a game</li>
<li>Invite other players</li>
<li>Play!</li>
</ol>
<input type="hidden" name="placeholder" value="{{ placeholder }}" /></li>
</form>
{% endblock %}

View file

@ -0,0 +1,20 @@
{% extends 'cwa/base.html.j2' %}
{% block body %}
<h1>New Game</h1>
<form action="{{ baseurl }}{{ player_uuid }}" method="POST">
<ol>
<li>Choose a name: <b>{{ player_name }}</b></li>
<li>Join or start a game:
<select name="puzzle">
<option selected="selected" value="sudoku">Sudoku</option>
<option value="set">Set</option>
<option value="carcassonne">Carcassonne</option>
</select>
<input type="text" name="game" placeholder="empty: new game"/><input type="submit" value="Go!"/></li>
<li>Invite other players</li>
<li>Play!</li>
</ol>
</form>
{% endblock %}

View file

@ -0,0 +1,29 @@
{% extends 'cwa/base.html.j2' %}
{% block body %}
<h1>{{ game.puzzle_name }}: {{ game.human_id }}</h1>
<form action="{{ baseurl }}{{ player.uuid }}/{{ game.uuid }}/play" method="post">
<ol>
<li>Choose a name: <b>{{ player.name }}</b></li>
<li>Join or start a game of <b>{{ game.puzzle_name }}</b>: <b>{{ game.human_id }}</b></li>
<li>Invite other players: <code id="joinurl">{{ host }}{{ baseurl }}{{ game.human_id }}</code></li>
<li><input type="submit" value="Play!" /></li>
</ol>
<h2>Game Options</h2>
{{ game.get_extra_options_html() }}
<h2>Players in Game</h2>
<ul>
{% for player in game.players.values() %}
<li>{{ player.name }}</li>
{% endfor %}
</ul>
<a href="{{ baseurl }}{{ player.uuid }}/{{ game.uuid }}/lobby">Refresh</a>
</form>
<script src="{{ baseurl }}static/script.js"></script>
{% endblock %}

View file

@ -0,0 +1,15 @@
{% extends 'cwa/base.html.j2' %}
{% block body %}
<h1>{{ game.puzzle_name }}: {{ game.human_id }}</h1>
<form action="{{ baseurl}}{{ game.human_id }}" method="POST">
<ol>
<li>Choose a name: <input type="text" name="name" placeholder="{{ placeholder }}" /><input type="submit" value="Go!"/></li>
<li>Join or start a game of <b>{{ game.puzzle_name }}</b>: <b>{{ game.human_id }}</b></li>
<li>Invite other players</li>
<li>Play!</li>
</ol>
<input type="hidden" name="placeholder" value="{{ placeholder }}" /></li>
</form>
{% endblock %}

View file

@ -0,0 +1,658 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sudoku</title>
<link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
<main>
<form method="POST" action="#">
<div id="sudoku-field">
<table>
<tr class="odd row-1">
<td class="odd col-1">
<input type="radio" id="sudoku-field-11", name="sudoku-field" value="11" class="sudoku-field"/>
<label for="sudoku-field-11" id="sudoku-field-11-label" class="sudoku-field">
1
</label>
</td>
<td class="even col-2">
<input type="radio" id="sudoku-field-12", name="sudoku-field" value="12" class="sudoku-field"/>
<label for="sudoku-field-12" id="sudoku-field-12-label" class="sudoku-field">
</label>
</td>
<td class="odd col-3">
<input type="radio" id="sudoku-field-13", name="sudoku-field" value="13" class="sudoku-field"/>
<label for="sudoku-field-13" id="sudoku-field-13-label" class="sudoku-field">
</label>
</td>
<td class="even col-4">
<input type="radio" id="sudoku-field-14", name="sudoku-field" value="14" class="sudoku-field"/>
<label for="sudoku-field-14" id="sudoku-field-14-label" class="sudoku-field">
</label>
</td>
<td class="odd col-5">
<input type="radio" id="sudoku-field-15", name="sudoku-field" value="15" class="sudoku-field"/>
<label for="sudoku-field-15" id="sudoku-field-15-label" class="sudoku-field">
</label>
</td>
<td class="even col-6">
<input type="radio" id="sudoku-field-16", name="sudoku-field" value="16" class="sudoku-field"/>
<label for="sudoku-field-16" id="sudoku-field-16-label" class="sudoku-field">
</label>
</td>
<td class="odd col-7">
<input type="radio" id="sudoku-field-17", name="sudoku-field" value="17" class="sudoku-field"/>
<label for="sudoku-field-17" id="sudoku-field-17-label" class="sudoku-field">
</label>
</td>
<td class="even col-8">
<input type="radio" id="sudoku-field-18", name="sudoku-field" value="18" class="sudoku-field"/>
<label for="sudoku-field-18" id="sudoku-field-18-label" class="sudoku-field">
</label>
</td>
<td class="odd col-9">
<input type="radio" id="sudoku-field-19", name="sudoku-field" value="19" class="sudoku-field"/>
<label for="sudoku-field-19" id="sudoku-field-19-label" class="sudoku-field">
</label>
</td>
</tr>
<tr class="even row-2">
<td class="odd col-1">
<input type="radio" id="sudoku-field-21", name="sudoku-field" value="21" class="sudoku-field"/>
<label for="sudoku-field-21" id="sudoku-field-21-label" class="sudoku-field">
</label>
</td>
<td class="even col-2">
<input type="radio" id="sudoku-field-22", name="sudoku-field" value="22" class="sudoku-field"/>
<label for="sudoku-field-22" id="sudoku-field-22-label" class="sudoku-field">
</label>
</td>
<td class="odd col-3">
<input type="radio" id="sudoku-field-23", name="sudoku-field" value="23" class="sudoku-field"/>
<label for="sudoku-field-23" id="sudoku-field-23-label" class="sudoku-field">
</label>
</td>
<td class="even col-4">
<input type="radio" id="sudoku-field-24", name="sudoku-field" value="24" class="sudoku-field"/>
<label for="sudoku-field-24" id="sudoku-field-24-label" class="sudoku-field">
1
</label>
</td>
<td class="odd col-5">
<input type="radio" id="sudoku-field-25", name="sudoku-field" value="25" class="sudoku-field"/>
<label for="sudoku-field-25" id="sudoku-field-25-label" class="sudoku-field">
</label>
</td>
<td class="even col-6">
<input type="radio" id="sudoku-field-26", name="sudoku-field" value="26" class="sudoku-field"/>
<label for="sudoku-field-26" id="sudoku-field-26-label" class="sudoku-field">
</label>
</td>
<td class="odd col-7">
<input type="radio" id="sudoku-field-27", name="sudoku-field" value="27" class="sudoku-field"/>
<label for="sudoku-field-27" id="sudoku-field-27-label" class="sudoku-field">
</label>
</td>
<td class="even col-8">
<input type="radio" id="sudoku-field-28", name="sudoku-field" value="28" class="sudoku-field"/>
<label for="sudoku-field-28" id="sudoku-field-28-label" class="sudoku-field">
</label>
</td>
<td class="odd col-9">
<input type="radio" id="sudoku-field-29", name="sudoku-field" value="29" class="sudoku-field"/>
<label for="sudoku-field-29" id="sudoku-field-29-label" class="sudoku-field">
</label>
</td>
</tr>
<tr class="odd row-3">
<td class="odd col-1">
<input type="radio" id="sudoku-field-31", name="sudoku-field" value="31" class="sudoku-field"/>
<label for="sudoku-field-31" id="sudoku-field-31-label" class="sudoku-field">
</label>
</td>
<td class="even col-2">
<input type="radio" id="sudoku-field-32", name="sudoku-field" value="32" class="sudoku-field"/>
<label for="sudoku-field-32" id="sudoku-field-32-label" class="sudoku-field">
</label>
</td>
<td class="odd col-3">
<input type="radio" id="sudoku-field-33", name="sudoku-field" value="33" class="sudoku-field"/>
<label for="sudoku-field-33" id="sudoku-field-33-label" class="sudoku-field">
</label>
</td>
<td class="even col-4">
<input type="radio" id="sudoku-field-34", name="sudoku-field" value="34" class="sudoku-field"/>
<label for="sudoku-field-34" id="sudoku-field-34-label" class="sudoku-field">
</label>
</td>
<td class="odd col-5">
<input type="radio" id="sudoku-field-35", name="sudoku-field" value="35" class="sudoku-field"/>
<label for="sudoku-field-35" id="sudoku-field-35-label" class="sudoku-field">
</label>
</td>
<td class="even col-6">
<input type="radio" id="sudoku-field-36", name="sudoku-field" value="36" class="sudoku-field"/>
<label for="sudoku-field-36" id="sudoku-field-36-label" class="sudoku-field">
</label>
</td>
<td class="odd col-7">
<input type="radio" id="sudoku-field-37", name="sudoku-field" value="37" class="sudoku-field"/>
<label for="sudoku-field-37" id="sudoku-field-37-label" class="sudoku-field">
1
</label>
</td>
<td class="even col-8">
<input type="radio" id="sudoku-field-38", name="sudoku-field" value="38" class="sudoku-field"/>
<label for="sudoku-field-38" id="sudoku-field-38-label" class="sudoku-field">
</label>
</td>
<td class="odd col-9">
<input type="radio" id="sudoku-field-39", name="sudoku-field" value="39" class="sudoku-field"/>
<label for="sudoku-field-39" id="sudoku-field-39-label" class="sudoku-field">
</label>
</td>
</tr>
<tr class="even row-4">
<td class="odd col-1">
<input type="radio" id="sudoku-field-41", name="sudoku-field" value="41" class="sudoku-field"/>
<label for="sudoku-field-41" id="sudoku-field-41-label" class="sudoku-field">
</label>
</td>
<td class="even col-2">
<input type="radio" id="sudoku-field-42", name="sudoku-field" value="42" class="sudoku-field"/>
<label for="sudoku-field-42" id="sudoku-field-42-label" class="sudoku-field">
1
</label>
</td>
<td class="odd col-3">
<input type="radio" id="sudoku-field-43", name="sudoku-field" value="43" class="sudoku-field"/>
<label for="sudoku-field-43" id="sudoku-field-43-label" class="sudoku-field">
</label>
</td>
<td class="even col-4">
<input type="radio" id="sudoku-field-44", name="sudoku-field" value="44" class="sudoku-field"/>
<label for="sudoku-field-44" id="sudoku-field-44-label" class="sudoku-field">
</label>
</td>
<td class="odd col-5">
<input type="radio" id="sudoku-field-45", name="sudoku-field" value="45" class="sudoku-field"/>
<label for="sudoku-field-45" id="sudoku-field-45-label" class="sudoku-field">
</label>
</td>
<td class="even col-6">
<input type="radio" id="sudoku-field-46", name="sudoku-field" value="46" class="sudoku-field"/>
<label for="sudoku-field-46" id="sudoku-field-46-label" class="sudoku-field">
</label>
</td>
<td class="odd col-7">
<input type="radio" id="sudoku-field-47", name="sudoku-field" value="47" class="sudoku-field"/>
<label for="sudoku-field-47" id="sudoku-field-47-label" class="sudoku-field">
</label>
</td>
<td class="even col-8">
<input type="radio" id="sudoku-field-48", name="sudoku-field" value="48" class="sudoku-field"/>
<label for="sudoku-field-48" id="sudoku-field-48-label" class="sudoku-field">
</label>
</td>
<td class="odd col-9">
<input type="radio" id="sudoku-field-49", name="sudoku-field" value="49" class="sudoku-field"/>
<label for="sudoku-field-49" id="sudoku-field-49-label" class="sudoku-field">
</label>
</td>
</tr>
<tr class="odd row-5">
<td class="odd col-1">
<input type="radio" id="sudoku-field-51", name="sudoku-field" value="51" class="sudoku-field"/>
<label for="sudoku-field-51" id="sudoku-field-51-label" class="sudoku-field">
</label>
</td>
<td class="even col-2">
<input type="radio" id="sudoku-field-52", name="sudoku-field" value="52" class="sudoku-field"/>
<label for="sudoku-field-52" id="sudoku-field-52-label" class="sudoku-field">
</label>
</td>
<td class="odd col-3">
<input type="radio" id="sudoku-field-53", name="sudoku-field" value="53" class="sudoku-field"/>
<label for="sudoku-field-53" id="sudoku-field-53-label" class="sudoku-field">
</label>
</td>
<td class="even col-4">
<input type="radio" id="sudoku-field-54", name="sudoku-field" value="54" class="sudoku-field"/>
<label for="sudoku-field-54" id="sudoku-field-54-label" class="sudoku-field">
</label>
</td>
<td class="odd col-5">
<input type="radio" id="sudoku-field-55", name="sudoku-field" value="55" class="sudoku-field"/>
<label for="sudoku-field-55" id="sudoku-field-55-label" class="sudoku-field">
1
</label>
</td>
<td class="even col-6">
<input type="radio" id="sudoku-field-56", name="sudoku-field" value="56" class="sudoku-field"/>
<label for="sudoku-field-56" id="sudoku-field-56-label" class="sudoku-field">
</label>
</td>
<td class="odd col-7">
<input type="radio" id="sudoku-field-57", name="sudoku-field" value="57" class="sudoku-field"/>
<label for="sudoku-field-57" id="sudoku-field-57-label" class="sudoku-field">
</label>
</td>
<td class="even col-8">
<input type="radio" id="sudoku-field-58", name="sudoku-field" value="58" class="sudoku-field"/>
<label for="sudoku-field-58" id="sudoku-field-58-label" class="sudoku-field">
</label>
</td>
<td class="odd col-9">
<input type="radio" id="sudoku-field-59", name="sudoku-field" value="59" class="sudoku-field"/>
<label for="sudoku-field-59" id="sudoku-field-59-label" class="sudoku-field">
</label>
</td>
</tr>
<tr class="even row-6">
<td class="odd col-1">
<input type="radio" id="sudoku-field-61", name="sudoku-field" value="61" class="sudoku-field"/>
<label for="sudoku-field-61" id="sudoku-field-61-label" class="sudoku-field">
</label>
</td>
<td class="even col-2">
<input type="radio" id="sudoku-field-62", name="sudoku-field" value="62" class="sudoku-field"/>
<label for="sudoku-field-62" id="sudoku-field-62-label" class="sudoku-field">
</label>
</td>
<td class="odd col-3">
<input type="radio" id="sudoku-field-63", name="sudoku-field" value="63" class="sudoku-field"/>
<label for="sudoku-field-63" id="sudoku-field-63-label" class="sudoku-field">
</label>
</td>
<td class="even col-4">
<input type="radio" id="sudoku-field-64", name="sudoku-field" value="64" class="sudoku-field"/>
<label for="sudoku-field-64" id="sudoku-field-64-label" class="sudoku-field">
</label>
</td>
<td class="odd col-5">
<input type="radio" id="sudoku-field-65", name="sudoku-field" value="65" class="sudoku-field"/>
<label for="sudoku-field-65" id="sudoku-field-65-label" class="sudoku-field">
</label>
</td>
<td class="even col-6">
<input type="radio" id="sudoku-field-66", name="sudoku-field" value="66" class="sudoku-field"/>
<label for="sudoku-field-66" id="sudoku-field-66-label" class="sudoku-field">
</label>
</td>
<td class="odd col-7">
<input type="radio" id="sudoku-field-67", name="sudoku-field" value="67" class="sudoku-field"/>
<label for="sudoku-field-67" id="sudoku-field-67-label" class="sudoku-field">
</label>
</td>
<td class="even col-8">
<input type="radio" id="sudoku-field-68", name="sudoku-field" value="68" class="sudoku-field"/>
<label for="sudoku-field-68" id="sudoku-field-68-label" class="sudoku-field">
1
</label>
</td>
<td class="odd col-9">
<input type="radio" id="sudoku-field-69", name="sudoku-field" value="69" class="sudoku-field"/>
<label for="sudoku-field-69" id="sudoku-field-69-label" class="sudoku-field">
</label>
</td>
</tr>
<tr class="odd row-7">
<td class="odd col-1">
<input type="radio" id="sudoku-field-71", name="sudoku-field" value="71" class="sudoku-field"/>
<label for="sudoku-field-71" id="sudoku-field-71-label" class="sudoku-field">
</label>
</td>
<td class="even col-2">
<input type="radio" id="sudoku-field-72", name="sudoku-field" value="72" class="sudoku-field"/>
<label for="sudoku-field-72" id="sudoku-field-72-label" class="sudoku-field">
</label>
</td>
<td class="odd col-3">
<input type="radio" id="sudoku-field-73", name="sudoku-field" value="73" class="sudoku-field"/>
<label for="sudoku-field-73" id="sudoku-field-73-label" class="sudoku-field">
1
</label>
</td>
<td class="even col-4">
<input type="radio" id="sudoku-field-74", name="sudoku-field" value="74" class="sudoku-field"/>
<label for="sudoku-field-74" id="sudoku-field-74-label" class="sudoku-field">
</label>
</td>
<td class="odd col-5">
<input type="radio" id="sudoku-field-75", name="sudoku-field" value="75" class="sudoku-field"/>
<label for="sudoku-field-75" id="sudoku-field-75-label" class="sudoku-field">
</label>
</td>
<td class="even col-6">
<input type="radio" id="sudoku-field-76", name="sudoku-field" value="76" class="sudoku-field"/>
<label for="sudoku-field-76" id="sudoku-field-76-label" class="sudoku-field">
</label>
</td>
<td class="odd col-7">
<input type="radio" id="sudoku-field-77", name="sudoku-field" value="77" class="sudoku-field"/>
<label for="sudoku-field-77" id="sudoku-field-77-label" class="sudoku-field">
</label>
</td>
<td class="even col-8">
<input type="radio" id="sudoku-field-78", name="sudoku-field" value="78" class="sudoku-field"/>
<label for="sudoku-field-78" id="sudoku-field-78-label" class="sudoku-field">
</label>
</td>
<td class="odd col-9">
<input type="radio" id="sudoku-field-79", name="sudoku-field" value="79" class="sudoku-field"/>
<label for="sudoku-field-79" id="sudoku-field-79-label" class="sudoku-field">
</label>
</td>
</tr>
<tr class="even row-8">
<td class="odd col-1">
<input type="radio" id="sudoku-field-81", name="sudoku-field" value="81" class="sudoku-field"/>
<label for="sudoku-field-81" id="sudoku-field-81-label" class="sudoku-field">
</label>
</td>
<td class="even col-2">
<input type="radio" id="sudoku-field-82", name="sudoku-field" value="82" class="sudoku-field"/>
<label for="sudoku-field-82" id="sudoku-field-82-label" class="sudoku-field">
</label>
</td>
<td class="odd col-3">
<input type="radio" id="sudoku-field-83", name="sudoku-field" value="83" class="sudoku-field"/>
<label for="sudoku-field-83" id="sudoku-field-83-label" class="sudoku-field">
</label>
</td>
<td class="even col-4">
<input type="radio" id="sudoku-field-84", name="sudoku-field" value="84" class="sudoku-field"/>
<label for="sudoku-field-84" id="sudoku-field-84-label" class="sudoku-field">
</label>
</td>
<td class="odd col-5">
<input type="radio" id="sudoku-field-85", name="sudoku-field" value="85" class="sudoku-field"/>
<label for="sudoku-field-85" id="sudoku-field-85-label" class="sudoku-field">
</label>
</td>
<td class="even col-6">
<input type="radio" id="sudoku-field-86", name="sudoku-field" value="86" class="sudoku-field"/>
<label for="sudoku-field-86" id="sudoku-field-86-label" class="sudoku-field">
1
</label>
</td>
<td class="odd col-7">
<input type="radio" id="sudoku-field-87", name="sudoku-field" value="87" class="sudoku-field"/>
<label for="sudoku-field-87" id="sudoku-field-87-label" class="sudoku-field">
</label>
</td>
<td class="even col-8">
<input type="radio" id="sudoku-field-88", name="sudoku-field" value="88" class="sudoku-field"/>
<label for="sudoku-field-88" id="sudoku-field-88-label" class="sudoku-field">
</label>
</td>
<td class="odd col-9">
<input type="radio" id="sudoku-field-89", name="sudoku-field" value="89" class="sudoku-field"/>
<label for="sudoku-field-89" id="sudoku-field-89-label" class="sudoku-field">
</label>
</td>
</tr>
<tr class="odd row-9">
<td class="odd col-1">
<input type="radio" id="sudoku-field-91", name="sudoku-field" value="91" class="sudoku-field"/>
<label for="sudoku-field-91" id="sudoku-field-91-label" class="sudoku-field">
</label>
</td>
<td class="even col-2">
<input type="radio" id="sudoku-field-92", name="sudoku-field" value="92" class="sudoku-field"/>
<label for="sudoku-field-92" id="sudoku-field-92-label" class="sudoku-field">
</label>
</td>
<td class="odd col-3">
<input type="radio" id="sudoku-field-93", name="sudoku-field" value="93" class="sudoku-field"/>
<label for="sudoku-field-93" id="sudoku-field-93-label" class="sudoku-field">
</label>
</td>
<td class="even col-4">
<input type="radio" id="sudoku-field-94", name="sudoku-field" value="94" class="sudoku-field"/>
<label for="sudoku-field-94" id="sudoku-field-94-label" class="sudoku-field">
</label>
</td>
<td class="odd col-5">
<input type="radio" id="sudoku-field-95", name="sudoku-field" value="95" class="sudoku-field"/>
<label for="sudoku-field-95" id="sudoku-field-95-label" class="sudoku-field">
</label>
</td>
<td class="even col-6">
<input type="radio" id="sudoku-field-96", name="sudoku-field" value="96" class="sudoku-field"/>
<label for="sudoku-field-96" id="sudoku-field-96-label" class="sudoku-field">
</label>
</td>
<td class="odd col-7">
<input type="radio" id="sudoku-field-97", name="sudoku-field" value="97" class="sudoku-field"/>
<label for="sudoku-field-97" id="sudoku-field-97-label" class="sudoku-field">
</label>
</td>
<td class="even col-8">
<input type="radio" id="sudoku-field-98", name="sudoku-field" value="98" class="sudoku-field"/>
<label for="sudoku-field-98" id="sudoku-field-98-label" class="sudoku-field">
</label>
</td>
<td class="odd col-9">
<input type="radio" id="sudoku-field-99", name="sudoku-field" value="99" class="sudoku-field"/>
<label for="sudoku-field-99" id="sudoku-field-99-label" class="sudoku-field">
1
</label>
</td>
</tr>
</table>
</div>
<div id="sudoku-input">
<table>
<tr>
<td><input type="submit" value="1" name="number" /></td>
<td><input type="submit" value="2" name="number" /></td>
<td><input type="submit" value="3" name="number" /></td>
</tr>
<tr>
<td><input type="submit" value="4" name="number" /></td>
<td><input type="submit" value="5" name="number" /></td>
<td><input type="submit" value="6" name="number" /></td>
</tr>
<tr>
<td><input type="submit" value="7" name="number" /></td>
<td><input type="submit" value="8" name="number" /></td>
<td><input type="submit" value="9" name="number" /></td>
</tr>
</table>
</div>
</form>
</main>
</body>
</html>

55
templates/sudoku.html Normal file
View file

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sudoku</title>
<link rel="stylesheet" type="text/css" href="/static/style.css" />
</head>
<body>
<main>
<form method="POST" action="/">
<div id="sudoku-field">
<table>
{% for row in range(1, 10) %}
<tr class="{{ loop.cycle('odd', 'even') }} row-{{ row }}">
{% for col in range(1, 10) %}
<td class="{{ loop.cycle('odd', 'even') }} col-{{ col }}">
{% if sudoku_original[row-1][col-1] %}
<label class="sudoku-field sudoku-field-original">
{{ sudoku_fields[row-1][col-1] | default('', true) }}
</label>
{% else %}
<input type="radio" id="sudoku-field-{{ row }}{{ col }}", name="sudoku-field" value="{{ row }}{{ col }}" class="sudoku-field"/>
<label for="sudoku-field-{{ row }}{{ col }}" id="sudoku-field-{{ row }}{{ col }}-label" class="sudoku-field">
{{ sudoku_fields[row-1][col-1] | default('', true) }}
</label>
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
</div>
<div id="sudoku-input">
<table>
{% for row in range(0, 3) %}
<tr>
{% for col in range(1, 4) %}
<td>
<input type="submit" value="{{ row * 3 + col }}" name="number" />
</td>
{% endfor %}
</tr>
{% endfor %}
<tr>
<td colspan="3">
<input type="submit" value="Delete" name="number" class="fill" />
</td>
</tr>
</table>
</div>
</form>
</main>
</body>
</html>

2
webgames/__init__.py Normal file
View file

@ -0,0 +1,2 @@
__version__ = '0.1'

165
webgames/__main__.py Normal file
View file

@ -0,0 +1,165 @@
import sys
import json
from uuid import UUID
from datetime import datetime
import jinja2
from bottle import Bottle, abort, request, redirect, static_file
from namesgenerator import get_random_name
from webgames import api
from webgames.api import apiv1, GAMES, PLAYERS
from webgames.human import HumanID
from webgames.game import GameState
app = Bottle()
app.jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader('templates'))
@app.get('/')
def get__root():
scheme, host, *_ = request.urlparts
tmpl = app.jinja_env.get_template('cwa/welcome.html.j2')
example_name = get_random_name(' ').title()
return tmpl.render(placeholder=example_name, host=f'{scheme}://{host}', baseurl=f'{urlbase}/')
@app.get('/favicon.ico')
def get_favicon():
redirect('/static/favicon.ico')
@app.post('/')
def post__root():
scheme, host, *_ = request.urlparts
name = request.forms.get('name')
placeholder = request.forms.get('placeholder')
if not name:
name = placeholder
uuid = api.create_player(name)
redirect(f'{urlbase}/{str(uuid)}')
@app.get('/<player>')
@app.get('/<player>/')
def get_player(player: str):
scheme, host, *_ = request.urlparts
if len(player) == 0:
return get__root()
elif len(player) == 4:
return get__blank_game(player)
player_id = UUID(player)
player = api.get_player(player_id)
tmpl = app.jinja_env.get_template('cwa/welcome1.html.j2')
return tmpl.render(player_name=player.name,
player_uuid=str(player.uuid), host=f'{scheme}://{host}', baseurl=f'{urlbase}/')
@app.post('/<player_id>')
@app.post('/<player_id>/')
def post_player(player_id: str):
scheme, host, *_ = request.urlparts
if len(player_id) == 4:
return post__blank_game(player_id)
playerid = UUID(player_id)
puzzle = request.forms.get('puzzle')
humanid = request.forms.get('game')
try:
game_id = HumanID.resolve(humanid)
except:
game_id = api.create_game(puzzle)
game = api.get_game(game_id)
player = api.get_player(playerid)
game.join(player)
redirect(f'{urlbase}/{player_id}/{str(game.uuid)}/lobby')
def get__blank_game(game_id: str):
scheme, host, *_ = request.urlparts
try:
gameid = HumanID.resolve(game_id)
except:
redirect(f'{urlbase}')
game = api.get_game(gameid)
tmpl = app.jinja_env.get_template('cwa/welcome3.html.j2')
example_name = get_random_name(' ').title()
return tmpl.render(game=game,placeholder=example_name, host=f'{scheme}://{host}', baseurl=f'{urlbase}/')
def post__blank_game(game_id: str):
scheme, host, *_ = request.urlparts
try:
gameid = HumanID.resolve(game_id)
except:
redirect(f'{urlbase}/')
game = api.get_game(gameid)
name = request.forms.get('name')
placeholder = request.forms.get('placeholder')
if not name:
name = placeholder
uuid = api.create_player(name)
player = api.get_player(uuid)
game.join(player)
redirect(f'{urlbase}/{uuid}/{game.uuid}/lobby')
@app.get('/<player_id>/<game_id>/lobby')
@app.get('/<player_id>/<game_id>/lobby/')
def get_player_game_lobby(player_id: str, game_id: str):
scheme, host, *_ = request.urlparts
playerid = UUID(player_id)
gameid = UUID(game_id)
player = api.get_player(playerid)
game = api.get_game(gameid)
if game.state == GameState.RUNNING:
redirect(f'{urlbase}/{player_id}/{game_id}/play')
elif game.state == GameState.FINISHED:
redirect(f'{urlbase}/{player_id}/{game_id}/done')
tmpl = app.jinja_env.get_template('cwa/welcome2.html.j2')
return tmpl.render(player=player, game=game, host=f'{scheme}://{host}', baseurl=f'{urlbase}/')
@app.get('/<player_id>/<game_id>/play')
@app.get('/<player_id>/<game_id>/play/')
def get_player_game_play(player_id: str, game_id: str):
playerid = UUID(player_id)
gameid = UUID(game_id)
player = api.get_player(playerid)
game = api.get_game(gameid)
if game.state == GameState.LOBBY:
game.begin({}, baseurl=f'{urlbase}/')
duration = str(datetime.utcnow() - game.start)
return game.render(app.jinja_env, player, duration)
@app.post('/<player_id>/<game_id>/play')
@app.post('/<player_id>/<game_id>/play/')
def post_player_game_play(player_id: str, game_id: str):
playerid = UUID(player_id)
gameid = UUID(game_id)
player = api.get_player(playerid)
game = api.get_game(gameid)
if game.state == GameState.LOBBY:
game.begin(options=request.forms, urlbase=f'{urlbase}/')
else:
game.process_action(playerid, request.forms)
duration = str(datetime.utcnow() - game.start)
return game.render(app.jinja_env, player, duration)
#@app.error(404)
#def error_404(error):
# redirect('/', 303)
@app.get('/static/<filename:path>')
def static(filename):
return static_file(filename, root='static')
host = sys.argv[1] if len(sys.argv) >= 3 else '127.0.0.1'
port = sys.argv[-1] if len(sys.argv) > 1 else 8080
urlbase = sys.argv[2] if len(sys.argv) >= 4 else ''
app.mount('/api/v1', apiv1)
app.run(host=host, port=port)

184
webgames/api.py Normal file
View file

@ -0,0 +1,184 @@
from typing import Optional
import json
from uuid import UUID
from bottle import Bottle, abort, request
from webgames.game import Game, GameState
from webgames.player import Player
from webgames.human import HumanID
GAMES = {}
PLAYERS = {}
apiv1 = Bottle()
@apiv1.get('/humanid/<humanid>')
def get_humanid_humanid(humanid: str) -> UUID:
try:
longid: Optional[UUID] = HumanID.resolve(humanid)
except KeyError:
longid = None
return json.dumps({
'uuid': str(longid),
'human_id': str(humanid)
})
def create_player(name: str) -> UUID:
player = Player(name)
PLAYERS[player.uuid] = player
return player.uuid
@apiv1.post('/player')
def post_player():
name = request.json.get('name', None)
create_player(name)
return get_player_uuid(str(player.uuid))
def get_player(uuid: UUID):
try:
return PLAYERS[uuid]
except KeyError:
abort(404, 'Player not found')
@apiv1.get('/player/<uuid>')
def get_player_uuid(uuid: str):
longid = UUID(uuid)
try:
player = PLAYERS[longid]
except KeyError:
abort(404, 'Player not found')
return json.dumps({
'uuid': str(player.uuid),
'name': str(player.name)
})
def create_game(puzzle: str, puzzle_args = None) -> UUID:
game = Game(puzzle, puzzle_args)
GAMES[game.uuid] = game
return game.uuid
@apiv1.post('/game')
def post_game():
puzzle = request.json.get('difficulty', 'sudoku')
puzzle_args = request.json.get('puzzle_args', None)
game = Game(puzzle, puzzle_args)
GAMES[game.uuid] = game
return get_game_uuid(str(game.uuid))
def get_game(uuid: UUID):
try:
return GAMES[uuid]
except KeyError:
abort(404, 'Game not found')
@apiv1.get('/game/<uuid>')
def get_game_uuid(uuid: str):
longid = UUID(uuid)
try:
game = GAMES[longid]
except KeyError:
abort(404, 'Game not found')
players = [
{
'uuid': str(pid),
'name': player.name
}
for pid, player in game.players.items()
]
puzzles = [
{
'uuid': str(pid),
'progress': puzzle.progress
}
for pid, puzzle in game.puzzles.items()
]
scores = [
{
'player': str(item['player']),
'duration': str(item['duration'])
}
for item in game.scoreboard
]
return json.dumps({
'uuid': str(game.uuid),
'human_id': str(game.human_id),
'state': str(game.state),
'players': players,
'puzzles': puzzles,
'scores': scores,
'start': game.start.timestamp() if game.start else None,
'end': game.end.timestamp() if game.end else None
})
@apiv1.post('/game/<uuid>/join')
def post_game_uuid_join(uuid: str):
pid: str = request.json.get('uuid')
game_id = UUID(uuid)
player_id = UUID(pid)
try:
game = GAMES[game_id]
except KeyError:
abort(404, 'Game not found')
try:
player = PLAYERS[player_id]
except KeyError:
abort(404, 'Player not found')
game.join(player)
@apiv1.post('/game/<uuid>/start')
def post_game_uuid_start(uuid: str):
game_id = UUID(uuid)
try:
game = GAMES[game_id]
except KeyError:
abort(404, 'Game not found')
game.begin(options=request.json)
@apiv1.post('/game/<uuid>/conclude')
def post_game_uuid_conclude(uuid: str):
game_id = UUID(uuid)
try:
game = GAMES[game_id]
except KeyError:
abort(404, 'Game not found')
game.conclude()
@apiv1.get('/game/<gid>/puzzle/<pid>')
def post_game_uuid_action(gid: str, pid: str):
game_id = UUID(gid)
try:
game = GAMES[game_id]
except KeyError:
abort(404, 'Game not found')
player_id = UUID(pid)
return json.dumps(game.serialize_puzzle(player_id))
@apiv1.post('/game/<gid>/puzzle/<pid>/action')
def post_game_gid_puzzle_pid_action(gid: str, pid: str):
game_id = UUID(gid)
try:
game = GAMES[game_id]
except KeyError:
abort(404, 'Game not found')
player_id = UUID(pid)
action = request.json.get('action')
game.process_action(player_id, action)

910
webgames/carcassonne.py Normal file
View file

@ -0,0 +1,910 @@
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):
RED = _Color('red', '#ff0000', '\033[31m')
GREEN = _Color('green', '#00aa00', '\033[32m')
BLUE = _Color('blue', '#0000ff', '\033[34m')
YELLOW = _Color('yellow', '#ff00ff', '\093[33m')
BLACK = _Color('black', '#000000', '\033[30m')
GREY = _Color('grey', '#888888', '\033[90m')
class Phase(enum.Enum):
PLACE = 1
CLAIM = 2
class ResourceBoundary(enum.Enum):
LEFT = 1
TOP = 2
RIGHT = 3
BOTTOM = 4
LEFT_TOP = 5
TOP_LEFT = 6
TOP_RIGHT = 7
RIGHT_TOP = 8
RIGHT_BOTTOM = 9
BOTTOM_RIGHT = 10
BOTTOM_LEFT = 11
LEFT_BOTTOM = 12
class Card:
def __init__(self, monastery=None, shrine=None, city_defense=False, template='',
road_top=None, road_bottom=None, road_left=None, road_right=None,
city_top=None, city_bottom=None, city_left=None, city_right=None,
pasture_lt=None, pasture_rt=None, pasture_rb=None, pasture_lb=None,
pasture_tl=None, pasture_tr=None, pasture_br=None, pasture_bl=None,
inn=None, cathedral=None):
self.resources = {}
self.template = template
self.rotation = 0
self.city_defense = city_defense
if monastery:
self.resources[monastery] = Monastery(self)
self.monastery = self.resources[monastery]
elif shrine:
self.resources[shrine] = Shrine(self)
self.monastery = self.resources[shrine]
else:
self.monastery = None
_roads = {i: Road(self, inn=inn==i) for i in [road_left, road_top, road_right, road_bottom] if i is not None}
_cities = {i: City(self, cathedral=cathedral==i) for i in [city_left, city_top, city_right, city_bottom] if i is not None}
_pastures = {i: Pasture(self) for i in [pasture_tl, pasture_tr, pasture_br, pasture_bl, pasture_lt, pasture_rt, pasture_rb, pasture_lb] if i is not None}
self.resources.update(_roads)
self.resources.update(_cities)
self.resources.update(_pastures)
self.placed = False
self.x = None
self.y = None
self.road_top, self.road_left, self.road_bottom, self.road_right = \
_roads.get(road_top), _roads.get(road_left), _roads.get(road_bottom), _roads.get(road_right)
self.city_top, self.city_left, self.city_bottom, self.city_right = \
_cities.get(city_top), _cities.get(city_left), _cities.get(city_bottom), _cities.get(city_right)
self.pasture_tl, self.pasture_tr, self.pasture_br, self.pasture_bl, self.pasture_lt, self.pasture_rt, self.pasture_rb, self.pasture_lb = \
_pastures.get(pasture_tl), _pastures.get(pasture_tr), _pastures.get(pasture_br), _pastures.get(pasture_bl), _pastures.get(pasture_lt), _pastures.get(pasture_rt), _pastures.get(pasture_rb), _pastures.get(pasture_lb)
def can_place(self, field, x, y):
left = field.get((x-1, y))
top = field.get((x, y-1))
right = field.get((x+1, y))
bottom = field.get((x, y+1))
if left and ((left.pasture_rt is None) != (self.pasture_lt is None) \
or (left.pasture_rb is None) != (self.pasture_lb is None) \
or (left.road_right is None) != (self.road_left is None) \
or (left.city_right is None) != (self.city_left is None)):
raise ValueError('Card does not match the one to the left')
if top and ((top.pasture_bl is None) != (self.pasture_tl is None) \
or (top.pasture_br is None) != (self.pasture_tr is None) \
or (top.road_bottom is None) != (self.road_top is None) \
or (top.city_bottom is None) != (self.city_top is None)):
raise ValueError('Card does not match the one to the top')
if right and ((right.pasture_lt is None) != (self.pasture_rt is None) \
or (right.pasture_lb is None) != (self.pasture_rb is None) \
or (right.road_left is None) != (self.road_right is None) \
or (right.city_left is None) != (self.city_right is None)):
raise ValueError('Card does not match the one to the right')
if bottom and ((bottom.pasture_tl is None) != (self.pasture_bl is None) \
or (bottom.pasture_tr is None) != (self.pasture_br is None) \
or (bottom.road_top is None) != (self.road_bottom is None) \
or (bottom.city_top is None) != (self.city_bottom is None)):
raise ValueError('Card does not match the one to the bottom')
def rotate_cw(self):
if self.placed:
raise RuntimeError('Cant rotate an already placed time')
self.rotation = (self.rotation + 90) % 360
self.road_top, self.road_left, self.road_bottom, self.road_right = self.road_left, self.road_bottom, self.road_right, self.road_top
self.city_top, self.city_left, self.city_bottom, self.city_right = self.city_left, self.city_bottom, self.city_right, self.city_top
self.pasture_tl, self.pasture_tr, self.pasture_rt, self.pasture_rb, self.pasture_br, self.pasture_bl, self.pasture_lb, self.pasture_lt = self.pasture_lb, self.pasture_lt, self.pasture_tl, self.pasture_tr, self.pasture_rt, self.pasture_rb, self.pasture_br, self.pasture_bl
def rotate_ccw(self):
if self.placed:
raise RuntimeError('Cant rotate an already placed time')
self.rotation = (self.rotation + 270) % 360
self.road_left, self.road_bottom, self.road_right, self.road_top = self.road_top, self.road_left, self.road_bottom, self.road_right
self.city_left, self.city_bottom, self.city_right, self.city_top = self.city_top, self.city_left, self.city_bottom, self.city_right
self.pasture_lb, self.pasture_lt, self.pasture_tl, self.pasture_tr, self.pasture_rt, self.pasture_rb, self.pasture_br, self.pasture_bl = self.pasture_tl, self.pasture_tr, self.pasture_rt, self.pasture_rb, self.pasture_br, self.pasture_bl, self.pasture_lb, self.pasture_lt
def svg(self, env, standalone=True, x=0, y=0, claimable={}, highlight=False):
tmpl = env.get_template(f'carcassonne/{self.template}.svg')
rendered = tmpl.render(resources=self.resources, rotation=self.rotation, x=x*100, y=y*100, claimable=claimable)
if highlight:
rendered += f'<path transform="translate({x*100} {y*100})" class="highlight" d="M 0 0 H 100 V 100 H 0 Z" />'
if standalone:
return '<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">\n' + rendered + '\n</svg>'
return rendered
@staticmethod
def generate_deck(extensions=[], field=None):
# https://upload.wikimedia.org/wikipedia/commons/f/f8/Carcassonne_tiles.svg
deck = \
[Card(monastery=1, pasture_tl=2, pasture_tr=2, pasture_br=2, pasture_bl=2, pasture_lt=2, pasture_rt=2, pasture_rb=2, pasture_lb=2, template='001') for _ in range(4)] + \
[Card(monastery=1, road_bottom=2, pasture_tl=3, pasture_tr=3, pasture_br=3, pasture_bl=3, pasture_lt=3, pasture_rt=3, pasture_rb=3, pasture_lb=3, template='011') for _ in range(2)] + \
[Card(city_top=2, pasture_br=1, pasture_bl=1, pasture_lt=1, pasture_rt=1, pasture_rb=1, pasture_lb=1, template='101') for _ in range(5)] + \
[Card(road_left=1, road_right=1, pasture_tl=2, pasture_tr=2, pasture_br=3, pasture_bl=3, pasture_lt=2, pasture_rt=2, pasture_rb=3, pasture_lb=3, template='021') for _ in range(8)] + \
[Card(road_bottom=1, road_left=1, pasture_tl=2, pasture_tr=2, pasture_br=2, pasture_bl=3, pasture_lt=2, pasture_rt=2, pasture_rb=2, pasture_lb=3, template='022') for _ in range(9)] + \
[Card(road_bottom=4, road_left=5, road_right=6, pasture_tl=1, pasture_tr=1, pasture_br=2, pasture_bl=3, pasture_lt=1, pasture_rt=1, pasture_rb=2, pasture_lb=3, template='031') for _ in range(4)] + \
[Card(road_bottom=5, road_left=6, road_right=7, road_top=8, pasture_tl=1, pasture_tr=2, pasture_br=3, pasture_bl=4, pasture_lt=1, pasture_rt=2, pasture_rb=3, pasture_lb=4, template='041') for _ in range(1)] + \
[Card(city_top=2, pasture_br=1, pasture_bl=1, pasture_lt=1, pasture_rt=1, pasture_rb=1, pasture_lb=1, template='101') for _ in range(5)] + \
[Card(road_left=3, road_right=3, city_top=4, pasture_br=2, pasture_bl=2, pasture_lt=1, pasture_rt=1, pasture_rb=2, pasture_lb=2, template='121') for _ in range(3)] + \
[Card(road_left=3, road_bottom=3, city_top=4, pasture_br=1, pasture_bl=2, pasture_lt=1, pasture_rt=1, pasture_rb=1, pasture_lb=2, template='122') for _ in range(3)] + \
[Card(road_right=3, road_bottom=3, city_top=4, pasture_br=2, pasture_bl=1, pasture_lt=1, pasture_rt=1, pasture_rb=2, pasture_lb=1, template='123') for _ in range(3)] + \
[Card(road_bottom=4, road_left=5, road_right=6, city_top=7, pasture_br=2, pasture_bl=3, pasture_lt=1, pasture_rt=1, pasture_rb=2, pasture_lb=3, template='131') for _ in range(3)] + \
[Card(city_left=3, city_right=3, pasture_tl=1, pasture_tr=1, pasture_br=2, pasture_bl=2, template='201') for _ in range(1)] + \
[Card(city_defense=True, city_left=3, city_right=3, pasture_tl=1, pasture_tr=1, pasture_br=2, pasture_bl=2, template='202') for _ in range(2)] + \
[Card(city_top=2, city_right=2, pasture_bl=1, pasture_br=1, pasture_lt=1, pasture_lb=1, template='203') for _ in range(3)] + \
[Card(city_defense=True, city_top=2, city_right=2, pasture_bl=1, pasture_br=1, pasture_lt=1, pasture_lb=1, template='204') for _ in range(2)] + \
[Card(city_top=2, city_bottom=3, pasture_lt=1, pasture_rt=1, pasture_lb=1, pasture_rb=1, template='205') for _ in range(3)] + \
[Card(city_top=2, city_right=3, pasture_bl=1, pasture_br=1, pasture_lb=1, pasture_lt=1, template='206') for _ in range(2)] + \
[Card(road_left=3, road_bottom=3, city_top=4, city_right=4, pasture_lt=1, pasture_lb=2, pasture_bl=2, pasture_br=1, template='221') for _ in range(2)] + \
[Card(road_left=3, road_bottom=3, city_defense=True, city_top=4, city_right=4, pasture_lt=1, pasture_lb=2, pasture_bl=2, pasture_br=1, template='222') for _ in range(2)] + \
[Card(city_left=1, city_top=1, city_right=1, pasture_bl=2, pasture_br=2, template='301') for _ in range(3)] + \
[Card(city_defense=True, city_left=1, city_top=1, city_right=1, pasture_bl=2, pasture_br=2, template='302') for _ in range(1)] + \
[Card(road_bottom=4, city_left=3, city_top=3, city_right=3, pasture_bl=1, pasture_br=2, template='311') for _ in range(1)] + \
[Card(road_bottom=4, city_defense=True, city_left=3, city_top=3, city_right=3, pasture_bl=1, pasture_br=2, template='312') for _ in range(2)] + \
[Card(city_defense=True, city_left=1, city_top=1, city_right=1, city_bottom=1, template='401') for _ in range(1)]
if 'heretics' in extensions:
# https://wikicarpedia.com/index.php/File:Sheet_Cult.png
deck += [
Card(shrine=1, road_top=2, road_bottom=3, pasture_tl=4, pasture_lt=4, pasture_lb=4, pasture_bl=4, pasture_tr=5, pasture_rt=5, pasture_rb=5, pasture_br=5, template='he-021'),
Card(shrine=1, pasture_tl=2, pasture_lt=2, pasture_lb=2, pasture_bl=2, pasture_tr=2, pasture_rt=2, pasture_rb=2, pasture_br=2, template='he-001'),
Card(shrine=1, city_top=3, road_bottom=4, pasture_lt=2, pasture_lb=2, pasture_bl=2, pasture_rt=2, pasture_rb=2, pasture_br=2, template='he-111'),
Card(shrine=1, road_bottom=3, pasture_tl=2, pasture_lt=2, pasture_lb=2, pasture_bl=2, pasture_tr=2, pasture_rt=2, pasture_rb=2, pasture_br=2, template='he-011'),
Card(shrine=1, city_top=3, pasture_lt=2, pasture_lb=2, pasture_bl=2, pasture_rt=2, pasture_rb=2, pasture_br=2, template='he-101')
]
if 'inns-cathedrals' in extensions:
# https://wikicarpedia.com/index.php/Inns_and_Cathedrals
center_pasture = Card(city_top=1, city_right=2, city_bottom=3, city_left=4, pasture_tl=5, template='ic-401-pasture')
center_pasture.pasture_tl = None
deck += [
Card(road_bottom=1, road_left=1, pasture_tl=2, pasture_tr=2, pasture_br=2, pasture_bl=3, pasture_lt=2, pasture_rt=2, pasture_rb=2, pasture_lb=3, template='ic-022-inn', inn=1),
Card(road_left=1, road_right=1, pasture_tl=2, pasture_tr=2, pasture_br=3, pasture_bl=3, pasture_lt=2, pasture_rt=2, pasture_rb=3, pasture_lb=3, template='ic-021-inn', inn=1),
Card(road_bottom=4, road_left=5, road_right=6, pasture_tl=1, pasture_tr=1, pasture_br=2, pasture_bl=3, pasture_lt=1, pasture_rt=1, pasture_rb=2, pasture_lb=3, template='ic-031-inn', inn=6),
Card(monastery=4, road_left=1, road_right=5, pasture_tl=2, pasture_tr=2, pasture_br=3, pasture_bl=3, pasture_lt=2, pasture_rt=2, pasture_rb=3, pasture_lb=3, template='ic-021-monastery'),
Card(road_bottom=1, road_right=1, road_top=2, road_left=2, pasture_tl=3, pasture_tr=4, pasture_br=5, pasture_bl=4, pasture_lt=3, pasture_rt=4, pasture_rb=5, pasture_lb=4, template='ic-041'),
Card(road_bottom=3, city_top=2, city_right=2, pasture_bl=1, pasture_br=4, pasture_lt=1, pasture_lb=1, template='ic-203-road'),
Card(city_top=1, pasture_br=3, pasture_bl=3, pasture_lt=3, pasture_rt=2, pasture_rb=2, pasture_lb=3, template='ic-101-branch'),
center_pasture,
Card(road_left=5, road_right=6, city_top=7, city_bottom=8, pasture_lt=1, pasture_rt=2, pasture_rb=4, pasture_lb=3, template='ic-221-crossing'),
Card(road_bottom=3, city_top=4, pasture_br=1, pasture_bl=2, pasture_lt=2, pasture_rt=1, pasture_rb=1, pasture_lb=2, template='ic-101-road'),
Card(city_left=1, city_top=1, city_right=1, city_bottom=1, template='ic-401-cathedral', cathedral=1),
Card(city_left=1, city_top=1, city_right=1, city_bottom=1, template='ic-401-cathedral', cathedral=1),
Card(road_left=3, road_bottom=3, city_defense=True, city_top=4, city_right=4, pasture_lt=1, pasture_lb=2, pasture_bl=2, pasture_br=1, template='ic-222-inn', inn=3),
Card(road_left=3, road_bottom=3, city_top=4, pasture_br=1, pasture_bl=2, pasture_lt=1, pasture_rt=1, pasture_rb=1, pasture_lb=2, template='ic-122-inn', inn=3),
Card(road_bottom=3, city_top=2, city_left=2, pasture_br=1, pasture_bl=4, pasture_rt=1, pasture_rb=1, template='ic-203-road-inn', inn=3),
Card(city_top=3, city_right=4, city_left=2, pasture_bl=1, pasture_br=1, template='ic-301-split'),
Card(city_defense=True, city_right=2, city_left=3, city_bottom=3, pasture_tl=1, pasture_tr=1, template='ic-302'),
Card(city_defense=True, city_left=5, city_right=5, road_top=6, road_bottom=7, pasture_tl=1, pasture_tr=2, pasture_bl=3, pasture_br=4, template='ic-202-roads')
]
random.shuffle(deck)
if 'river' in extensions:
# https://wikicarpedia.com/index.php/File:River_I_And_River_II_C2_Examples.png
inn = 5 if 'inns-cathedrals' in extensions else None
riverdeck = [
RiverCard(river_bottom=True, pasture_bl=1, pasture_lb=1, pasture_lt=1, pasture_tl=1, pasture_tr=1, pasture_rt=1, pasture_rb=1, pasture_br=1, template='r2-source'),
RiverCard(river_bottom=True, river_left=True, pasture_bl=2, pasture_lb=2, pasture_lt=1, pasture_tl=1, pasture_tr=1, pasture_rt=1, pasture_rb=1, pasture_br=1, template='r2-curve'),
RiverCard(river_bottom=True, river_right=True, city_defense=True, city_left=3, city_top=3, pasture_bl=1, pasture_rt=1, pasture_rb=2, pasture_br=2, template='r2-city'),
RiverCard(river_top=True, river_bottom=True, road_left=5, road_right=5, pasture_bl=1, pasture_lb=1, pasture_lt=2, pasture_tl=2, pasture_tr=3, pasture_rt=3, pasture_rb=4, pasture_br=4, inn=inn, template='r2-road'),
RiverCard(river_top=True, river_bottom=True, city_left=3, city_right=3, pasture_bl=1, pasture_tl=1, pasture_tr=2, pasture_br=2, template='r2-citybridge'),
RiverCard(river_bottom=True, river_left=True, pasture_bl=2, pasture_lb=2, pasture_lt=1, pasture_tl=1, pasture_tr=1, pasture_rt=1, pasture_rb=1, pasture_br=1, template='r2-curve'),
RiverCard(river_bottom=True, river_left=True, road_top=4, road_right=4, pasture_bl=2, pasture_lb=2, pasture_lt=1, pasture_tl=1, pasture_tr=3, pasture_rt=3, pasture_rb=1, pasture_br=1, template='r2-road2'),
RiverCard(river_bottom=True, river_top=True, road_left=5, city_right=6, pasture_bl=1, pasture_lb=1, pasture_lt=2, pasture_tl=2, pasture_tr=3, pasture_br=4, template='r2-roadcity'),
RiverCard(river_top=True, pasture_bl=1, pasture_lb=1, pasture_lt=1, pasture_tl=1, pasture_tr=1, pasture_rt=1, pasture_rb=1, pasture_br=1, template='r2-lake'),
RiverCard(river_left=True, river_right=True, monastery=3, pasture_bl=1, pasture_lb=1, pasture_lt=2, pasture_tl=2, pasture_tr=2, pasture_rt=2, pasture_rb=1, pasture_br=1, template='r2-monastery'),
]
random.shuffle(riverdeck)
riverdeck.insert(0, RiverCard(river_bottom=True, city_top=3, pasture_bl=1, pasture_lb=1, pasture_lt=1, pasture_rt=2, pasture_rb=2, pasture_br=2, template='r2-lake2'))
deck += riverdeck
if field:
field._resources = {}
field._field = {}
field.place_card(0, 0, RiverCard(river_bottom=True, river_top=True, river_right=True, pasture_bl=1, pasture_lb=1, pasture_lt=1, pasture_tl=1, pasture_tr=2, pasture_rt=2, pasture_rb=3, pasture_br=3, template='r2-fork'))
return deck
class RiverCard(Card):
def __init__(self, river_left=False, river_top=False, river_right=False, river_bottom=False, **kwargs):
self.river_left = river_left
self.river_top = river_top
self.river_right = river_right
self.river_bottom = river_bottom
super().__init__(**kwargs)
def can_place(self, field, x, y):
super().can_place(field, x, y)
left = field.get((x-1, y))
top = field.get((x, y-1))
right = field.get((x+1, y))
bottom = field.get((x, y+1))
adj = [t for t in [left, top, right, bottom] if t is not None]
if len(adj) != 1:
raise ValueError('Cannot place river tile here')
if left and (not left.river_right or not self.river_left):
raise ValueError('Card does not match the one to the left')
if top and (not top.river_bottom or not self.river_top):
raise ValueError('Card does not match the one to the top')
if right and (not right.river_left or not self.river_right):
raise ValueError('Card does not match the one to the right')
if bottom and (not bottom.river_top or not self.river_bottom):
raise ValueError('Card does not match the one to the bottom')
# Check that there are no immediate 180deg turn
if self.river_left and not left:
if (top and top.river_left) or (bottom and bottom.river_left):
raise ValueError('Card placement introduces a 180 degree angle')
elif self.river_top and not top:
if (left and left.river_top) or (right and right.river_top):
raise ValueError('Card placement introduces a 180 degree angle')
elif self.river_right and not right:
if (top and top.river_right) or (bottom and bottom.river_right):
raise ValueError('Card placement introduces a 180 degree angle')
elif self.river_bottom and not bottom:
if (left and left.river_bottom) or (right and right.river_bottom):
raise ValueError('Card placement introduces a 180 degree angle')
def rotate_cw(self):
super().rotate_cw()
self.river_top, self.river_left, self.river_bottom, self.river_right = self.river_left, self.river_bottom, self.river_right, self.river_top
def rotate_ccw(self):
super().rotate_ccw()
self.river_left, self.river_bottom, self.river_right, self.river_top = self.river_top, self.river_left, self.river_bottom, self.river_right
class Resource:
def __init__(self, card):
self.owners = set()
self.cards = set([card])
self.joined = self
self.completed = False
self.completed_before_end = False
self.boundaries = set()
self._uuid = uuid4()
def root(self):
a = self
while a != a.joined:
a = a.joined
self.joined = a
return a
def uuid(self):
return self.root()._uuid
def join(self, other):
if type(self) != type(other.joined):
raise TypeError(f'Cannot join {type(self)} with {type(other.joined)}')
a = self.root()
b = other.root()
if a.completed or b.completed:
raise RuntimeError(f'Cannot join completed resources')
a.owners.update(b.owners)
a.cards.update(b.cards)
a.boundaries.update(b.boundaries)
b.joined = a
return a
def claim(self, player, card):
a = self.root()
if len(a.owners) > 0:
raise RuntimeError('Cannot claim already-claimed resource')
if len([1 for f in player.followers if f.resource is None]) == 0:
raise RuntimeError('Player has no followers left')
for f in player.followers:
if f.resource is None:
a.owners.add(f)
f.claim(self, card)
break
def unclaim(self):
r = self.root()
for follower in r.owners:
follower.resource = None
r.owners = []
def complete(self, game_end, players):
r = self.root()
if r.completed:
return
r.completed = True
r.completed_before_end = not game_end
owners = {}
for follower in r.owners:
owners.setdefault(follower.player.uuid, 0)
owners[follower.player.uuid] += 1
if not game_end:
r.unclaim()
nmax = max(owners.values(), default=0)
scoring_players = [uuid for uuid, n in owners.items() if n == nmax]
for p in scoring_players:
players[p].score += r.get_score(game_end)
def get_score(self, game_end):
return 0
class Monastery(Resource):
def __init__(self, card):
super().__init__(card)
self._score = 0
self._competitors = set()
def join(self, other):
raise TypeError('Cannot join monasteries')
def get_score(self, game_end):
for c in self._competitors:
c.unclaim()
return self._score
def add_competitor(self, other):
if isinstance(other, Shrine):
self._competitors.add(other)
other._competitors.add(self)
class Shrine(Monastery):
def __init__(self, card):
super().__init__(card)
def join(self, other):
raise TypeError('Cannot join shrines')
def add_competitor(self, other):
if isinstance(other, Monastery) and not isinstance(other, Shrine):
self._competitors.add(other)
other._competitors.add(self)
class Road(Resource):
def __init__(self, card, inn=False):
super().__init__(card)
self._inn = inn
def get_score(self, game_end):
r = self.root()
if r._inn:
return (2 * len(r.cards)) if not game_end else 0
else:
return len(r.cards)
def join(self, other):
a = self.root()
b = other.root()
joined = super().join(other)
joined._inn = a._inn or b._inn
return joined
class City(Resource):
def __init__(self, card, cathedral=False):
super().__init__(card)
self._cathedral = cathedral
def get_score(self, game_end):
r = self.root()
score = sum([2 if c.city_defense else 1 for c in r.cards])
if r._cathedral:
return (score * 3) if not game_end else 0
else:
return (score * 2) if not game_end else score
def join(self, other):
a = self.root()
b = other.root()
joined = super().join(other)
joined._cathedral = a._cathedral or b._cathedral
return joined
class Pasture(Resource):
def __init__(self, card):
super().__init__(card)
def complete(self, game_end, players):
if not game_end:
return
return super().complete(game_end, players)
def get_score(self, game_end):
if not game_end:
return 0
r = self.root()
cities = set()
for card in r.cards:
if card.pasture_tl and card.pasture_tl.root() is self and card.city_left and card.city_left.completed_before_end:
cities.add(card.city_left.root().uuid())
if card.pasture_tr and card.pasture_tr.root() is self and card.city_right and card.city_right.completed_before_end:
cities.add(card.city_right.root().uuid())
if card.pasture_rt and card.pasture_rt.root() is self and card.city_top and card.city_top.completed_before_end:
cities.add(card.city_top.root().uuid())
if card.pasture_rb and card.pasture_rb.root() is self and card.city_bottom and card.city_bottom.completed_before_end:
cities.add(card.city_bottom.root().uuid())
if card.pasture_br and card.pasture_br.root() is self and card.city_right and card.city_right.completed_before_end:
cities.add(card.city_right.root().uuid())
if card.pasture_bl and card.pasture_bl.root() is self and card.city_left and card.city_left.completed_before_end:
cities.add(card.city_left.root().uuid())
if card.pasture_lb and card.pasture_lb.root() is self and card.city_bottom and card.city_bottom.completed_before_end:
cities.add(card.city_bottom.root().uuid())
if card.pasture_lt and card.pasture_lt.root() is self and card.city_top and card.city_top.completed_before_end:
cities.add(card.city_top.root().uuid())
return 3 * len(cities)
def p(r, symbol, other, players):
if not r:
return other
owners = {}
for follower in r.root().owners:
owners.setdefault(follower.player.uuid, 0)
owners[follower.player.uuid] += 1
max_p, _ = max(owners.items(), key=lambda x: x[1], default=(None, 0))
if max_p:
return players[max_p].color.value.ansi + symbol + '\033[0m'
return symbol
class CarcassonneField:
def __init__(self):
self._min_x = 0
self._max_x = 0
self._min_y = 0
self._max_y = 0
self._field = {}
self._resources = {}
self.place_card(0, 0, Card(road_left=3, road_right=3, city_top=4, pasture_br=2, pasture_bl=2, pasture_lt=1, pasture_rt=1, pasture_rb=2, pasture_lb=2, template='121'))
def can_place_card(self, x, y, card):
if self._field.get((x, y)):
raise KeyError(f'There already is a card at ({x}, {y})')
left = self._field.get((x-1, y))
top = self._field.get((x, y-1))
right = self._field.get((x+1, y))
bottom = self._field.get((x, y+1))
# Don't enforce placement rules for the first tile
if x != 0 or y != 0:
if not left and not top and not right and not bottom:
raise KeyError(f'There is no card around ({x}, {y})')
card.can_place(self._field, x, y)
def place_card(self, x, y, card):
self.can_place_card(x, y, card)
left = self._field.get((x-1, y))
top = self._field.get((x, y-1))
right = self._field.get((x+1, y))
bottom = self._field.get((x, y+1))
card.road_left.boundaries.add((x, y, ResourceBoundary.LEFT)) if card.road_left else None
card.road_top.boundaries.add((x, y, ResourceBoundary.TOP)) if card.road_top else None
card.road_right.boundaries.add((x, y, ResourceBoundary.RIGHT)) if card.road_right else None
card.road_bottom.boundaries.add((x, y, ResourceBoundary.BOTTOM)) if card.road_bottom else None
card.city_left.boundaries.add((x, y, ResourceBoundary.LEFT)) if card.city_left else None
card.city_top.boundaries.add((x, y, ResourceBoundary.TOP)) if card.city_top else None
card.city_right.boundaries.add((x, y, ResourceBoundary.RIGHT)) if card.city_right else None
card.city_bottom.boundaries.add((x, y, ResourceBoundary.BOTTOM)) if card.city_bottom else None
card.pasture_lt.boundaries.add((x, y, ResourceBoundary.LEFT_TOP)) if card.pasture_lt else None
card.pasture_tl.boundaries.add((x, y, ResourceBoundary.TOP_LEFT)) if card.pasture_tl else None
card.pasture_tr.boundaries.add((x, y, ResourceBoundary.TOP_RIGHT)) if card.pasture_tr else None
card.pasture_rt.boundaries.add((x, y, ResourceBoundary.RIGHT_TOP)) if card.pasture_rt else None
card.pasture_rb.boundaries.add((x, y, ResourceBoundary.RIGHT_BOTTOM)) if card.pasture_rb else None
card.pasture_br.boundaries.add((x, y, ResourceBoundary.BOTTOM_RIGHT)) if card.pasture_br else None
card.pasture_bl.boundaries.add((x, y, ResourceBoundary.BOTTOM_LEFT)) if card.pasture_bl else None
card.pasture_lb.boundaries.add((x, y, ResourceBoundary.LEFT_BOTTOM)) if card.pasture_lb else None
if card.road_left and left and left.road_right:
j = card.road_left.join(left.road_right)
j.boundaries.discard((x, y, ResourceBoundary.LEFT))
j.boundaries.discard((x-1, y, ResourceBoundary.RIGHT))
if card.road_top and top and top.road_bottom:
j = card.road_top.join(top.road_bottom)
j.boundaries.discard((x, y, ResourceBoundary.TOP))
j.boundaries.discard((x, y-1, ResourceBoundary.BOTTOM))
if card.road_right and right and right.road_left:
j = card.road_right.join(right.road_left)
j.boundaries.discard((x, y, ResourceBoundary.RIGHT))
j.boundaries.discard((x+1, y, ResourceBoundary.LEFT))
if card.road_bottom and bottom and bottom.road_top:
j = card.road_bottom.join(bottom.road_top)
j.boundaries.discard((x, y, ResourceBoundary.BOTTOM))
j.boundaries.discard((x, y+1, ResourceBoundary.TOP))
if card.city_left and left and left.city_right:
j = card.city_left.join(left.city_right)
j.boundaries.discard((x, y, ResourceBoundary.LEFT))
j.boundaries.discard((x-1, y, ResourceBoundary.RIGHT))
if card.city_top and top and top.city_bottom:
j = card.city_top.join(top.city_bottom)
j.boundaries.discard((x, y, ResourceBoundary.TOP))
j.boundaries.discard((x, y-1, ResourceBoundary.BOTTOM))
if card.city_right and right and right.city_left:
j = card.city_right.join(right.city_left)
j.boundaries.discard((x, y, ResourceBoundary.RIGHT))
j.boundaries.discard((x+1, y, ResourceBoundary.LEFT))
if card.city_bottom and bottom and bottom.city_top:
j = card.city_bottom.join(bottom.city_top)
j.boundaries.discard((x, y, ResourceBoundary.BOTTOM))
j.boundaries.discard((x, y+1, ResourceBoundary.TOP))
if card.pasture_lt and left and left.pasture_rt:
j = card.pasture_lt.join(left.pasture_rt)
j.boundaries.discard((x, y, ResourceBoundary.LEFT_TOP))
j.boundaries.discard((x-1, y, ResourceBoundary.RIGHT_TOP))
if card.pasture_tl and top and top.pasture_bl:
j = card.pasture_tl.join(top.pasture_bl)
j.boundaries.discard((x, y, ResourceBoundary.TOP_LEFT))
j.boundaries.discard((x, y-1, ResourceBoundary.BOTTOM_LEFT))
if card.pasture_rt and right and right.pasture_lt:
j = card.pasture_rt.join(right.pasture_lt)
j.boundaries.discard((x, y, ResourceBoundary.RIGHT_TOP))
j.boundaries.discard((x+1, y, ResourceBoundary.LEFT_TOP))
if card.pasture_tr and top and top.pasture_br:
j = card.pasture_tr.join(top.pasture_br)
j.boundaries.discard((x, y, ResourceBoundary.TOP_RIGHT))
j.boundaries.discard((x, y-1, ResourceBoundary.BOTTOM_RIGHT))
if card.pasture_rb and right and right.pasture_lb:
j = card.pasture_rb.join(right.pasture_lb)
j.boundaries.discard((x, y, ResourceBoundary.RIGHT_BOTTOM))
j.boundaries.discard((x+1, y, ResourceBoundary.LEFT_BOTTOM))
if card.pasture_br and bottom and bottom.pasture_tr:
j = card.pasture_br.join(bottom.pasture_tr)
j.boundaries.discard((x, y, ResourceBoundary.BOTTOM_RIGHT))
j.boundaries.discard((x, y+1, ResourceBoundary.TOP_RIGHT))
if card.pasture_lb and left and left.pasture_rb:
j = card.pasture_lb.join(left.pasture_rb)
j.boundaries.discard((x, y, ResourceBoundary.LEFT_BOTTOM))
j.boundaries.discard((x-1, y, ResourceBoundary.RIGHT_BOTTOM))
if card.pasture_bl and bottom and bottom.pasture_tl:
j = card.pasture_bl.join(bottom.pasture_tl)
j.boundaries.discard((x, y, ResourceBoundary.BOTTOM_LEFT))
j.boundaries.discard((x, y+1, ResourceBoundary.TOP_LEFT))
self._field[(x, y)] = card
card.placed = True
card.x = x
card.y = y
self._min_x = min(self._min_x, x)
self._max_x = max(self._max_x, x)
self._min_y = min(self._min_y, y)
self._max_y = max(self._max_y, y)
completed_resources = set()
for r in card.resources.values():
r = r.root()
self._resources[r.uuid] = r
if len(r.boundaries) == 0 and not isinstance(r, Monastery):
completed_resources.add(r)
# special handling for monasteries, as they can't be joined and require diagonally adjacent tiles
if card.monastery:
card.monastery._score = len([1 for i in range(x-1, x+2) for j in range(y-1, y+2) if (i, j) in self._field]) -1
for c in [self._field[(i, j)] for i in range(x-1, x+2) for j in range(y-1, y+2) if (i, j) in self._field]:
if c.monastery:
c.monastery._score += 1
if card.monastery:
c.monastery.add_competitor(card.monastery)
if c.monastery._score == 9:
completed_resources.add(c.monastery)
return completed_resources
def get_available_fields(self, card=None):
empty_fields = set()
for x, y in self._field.keys():
for ax, ay in [(x-1,y), (x,y-1), (x+1,y), (x,y+1)]:
if card:
try:
self.can_place_card(ax, ay, card)
except:
continue
empty_fields.add((ax, ay))
for c in self._field.keys():
empty_fields.discard(c)
return empty_fields
def svg(self, env, claimable, new_card, followers, phase, controls):
empty_fields = self.get_available_fields(new_card if phase == Phase.PLACE else None)
cards = []
empties = []
follows = []
for coord, card in self._field.items():
if controls and card is new_card and phase == Phase.CLAIM:
claim = claimable
else:
claim = {}
x, y = coord
gx = x - self._min_x + 1
gy = y - self._min_y + 1
cards.append(card.svg(env, standalone=False, x=gx, y=gy, claimable=claim, highlight=card is new_card))
if controls and phase == Phase.PLACE:
for x, y in empty_fields:
empties.append((x, y, (x-self._min_x+1)*100, (y-self._min_y+1)*100))
for follower in followers:
follows.append(follower.svg(env, -self._min_x+1, -self._min_y+1))
tmpl = env.get_template('carcassonne/field.svg')
return tmpl.render(width=self._max_x-self._min_x+3, height=self._max_y-self._min_y+3, cards=cards, empties=empties, follows=follows)
def print_board(self, next_card=None, players={}):
width = (self._max_x - self._min_x + 3) * 4 + 1
height = (self._max_y - self._min_y + 3) * 4 + 1
chrs = [[' ' for x in range(width)] for y in range(height)]
for x in range(0, width, 4):
for y in range(height):
chrs[y][x] = ''
for y in range(0, height, 4):
for x in range(width):
chrs[y][x] = '' if x % 4 != 0 else ''
items = list(self._field.items())
if next_card:
items.append(((self._min_x-1, self._min_y-1), next_card))
for coord, card in items:
x, y = coord
x = (x - self._min_x + 1) * 4 + 2
y = (y - self._min_y + 1) * 4 + 2
chrs[y-1][x] = p(card.road_top, '=', chrs[y-1][x], players)
chrs[y-1][x] = p(card.city_top, '#', chrs[y-1][x], players)
chrs[y][x-1] = p(card.road_left, '=', chrs[y][x-1], players)
chrs[y][x-1] = p(card.city_left, '#', chrs[y][x-1], players)
chrs[y][x] = p(card.monastery, 'M', chrs[y][x], players)
chrs[y][x+1] = p(card.road_right, '=', chrs[y][x+1], players)
chrs[y][x+1] = p(card.city_right, '#', chrs[y][x+1], players)
chrs[y+1][x] = p(card.road_bottom, '=', chrs[y+1][x], players)
chrs[y+1][x] = p(card.city_bottom, '#', chrs[y+1][x], players)
for line in chrs:
print(''.join(line))
class Player:
def __init__(self, uuid, i):
self.uuid = uuid
self.color = list(Color)[i]
self.score = 0
self.followers = [Follower(self) for _ in range(7)]
class Follower:
def __init__(self, player):
self.uuid = uuid4()
self.player = player
self.resource = None
self.cx = None
self.cy = None
self.lx = 50
self.ly = 50
def claim(self, r, card):
if self.resource is not None:
raise RuntimeError('Follower already claimed')
self.resource = r.root()
self.cx = card.x
self.cy = card.y
self.lx = 50
self.ly = 50
if card.road_top is r or card.city_top is r:
self.ly = 15
elif card.road_bottom is r or card.city_bottom is r:
self.ly = 85
elif card.road_left is r or card.city_left is r:
self.lx = 15
elif card.road_right is r or card.city_right is r:
self.lx = 85
elif card.pasture_lt is r:
self.lx = 15
self.ly = 25
elif card.pasture_tl is r:
self.lx = 25
self.ly = 15
elif card.pasture_rt is r:
self.lx = 85
self.ly = 25
elif card.pasture_tr is r:
self.lx = 75
self.ly = 15
elif card.pasture_lb is r:
self.lx = 15
self.ly = 75
elif card.pasture_bl is r:
self.lx = 25
self.ly = 85
elif card.pasture_rb is r:
self.lx = 85
self.ly = 75
elif card.pasture_br is r:
self.lx = 75
self.ly = 85
def svg(self, env, xoff, yoff):
if self.cx is None or self.cy is None or self.resource is None:
return ''
if isinstance(self.resource, Pasture):
tmpl = env.get_template(f'carcassonne/follower_pasture.svg')
else:
tmpl = env.get_template(f'carcassonne/follower.svg')
return tmpl.render(x=(self.cx+xoff)*100, y=(self.cy+yoff)*100, lx=self.lx, ly=self.ly, color=self.player.color.value.html)
class Carcassonne(Puzzle):
def __init__(self, pargs = None):
super().__init__('carcassonne', self.__class__)
if not pargs:
pargs = {}
self._players = {}
self._followers = []
self._scores = {}
self._turn_order = []
self._turn = 0
self._phase = Phase.PLACE
self._done = False
self._card = None
self._claimable = {}
self._completed_resources = set()
self._completed_pastures = set()
self._field = CarcassonneField()
self._urlbase = '/'
def add_player(self, uuid: UUID) -> None:
p = Player(uuid, len(self._turn_order))
self._players[uuid] = p
self._followers.extend(p.followers)
self._turn_order.append(uuid)
self._turn += 1
def get_extra_options_html(self) -> str:
return '''
<h3>Extensions</h3>
<input type=checkbox name="extensions" value="river" id="carcassonne-extension-river" />
<label for="carcassonne-extension-river">The River II</label><br/>
<input type=checkbox name="extensions" value="heretics" id="carcassonne-extension-heretics" />
<label for="carcassonne-extension-heretics">Heretics and Shrines</label><br/>
<input type=checkbox name="extensions" value="inns-cathedrals" id="carcassonne-extension-inns-cathedrals" />
<label for="carcassonne-extension-inns-cathedrals">Inns and Cathedrals</label><br/>
'''
def begin(self, options, urlbase):
self._urlbase = urlbase
self._deck = Card.generate_deck(options.getall('extensions'), self._field)
random.shuffle(self._turn_order)
self.next_turn()
def _check_done(self):
if self._done or len(self._deck) > 0 or self._card is not None:
return
self._done = True
for r in self._field._resources.values():
if r.completed:
continue
r.complete(True, self._players)
self._completed_pastures = set()
for r in self._field._resources.values():
r = r.root()
if r.completed and len(r.owners) > 0:
self._completed_pastures.add(r.uuid())
def next_turn(self):
self._claimable = {}
for r in self._completed_resources:
r.complete(False, self._players)
self._completed_resources.clear()
self._phase = Phase.PLACE
self._turn = (self._turn + 1) % len(self._turn_order)
try:
# Discard cards that can't be placed
placeable = False
discarded = []
card = None
while not placeable:
if card is not None:
discarded.insert(0, card)
card = self._deck.pop()
for _ in range(4):
if len(self._field.get_available_fields(card)) > 0:
placeable = True
break
card.rotate_cw()
# Return the discarded cards to the deck
self._deck.extend(discarded)
self._card = card
except IndexError:
self._card = None
self._check_done()
def process_action(self, puuid, action) -> None:
try:
act = action.get('action', None)
if puuid != self._turn_order[self._turn]:
return
if act == 'Rotate CW':
if self._phase != Phase.PLACE:
return
self._card.rotate_cw()
if act == 'Rotate CCW':
if self._phase != Phase.PLACE:
return
self._card.rotate_ccw()
if act == 'Place Tile':
if self._phase != Phase.PLACE:
return
x = int(action.get('x'))
y = int(action.get('y'))
self._completed_resources = self._field.place_card(x, y, self._card)
self._claimable = {r.uuid(): r.root() for r in self._card.resources.values() if len(r.root().owners) == 0}
if len(self._claimable) > 0 and len([f for f in self._players[puuid].followers if f.resource is None]) > 0:
self._phase = Phase.CLAIM
else:
self.next_turn()
if act == 'Claim':
if self._phase != Phase.CLAIM:
return
cuuid = action.get('claim')
if cuuid:
cuuid = UUID(hex=cuuid)
self._claimable[cuuid].claim(self._players[puuid], self._card)
self.next_turn()
except BaseException as e:
print(e)
def serialize(self, player):
# todo
return {}
def render(self, env, game, player, duration):
show_controls = player.uuid == self._turn_order[self._turn] and not self._done
field = self._field.svg(env, self._claimable, self._card, self._followers, self._phase, show_controls)
card = ''
claimable = ''
if self._card and self._phase == Phase.PLACE:
card = self._card.svg(env, standalone=True)
if self._claimable and self._phase == Phase.CLAIM:
claimable = '<option value="">Skip</option>\n ' + '\n '.join([f'<option value="{r.root().uuid()}">{r.root().uuid()}</option>' for r in self._claimable.values()])
tmpl = env.get_template('carcassonne/carcassonne.html.j2')
return tmpl.render(field=field, card=card, claimable=claimable, phase='place' if self._phase == Phase.PLACE else 'claim',
players=self._players.values(), me=self._players[player.uuid],
current_player=self._players[self._turn_order[self._turn]],
game=game, done=self._done,
completed_pastures=self._completed_pastures, deck=self._deck,
baseurl=self._urlbase)
@property
def human_name(self) -> str:
return 'Carcassonne'
@property
def scores(self):
return sorted(
[{'player': uuid, 'score': player.score} for uuid, player in self._players.items()],
key=lambda x: x['score'], reverse=True)
@property
def is_completed(self) -> bool:
return self._done
Carcassonne()

105
webgames/game.py Normal file
View file

@ -0,0 +1,105 @@
from typing import Any, Dict, List
from enum import Enum
from uuid import uuid4, UUID
from datetime import datetime
from webgames.puzzle import Puzzle
from webgames.sudoku_puzzle import Sudoku
from webgames.set_puzzle import SetPuzzle
from webgames.carcassonne import Carcassonne
from webgames.player import Player
from webgames.human import HumanID
class GameState(Enum):
LOBBY = 0
RUNNING = 1
FINISHED = 2
class Game:
def __init__(self, puzzle: str, puzzle_args: Dict[str, Any] = None) -> None:
self._uuid: UUID = uuid4()
self._human_id: HumanId = HumanID(self.uuid)
self._players: Dict[UUID, Player] = {}
self._puzzle: Puzzle = Puzzle.create(puzzle, puzzle_args)
self._start: datetime = None
self._end: datetime = None
self._state = GameState.LOBBY
def join(self, player: Player) -> None:
if self._state != GameState.LOBBY:
raise RuntimeError(f'Can\'t join a game in {self._state} state')
player.game = self
self._players[player.uuid] = player
self._puzzle.add_player(player.uuid)
def render(self, env, player, duration):
return self._puzzle.render(env, self, player, duration)
def get_extra_options_html(self) -> str:
return self._puzzle.get_extra_options_html()
def begin(self, options, urlbase) -> None:
if self._state != GameState.LOBBY:
raise RuntimeError(f'Can\'t start a game in {self._state} state')
self._start = datetime.utcnow()
self._state = GameState.RUNNING
self._puzzle.begin(options, urlbase)
def conclude(self) -> None:
if self._state == GameState.FINISHED:
raise RuntimeError(f'Can\'t start a game in {self._state} state')
if not self._start:
self._start = datetime.utcnow()
self._end = datetime.utcnow()
self._state = GameState.FINISHED
def process_action(self, player: UUID, action) -> None:
if self._state != GameState.RUNNING:
raise RuntimeError(f'Can\'t process a game action in {self._state} state')
self._puzzle.process_action(player, action)
def serialize_puzzle(self, player: UUID) -> None:
if self._state != GameState.RUNNING:
raise RuntimeError(f'Can\'t fetch a player puzzle in {self._state} state')
return self._puzzle.serialize(player)
@property
def uuid(self) -> UUID:
return self._uuid
@property
def human_id(self) -> HumanID:
return self._human_id
@property
def puzzle_name(self):
return self._puzzle.human_name
@property
def puzzle(self):
return self._puzzle
@property
def players(self) -> Dict[UUID, Player]:
return self._players
@property
def scoreboard(self) -> List[Any]:
return self._puzzle.scores
@property
def state(self) -> GameState:
return self._state
@property
def start(self) -> datetime:
return self._start
@property
def end(self) -> datetime:
return self._end

57
webgames/human.py Normal file
View file

@ -0,0 +1,57 @@
import uuid
import hashlib
from functools import reduce
class HumanID:
_CODE = {
0: 'A',
1: 'B',
2: 'F',
3: 'H',
4: 'J',
5: 'K',
6: 'L',
7: 'M',
8: 'P',
9: 'Q',
10: 'R',
11: 'S',
12: 'T',
13: 'U',
14: 'X',
15: 'Z'
}
_resolver = {}
def __init__(self, longid: uuid.UUID = None) -> None:
if not longid:
longid = uuid.uuid4()
sha = hashlib.sha1(longid.bytes).digest()
e = reduce(lambda a,b: a^b, [e for e in sha[::2]], 0)
o = reduce(lambda a,b: a^b, [o for o in sha[1::2]], 0)
nibbles = [(e & 0xf0) >> 4, e & 0x0f, (o & 0xf0) >> 4, o & 0x0f]
self._human_id = ''.join([HumanID._CODE[i] for i in nibbles])
self._uuid = longid
HumanID._resolver[self._human_id] = self._uuid
def __del__(self) -> None:
HumanID._resolver[self._human_id]
@classmethod
def resolve(cls, human_id: str) -> uuid.UUID:
return cls._resolver[human_id]
def __str__(self) -> str:
return self._human_id
@property
def human_id(self) -> str:
return self._human_id
@property
def uuid(self) -> uuid.UUID:
return self._uuid

23
webgames/player.py Normal file
View file

@ -0,0 +1,23 @@
from uuid import uuid4, UUID
from namesgenerator import get_random_name
class Player:
def __init__(self, name: str = None) -> None:
if name is None:
name = get_random_name(' ').title()
self._uuid: UUID = uuid4()
self._name: str = name
@property
def uuid(self) -> UUID:
return self._uuid
@property
def name(self) -> str:
return self._name
def __repr__(self) -> str:
return f'Player({self._name})'

50
webgames/puzzle.py Normal file
View file

@ -0,0 +1,50 @@
from typing import Any, Dict
import abc
from uuid import UUID
_PUZZLES = {}
class Puzzle(abc.ABC):
def __init__(self, name, clazz) -> None:
global _PUZZLES
_PUZZLES[name] = clazz
def get_extra_options_html(self) -> str:
return ''
def add_player(self, uuid: UUID) -> None:
pass
def begin(self, options: Dict[str, Any], urlbase: str) -> None:
pass
def process_action(self, player, action) -> None:
pass
def serialize(self, player):
pass
def render(self, env, game, player, duration):
pass
@property
def human_name(self) -> str:
pass
@property
def scores(self):
pass
@property
def is_completed(self) -> bool:
pass
@staticmethod
def create(puzzle: str, args) -> 'Puzzle':
return _PUZZLES[puzzle](args)

206
webgames/set_puzzle.py Normal file
View file

@ -0,0 +1,206 @@
import random
import enum
from uuid import UUID
from webgames.puzzle import Puzzle
class _Color:
def __init__(self, name, html):
self.name = name
self.html = html
class Infill(enum.Enum):
NONE = 1
SEMI = 2
FULL = 3
class Count(enum.Enum):
ONE = 1
TWO = 2
THREE = 3
class Shape(enum.Enum):
RECTANGLE = 1
TILDE = 2
ELLIPSE = 3
class Color(enum.Enum):
RED = _Color('red', '#ff0000')
GREEN = _Color('green', '#00ff00')
BLUE = _Color('blue', '#0000ff')
class Card:
def __init__(self, infill, count, shape, color):
self.infill = infill
self.count = count
self.shape = shape
self.color = color
def __repr__(self) -> str:
return f'<Card({self.infill},{self.count},{self.shape},{self.color})>'
def serialize(self):
return {
'infill': self.infill.name,
'count': self.count.name,
'shape': self.shape.name,
'color': self.color.name
}
@staticmethod
def validate_set(c1, c2, c3):
return sum([
len(x) == 1 or len(x) == 3
for x in [
{ c1.infill, c2.infill, c3.infill },
{ c1.count, c2.count, c3.count },
{ c1.shape, c2.shape, c3.shape },
{ c1.color, c2.color, c3.color }
]
]) == 4
@staticmethod
def generate_deck():
deck = [
Card(infill, count, shape, color)
for infill in Infill
for count in Count
for shape in Shape
for color in Color
]
random.shuffle(deck)
return deck
class SetPuzzle(Puzzle):
def __init__(self, pargs = None):
super().__init__('set', self.__class__)
if not pargs:
pargs = {}
self._players = {}
self._scores = {}
self._deck = Card.generate_deck()
self._playing_field = []
self._draw_votes = set()
self._draw_majority = 0
self._done = False
self._draw(9)
self._urlbase = '/'
def get_extra_options_html(self) -> str:
return ''
def add_player(self, uuid: UUID) -> None:
self._players[uuid] = []
self._scores[uuid] = 0
self._draw_majority = int(-(-(len(self._players)+0.5)//2))
def begin(self, options, urlbase):
self._urlbase = urlbase
def _draw(self, n: int = 3, append=True):
ret = []
for i in range(n):
try:
card = self._deck.pop()
except IndexError:
break
ret.append(card)
if append:
self._playing_field.append(card)
self._draw_votes.clear()
return ret
def _check_done(self):
if self._done or len(self._deck) > 0:
return
for i1 in range(0, len(self._playing_field)-2):
for i2 in range(i1+1, len(self._playing_field)-1):
for i3 in range(i2+1, len(self._playing_field)):
if Card.validate_set(self._playing_field[i1], self._playing_field[i2], self._playing_field[i3]):
# There is still a valid set - not done yet
return
self._done = True
def process_action(self, player, action) -> None:
act = action.get('action', None)
if act == 'Draw 3':
self._draw_votes.add(player)
if len(self._draw_votes) >= self._draw_majority:
self._draw()
self._check_done()
elif act == 'Set!':
ci = [int(x) for x in action.getall('set')]
cards = [self._playing_field[i] for i in ci]
if len(cards) != 3:
return
if Card.validate_set(*cards):
self._scores[player] += 3
new = self._draw(append=False)
for card, i in zip(cards, ci):
try:
self._playing_field[i] = new.pop()
except IndexError:
self._playing_field.remove(card)
self._players[player].append(card)
self._check_done()
else:
self._scores[player] -= 1
self._draw_votes.clear()
def serialize(self, player):
return {
'field': [x.serialize() for x in self._playing_field],
'players': sorted([{
'name': x,
'score': self._scores[x],
'cards': [y.serialize() for y in self._players[x][-3:]]
} for x in self._players.keys()], key=lambda x: x['score'], reverse=True),
'deck': len(self._deck),
'draw': {
'votes': len(self._draw_votes),
'majority': self._draw_majority
},
'done': self._done
}
def render(self, env, game, player, duration):
tmpl = env.get_template('cwa/set.html.j2')
return tmpl.render(player=player,
game=game,
duration=duration,
cards=self._playing_field,
deck=self._deck,
lastset={k: v[-3:] for k, v in self._players.items()},
draw_votes=len(self._draw_votes),
draw_majority=self._draw_majority,
Color=Color,
Shape=Shape,
Infill=Infill,
baseurl=self._urlbase)
@property
def human_name(self) -> str:
return 'Set'
@property
def scores(self):
return sorted(
[{'player': k, 'score': v} for k, v in self._scores.items()],
key=lambda x: x['score'], reverse=True)
@property
def is_completed(self) -> bool:
return self._done
SetPuzzle()

169
webgames/sudoku_puzzle.py Normal file
View file

@ -0,0 +1,169 @@
from typing import Any, Dict
from uuid import UUID
from sudokugen.solver import solve, NoSolution
from sudoku_manager import Sudoku as SudokuManager
from webgames.puzzle import Puzzle
class Sudoku:
def __init__(self, difficulty: int = 2) -> None:
self._solved = False
self.grid = SudokuManager.generate_grid(difficulty)
self.original = [[col is not None for col in row] for row in self.grid]
def put(self, row: int, col: int, value: int) -> None:
if self.original[row-1][col-1]:
raise KeyError(f'Cell ({row},{col}) is part of the original grid')
if value == 0:
value = None
if value is not None and value not in range(10):
raise ValueError(f'Don\'t understand value {value}')
self.grid[row-1][col-1] = value
def get(self, row: int, col: int) -> int:
return self.grid[row-1][col-1]
def begin(self):
pass
def make_instance(self) -> 'Sudoku':
grid = [[None for i in range(9)] for i in range(9)]
for row in range(9):
for col in range(9):
if self.original[row][col]:
grid[row][col] = self.grid[row][col]
else:
grid[row][col] = None
s = Sudoku()
s.grid = grid
s.original = self.original
return s
def process_action(self, action: Dict[str, Any]) -> None:
field = action['sudoku-field']
row = int(field[:1])
col = int(field[1:])
number = action['number']
if number == 'Delete':
self.put(row, col, None)
else:
self.put(row, col, int(number))
def serialize(self) -> Dict[str, Any]:
return {
'grid': self.grid,
'original': self.original,
'filled': self.filled,
'solved': self.solved
}
@property
def filled(self) -> bool:
if self._solved:
return True
return len([1 for row in self.grid for col in row if col is None]) == 0
@property
def solved(self) -> bool:
if not self.filled:
return False
if self._solved:
return True
try:
solve(self.grid)
self._solved = True
except NoSolution:
return False
return True
@property
def progress(self) -> float:
n = len([1 for row in self.original for col in row if not col])
i = len([1 for row in self.grid for col in row if col is None])
return 1 - i/n
class SudokuPuzzle(Puzzle):
def __init__(self, pargs = None):
super().__init__('sudoku', self.__class__)
if not pargs:
pargs = {}
self._puzzles: Dict[UUID, Sudoku] = {}
self._scores: List[Any] = []
self._urlbase = '/'
def begin(self, options: Dict[str, Any], urlbase):
difficulty = int(options.get('sudoku-input-difficulty', '2'))
self._urlbase = urlbase
self._puzzle = Sudoku(difficulty)
for p in self._puzzles.keys():
self._puzzles[p] = self._puzzle.make_instance()
def get_extra_options_html(self) -> str:
return '''
<label for="sudoku-input-difficulty">Difficulty: </label>
<input id="sudoku-input-difficulty" name="sudoku-input-difficulty" type="number" value="2" min="1" max="3" />
'''
def add_player(self, uuid: UUID) -> None:
self._puzzles[uuid] = None
def process_action(self, player, action) -> None:
cell = action.get('sudoku-field')
row, col = int(cell[0]), int(cell[1])
v = action.get('number')
if v == 'Delete':
value = None
else:
value = int(v)
puzzle = self._puzzles[player]
if puzzle.solved:
raise RuntimeError(f'Can\'t process a game action for a solved puzzle')
puzzle.process_action(action)
if puzzle.solved:
duration = datetime.utcnow() - self._start
self._scores.append({
'player': player,
'duration': duration
})
def serialize(self, player):
puzzle = self._puzzles[player]
return puzzle.serialize()
def render(self, env, game, player, duration):
puzzle = self._puzzles[player.uuid]
tmpl = env.get_template('cwa/sudoku.html.j2')
return tmpl.render(player=player,
game=game,
puzzle=puzzle,
duration=duration,
baseurl=self._urlbase)
@property
def human_name(self) -> str:
return 'Sudoku'
@property
def puzzles(self):
return self._puzzles
@property
def scores(self):
return self._scores
@property
def is_completed(self) -> bool:
for x in self._puzzles.values():
if not x.solved:
return False
return True
SudokuPuzzle()