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