Initial commit
This commit is contained in:
commit
a1e6661249
91 changed files with 3767 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
**/__pycache__/
|
||||||
|
*.pyc
|
||||||
|
**/*.egg-info/
|
||||||
|
*.coverage
|
||||||
|
**/.mypy_cache/
|
5
requirements.txt
Normal file
5
requirements.txt
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
jinja3
|
||||||
|
bottle
|
||||||
|
sudoku-manager
|
||||||
|
sudokugen
|
||||||
|
namesgenerator
|
39
setup.py
Executable file
39
setup.py
Executable 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'
|
||||||
|
],
|
||||||
|
)
|
BIN
static/carcassonne-pasture.jpg
Normal file
BIN
static/carcassonne-pasture.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 241 KiB |
17
static/script.js
Normal file
17
static/script.js
Normal 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
60
static/set.js
Normal 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
144
static/style.css
Normal 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
0
static/sudoku.js
Normal file
6
templates/carcassonne/001.svg
Normal file
6
templates/carcassonne/001.svg
Normal 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>
|
7
templates/carcassonne/011.svg
Normal file
7
templates/carcassonne/011.svg
Normal 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>
|
5
templates/carcassonne/021.svg
Normal file
5
templates/carcassonne/021.svg
Normal 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>
|
5
templates/carcassonne/022.svg
Normal file
5
templates/carcassonne/022.svg
Normal 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>
|
9
templates/carcassonne/031.svg
Normal file
9
templates/carcassonne/031.svg
Normal 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>
|
11
templates/carcassonne/041.svg
Normal file
11
templates/carcassonne/041.svg
Normal 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>
|
5
templates/carcassonne/101.svg
Normal file
5
templates/carcassonne/101.svg
Normal 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>
|
6
templates/carcassonne/121.svg
Normal file
6
templates/carcassonne/121.svg
Normal 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>
|
6
templates/carcassonne/122.svg
Normal file
6
templates/carcassonne/122.svg
Normal 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>
|
6
templates/carcassonne/123.svg
Normal file
6
templates/carcassonne/123.svg
Normal 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>
|
10
templates/carcassonne/131.svg
Normal file
10
templates/carcassonne/131.svg
Normal 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>
|
5
templates/carcassonne/201.svg
Normal file
5
templates/carcassonne/201.svg
Normal 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>
|
8
templates/carcassonne/202.svg
Normal file
8
templates/carcassonne/202.svg
Normal 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>
|
6
templates/carcassonne/203.svg
Normal file
6
templates/carcassonne/203.svg
Normal 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>
|
7
templates/carcassonne/204.svg
Normal file
7
templates/carcassonne/204.svg
Normal 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>
|
7
templates/carcassonne/205.svg
Normal file
7
templates/carcassonne/205.svg
Normal 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>
|
7
templates/carcassonne/206.svg
Normal file
7
templates/carcassonne/206.svg
Normal 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>
|
8
templates/carcassonne/221.svg
Normal file
8
templates/carcassonne/221.svg
Normal 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>
|
9
templates/carcassonne/222.svg
Normal file
9
templates/carcassonne/222.svg
Normal 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>
|
6
templates/carcassonne/301.svg
Normal file
6
templates/carcassonne/301.svg
Normal 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>
|
7
templates/carcassonne/302.svg
Normal file
7
templates/carcassonne/302.svg
Normal 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>
|
8
templates/carcassonne/311.svg
Normal file
8
templates/carcassonne/311.svg
Normal 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>
|
9
templates/carcassonne/312.svg
Normal file
9
templates/carcassonne/312.svg
Normal 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>
|
6
templates/carcassonne/401.svg
Normal file
6
templates/carcassonne/401.svg
Normal 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>
|
161
templates/carcassonne/carcassonne.html.j2
Normal file
161
templates/carcassonne/carcassonne.html.j2
Normal 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>
|
3
templates/carcassonne/defense.svg
Normal file
3
templates/carcassonne/defense.svg
Normal 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>
|
4
templates/carcassonne/empty.svg
Normal file
4
templates/carcassonne/empty.svg
Normal 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>
|
14
templates/carcassonne/field.svg
Normal file
14
templates/carcassonne/field.svg
Normal 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 |
3
templates/carcassonne/follower.svg
Normal file
3
templates/carcassonne/follower.svg
Normal 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>
|
3
templates/carcassonne/follower_pasture.svg
Normal file
3
templates/carcassonne/follower_pasture.svg
Normal 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>
|
7
templates/carcassonne/he-001.svg
Normal file
7
templates/carcassonne/he-001.svg
Normal 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>
|
8
templates/carcassonne/he-011.svg
Normal file
8
templates/carcassonne/he-011.svg
Normal 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>
|
11
templates/carcassonne/he-021.svg
Normal file
11
templates/carcassonne/he-021.svg
Normal 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>
|
8
templates/carcassonne/he-101.svg
Normal file
8
templates/carcassonne/he-101.svg
Normal 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>
|
9
templates/carcassonne/he-111.svg
Normal file
9
templates/carcassonne/he-111.svg
Normal 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>
|
8
templates/carcassonne/ic-021-inn.svg
Normal file
8
templates/carcassonne/ic-021-inn.svg
Normal 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>
|
9
templates/carcassonne/ic-021-monastery.svg
Normal file
9
templates/carcassonne/ic-021-monastery.svg
Normal 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>
|
8
templates/carcassonne/ic-022-inn.svg
Normal file
8
templates/carcassonne/ic-022-inn.svg
Normal 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>
|
12
templates/carcassonne/ic-031-inn.svg
Normal file
12
templates/carcassonne/ic-031-inn.svg
Normal 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>
|
7
templates/carcassonne/ic-041.svg
Normal file
7
templates/carcassonne/ic-041.svg
Normal 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>
|
5
templates/carcassonne/ic-101-branch.svg
Normal file
5
templates/carcassonne/ic-101-branch.svg
Normal 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>
|
6
templates/carcassonne/ic-101-road.svg
Normal file
6
templates/carcassonne/ic-101-road.svg
Normal 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>
|
11
templates/carcassonne/ic-122-inn.svg
Normal file
11
templates/carcassonne/ic-122-inn.svg
Normal 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>
|
12
templates/carcassonne/ic-202-roads.svg
Normal file
12
templates/carcassonne/ic-202-roads.svg
Normal 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>
|
11
templates/carcassonne/ic-203-road-inn.svg
Normal file
11
templates/carcassonne/ic-203-road-inn.svg
Normal 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>
|
8
templates/carcassonne/ic-203-road.svg
Normal file
8
templates/carcassonne/ic-203-road.svg
Normal 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>
|
12
templates/carcassonne/ic-221-crossing.svg
Normal file
12
templates/carcassonne/ic-221-crossing.svg
Normal 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>
|
12
templates/carcassonne/ic-222-inn.svg
Normal file
12
templates/carcassonne/ic-222-inn.svg
Normal 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>
|
6
templates/carcassonne/ic-301-split.svg
Normal file
6
templates/carcassonne/ic-301-split.svg
Normal 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>
|
6
templates/carcassonne/ic-302.svg
Normal file
6
templates/carcassonne/ic-302.svg
Normal 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>
|
6
templates/carcassonne/ic-401-cathedral.svg
Normal file
6
templates/carcassonne/ic-401-cathedral.svg
Normal 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>
|
7
templates/carcassonne/ic-401-pasture.svg
Normal file
7
templates/carcassonne/ic-401-pasture.svg
Normal 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>
|
8
templates/carcassonne/r2-city.svg
Normal file
8
templates/carcassonne/r2-city.svg
Normal 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>
|
9
templates/carcassonne/r2-citybridge.svg
Normal file
9
templates/carcassonne/r2-citybridge.svg
Normal 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>
|
7
templates/carcassonne/r2-curve.svg
Normal file
7
templates/carcassonne/r2-curve.svg
Normal 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>
|
8
templates/carcassonne/r2-fork.svg
Normal file
8
templates/carcassonne/r2-fork.svg
Normal 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>
|
7
templates/carcassonne/r2-lake.svg
Normal file
7
templates/carcassonne/r2-lake.svg
Normal 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>
|
9
templates/carcassonne/r2-lake2.svg
Normal file
9
templates/carcassonne/r2-lake2.svg
Normal 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>
|
8
templates/carcassonne/r2-monastery.svg
Normal file
8
templates/carcassonne/r2-monastery.svg
Normal 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>
|
15
templates/carcassonne/r2-road.svg
Normal file
15
templates/carcassonne/r2-road.svg
Normal 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>
|
9
templates/carcassonne/r2-road2.svg
Normal file
9
templates/carcassonne/r2-road2.svg
Normal 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>
|
13
templates/carcassonne/r2-roadcity.svg
Normal file
13
templates/carcassonne/r2-roadcity.svg
Normal 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>
|
6
templates/carcassonne/r2-source.svg
Normal file
6
templates/carcassonne/r2-source.svg
Normal 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>
|
13
templates/cwa/base.html.j2
Normal file
13
templates/cwa/base.html.j2
Normal 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
58
templates/cwa/set.html.j2
Normal 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 %}
|
50
templates/cwa/setcard.svg.j2
Normal file
50
templates/cwa/setcard.svg.j2
Normal 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>
|
68
templates/cwa/sudoku.html.j2
Normal file
68
templates/cwa/sudoku.html.j2
Normal 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 %}
|
15
templates/cwa/welcome.html.j2
Normal file
15
templates/cwa/welcome.html.j2
Normal 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 %}
|
20
templates/cwa/welcome1.html.j2
Normal file
20
templates/cwa/welcome1.html.j2
Normal 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 %}
|
29
templates/cwa/welcome2.html.j2
Normal file
29
templates/cwa/welcome2.html.j2
Normal 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 %}
|
15
templates/cwa/welcome3.html.j2
Normal file
15
templates/cwa/welcome3.html.j2
Normal 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 %}
|
658
templates/sudoku.example.html
Normal file
658
templates/sudoku.example.html
Normal 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
55
templates/sudoku.html
Normal 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
2
webgames/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
__version__ = '0.1'
|
165
webgames/__main__.py
Normal file
165
webgames/__main__.py
Normal 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
184
webgames/api.py
Normal 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
910
webgames/carcassonne.py
Normal 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
105
webgames/game.py
Normal 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
57
webgames/human.py
Normal 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
23
webgames/player.py
Normal 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
50
webgames/puzzle.py
Normal 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
206
webgames/set_puzzle.py
Normal 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
169
webgames/sudoku_puzzle.py
Normal 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()
|
Loading…
Reference in a new issue