2020-09-19 23:47:58 +02:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright (C) 2020 Guillaume Collic
|
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
import math
|
|
|
|
from functools import partial
|
|
|
|
from boxes import Boxes, edges
|
|
|
|
from .dividertray import (
|
|
|
|
SlotDescriptionsGenerator,
|
|
|
|
DividerSlotsEdge,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class AgricolaInsert(Boxes):
|
|
|
|
"""
|
|
|
|
Agricola Revised Edition game box insert, including some expansions.
|
|
|
|
"""
|
|
|
|
|
|
|
|
ui_group = "Misc"
|
|
|
|
|
|
|
|
description = """
|
2020-11-15 18:50:08 +01:00
|
|
|
This insert was designed with 3 mm plywood in mind, and should work fine with
|
|
|
|
materials around this thickness.
|
|
|
|
|
2020-09-19 23:47:58 +02:00
|
|
|
This is an insert for the [Agricola Revised Edition](https://boardgamegeek.com/boardgame/200680/agricola-revised-edition)
|
|
|
|
board game. It is specifically designed around the [Farmers Of The Moor expansion](https://boardgamegeek.com/boardgameexpansion/257344/agricola-farmers-moor),
|
|
|
|
and should also store the [5-6 players expansion](https://boardgamegeek.com/boardgameexpansion/210625/agricola-expansion-5-and-6-players)
|
|
|
|
(not tested, but I tried to take everything into account for it, please inform
|
|
|
|
us if you tested it).
|
|
|
|
|
|
|
|
It can be stored inside the original game box, including the 2 expansions,
|
|
|
|
with the lid slightly raised.
|
|
|
|
|
|
|
|
The parts of a given element are mostly generated next to each other vertically.
|
|
|
|
It should be straightforward to match them.
|
|
|
|
|
|
|
|
Here are the different elements, from left to right in the generated file.
|
|
|
|
|
|
|
|
#### Card tray
|
|
|
|
|
|
|
|
The cards are all kept in a tray, with paper dividers to sort them easily. When
|
|
|
|
the tray is not full of cards, wood dividers slides in slots in order to keep
|
|
|
|
the cards from falling into the empty space.
|
|
|
|
|
|
|
|
There should be enough space for the main game, Farmers Of The Moor, and the 5-6
|
|
|
|
player expansion, but not much more than that.
|
|
|
|
|
|
|
|
To keep a lower profile, the cards are at a slight angle, and the paper dividers
|
|
|
|
tabs are horizontal instead of vertical.
|
|
|
|
A small wall keeps the card against one side while the tabs protrude on the
|
|
|
|
other side, above the small wall.
|
|
|
|
|
|
|
|
The wall with the big hole is the sloped one. It goes between the two
|
|
|
|
"comb-like" walls first, with its two small holes at the bottom. Then there is a
|
|
|
|
low-height long wall with a sloped edge which should go from the sloped wall to
|
|
|
|
the other side. You can finish the tray with the last wall at the end.
|
|
|
|
|
|
|
|
#### Upper level trays
|
|
|
|
|
|
|
|
4 trays with movable walls are used to store resources. They were designed to
|
|
|
|
store them in this order:
|
|
|
|
|
|
|
|
* Stone / Vegetable / Pig / Cow
|
|
|
|
* Reed / Grain / Sheep
|
|
|
|
* Wood / Clay
|
|
|
|
* Food / Fire
|
|
|
|
|
|
|
|
The wall would probably be better if fixed instead of movable, but I would like
|
|
|
|
to test with the 5-6 player expansion to be sure their positions are correct
|
|
|
|
with it too.
|
|
|
|
|
|
|
|
The little feet of the movable wall should be glued. The triangles are put
|
|
|
|
horizontally, with their bases towards the sides.
|
|
|
|
|
|
|
|
#### Lower level tray
|
|
|
|
|
|
|
|
The lower level tray is used to store the horses.
|
|
|
|
|
|
|
|
#### Room/Field tiles
|
|
|
|
|
|
|
|
Two boxes are generated to store the room/field tiles. One for the wood/field,
|
|
|
|
the other for the clay/stone. They are stored with the main opening upside, but
|
|
|
|
I prefer to use them during play with this face on the side.
|
|
|
|
|
|
|
|
#### Moor/Forest and miscellaneous tiles
|
|
|
|
|
|
|
|
A box is generated to store the Moor/Forest tiles, and some other tiles such as
|
|
|
|
the "multiple resources" cardboard tokens.
|
|
|
|
|
|
|
|
The Moor/Forest tiles are at the same height as the Room/Field, and the upper
|
|
|
|
level trays are directly on them. The horse box and player box are slightly
|
|
|
|
lower. This Moor/Forest box have a lowered corner (the one for the miscellaneous
|
|
|
|
tiles). Two cardboard pieces can be stored between the smaller boxes and the
|
|
|
|
upper level trays (as seen on the picture).
|
|
|
|
|
|
|
|
Be sure to match the pieces so that the walls with smaller heights are next to
|
|
|
|
each other.
|
|
|
|
|
|
|
|
#### Players bit boxes
|
|
|
|
|
|
|
|
Each player has its own box where the bits of his color are stored.
|
|
|
|
The cardboard bed from Farmers Of The Moor is central to this box.
|
|
|
|
|
|
|
|
* The fences are stored inside the bed
|
|
|
|
* The bed is placed in the box, with holes to keep it there (and to take less
|
|
|
|
height)
|
|
|
|
* The stables are stored in the two corners
|
|
|
|
* The five farmers are stored between the bed and the three walls, alternatively
|
|
|
|
head up and head down.
|
|
|
|
|
|
|
|
During assembly, the small bars are put in the middle holes. The two bigger
|
|
|
|
holes at the ends are used for the bed feet. The bar keeps the bed from
|
|
|
|
protruding underneath.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
Boxes.__init__(self)
|
|
|
|
self.addSettingsArgs(edges.FingerJointSettings, surroundingspaces=1.0)
|
|
|
|
|
|
|
|
def render(self):
|
|
|
|
player_box_height = 34.5
|
2020-11-15 18:50:08 +01:00
|
|
|
player_box_inner_width = 50.5
|
2020-09-19 23:47:58 +02:00
|
|
|
bigger_box_inner_height = 36.7
|
|
|
|
row_width = 37.2
|
|
|
|
tray_inner_height = 17
|
|
|
|
box_width = 218
|
2020-11-15 18:50:08 +01:00
|
|
|
card_tray_height = (
|
|
|
|
self.thickness * 2 + tray_inner_height + bigger_box_inner_height
|
2020-09-19 23:47:58 +02:00
|
|
|
)
|
2020-11-15 18:50:08 +01:00
|
|
|
card_tray_width = (
|
|
|
|
305.35 - player_box_inner_width * 2 - row_width * 2 - 9 * self.thickness
|
|
|
|
)
|
|
|
|
|
|
|
|
self.render_card_divider_tray(card_tray_height, box_width, card_tray_width)
|
2020-09-19 23:47:58 +02:00
|
|
|
self.render_upper_token_trays(tray_inner_height, box_width)
|
2020-11-15 18:50:08 +01:00
|
|
|
wood_room_box_width = 39.8
|
|
|
|
self.render_room_box(wood_room_box_width, bigger_box_inner_height, row_width)
|
|
|
|
stone_room_box_width = 26.7
|
|
|
|
self.render_room_box(stone_room_box_width, bigger_box_inner_height, row_width)
|
|
|
|
moor_box_length = 84.6
|
|
|
|
self.render_moor_box(
|
|
|
|
bigger_box_inner_height, player_box_height, row_width, moor_box_length
|
|
|
|
)
|
|
|
|
horse_box_margin = 0.5
|
|
|
|
horse_box_length = (
|
|
|
|
box_width
|
|
|
|
- wood_room_box_width
|
|
|
|
- stone_room_box_width
|
|
|
|
- moor_box_length
|
|
|
|
- 6 * self.thickness
|
|
|
|
- horse_box_margin
|
|
|
|
)
|
|
|
|
self.render_horse_box(player_box_height, row_width, horse_box_length)
|
2020-09-19 23:47:58 +02:00
|
|
|
for _ in range(6):
|
2020-11-15 18:50:08 +01:00
|
|
|
self.render_player_box(player_box_height, player_box_inner_width)
|
2020-09-19 23:47:58 +02:00
|
|
|
|
|
|
|
def render_card_divider_tray(
|
2020-11-15 18:50:08 +01:00
|
|
|
self, card_tray_height, card_tray_length, card_tray_width
|
2020-09-19 23:47:58 +02:00
|
|
|
):
|
|
|
|
"""
|
|
|
|
The whole tray which contains the cards, including its dividers.
|
|
|
|
Cards are at an angle, to save height.
|
|
|
|
"""
|
|
|
|
self.ctx.save()
|
|
|
|
|
2020-11-15 18:50:08 +01:00
|
|
|
tray_inner_length = card_tray_length - self.thickness
|
2020-09-19 23:47:58 +02:00
|
|
|
margin_for_score_sheet = 0 # 3 if you want more space for score sheet
|
|
|
|
sleeved_cards_width = 62 + margin_for_score_sheet
|
|
|
|
|
|
|
|
rad = math.acos(card_tray_height / sleeved_cards_width)
|
|
|
|
angle = math.degrees(rad)
|
|
|
|
cos = math.cos(rad)
|
|
|
|
tan = math.tan(rad)
|
|
|
|
sin = math.sin(rad)
|
|
|
|
|
|
|
|
slots_number = 19
|
|
|
|
slot_depth = 30
|
|
|
|
slot_descriptions = SlotDescriptionsGenerator().generate_all_same_angles(
|
|
|
|
[tray_inner_length / slots_number for _ in range(slots_number)],
|
|
|
|
self.thickness,
|
|
|
|
0.2,
|
|
|
|
slot_depth,
|
|
|
|
card_tray_height,
|
|
|
|
angle,
|
|
|
|
)
|
|
|
|
slot_descriptions.adjust_to_target_length(tray_inner_length)
|
|
|
|
|
|
|
|
sloped_wall_height = sleeved_cards_width - self.thickness * (tan + 1 / tan)
|
|
|
|
sloped_wall_posx_at_y0 = (
|
|
|
|
tray_inner_length - sloped_wall_height * tan - cos * self.thickness
|
|
|
|
)
|
|
|
|
sloped_wall_posx = sloped_wall_posx_at_y0 + cos * self.thickness / 2
|
|
|
|
sloped_wall_posy = sin * self.thickness / 2
|
|
|
|
|
|
|
|
dse = DividerSlotsEdge(self, slot_descriptions.descriptions)
|
|
|
|
for _ in range(2):
|
|
|
|
self.rectangularWall(
|
|
|
|
tray_inner_length,
|
|
|
|
card_tray_height,
|
|
|
|
["e", "e", dse, "f"],
|
|
|
|
move="up",
|
|
|
|
callback=[
|
|
|
|
partial(
|
|
|
|
lambda: self.fingerHolesAt(
|
|
|
|
sloped_wall_posx,
|
|
|
|
sloped_wall_posy,
|
|
|
|
sloped_wall_height,
|
|
|
|
angle=90 - angle,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
# generate spacer
|
|
|
|
spacer_height = card_tray_height / 2
|
2020-11-15 18:50:08 +01:00
|
|
|
spacer_spacing = card_tray_width-99.8
|
2020-09-19 23:47:58 +02:00
|
|
|
spacer_upper_width = sloped_wall_posx_at_y0 + spacer_height * tan
|
|
|
|
self.trapezoidWall(
|
|
|
|
spacer_height,
|
|
|
|
spacer_upper_width,
|
|
|
|
sloped_wall_posx_at_y0,
|
|
|
|
"fefe",
|
|
|
|
move="up rotated",
|
|
|
|
)
|
|
|
|
|
|
|
|
self.rectangularWall(
|
|
|
|
card_tray_width,
|
|
|
|
card_tray_height,
|
|
|
|
"eFeF",
|
|
|
|
move="up",
|
|
|
|
callback=[
|
|
|
|
partial(
|
|
|
|
lambda: self.fingerHolesAt(
|
|
|
|
spacer_spacing - self.thickness / 2, 0, spacer_height
|
|
|
|
)
|
|
|
|
)
|
|
|
|
],
|
|
|
|
)
|
|
|
|
self.rectangularWall(
|
|
|
|
card_tray_width,
|
|
|
|
sloped_wall_height,
|
|
|
|
"efef",
|
|
|
|
move="up",
|
|
|
|
callback=[
|
|
|
|
partial(
|
|
|
|
self.generate_card_tray_sloped_wall_holes,
|
|
|
|
card_tray_width,
|
|
|
|
sloped_wall_height,
|
|
|
|
spacer_height,
|
|
|
|
spacer_spacing,
|
|
|
|
rad,
|
|
|
|
)
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
self.ctx.restore()
|
2020-11-15 18:50:08 +01:00
|
|
|
self.rectangularWall(card_tray_length, 0, "FFFF", move="right only")
|
2020-09-19 23:47:58 +02:00
|
|
|
self.ctx.save()
|
|
|
|
|
|
|
|
divider_height = sleeved_cards_width - self.thickness * tan
|
|
|
|
self.generate_divider(
|
|
|
|
card_tray_width, divider_height, slot_depth, spacer_spacing, "up"
|
|
|
|
)
|
|
|
|
self.explain(
|
|
|
|
[
|
|
|
|
"Wood divider",
|
|
|
|
"Hard separation to keep the card",
|
|
|
|
"from slipping in empty space left.",
|
|
|
|
"Takes more space, but won't move.",
|
|
|
|
"Duplicate as much as you want",
|
|
|
|
"(I use 2).",
|
|
|
|
]
|
|
|
|
)
|
|
|
|
|
|
|
|
self.ctx.restore()
|
|
|
|
self.rectangularWall(card_tray_width, 0, "ffff", move="right only")
|
|
|
|
self.ctx.save()
|
|
|
|
|
|
|
|
self.generate_paper_divider(
|
|
|
|
card_tray_width, sleeved_cards_width, slot_depth, spacer_spacing, "up"
|
|
|
|
)
|
|
|
|
self.explain(
|
|
|
|
[
|
|
|
|
"Paper divider",
|
|
|
|
"Soft separation to search easily",
|
|
|
|
"the card group you need",
|
|
|
|
"(by expansion, number of player,",
|
|
|
|
"etc.).",
|
|
|
|
"Duplicate as much as you want",
|
|
|
|
"(I use 7).",
|
|
|
|
]
|
|
|
|
)
|
|
|
|
self.ctx.restore()
|
|
|
|
self.rectangularWall(card_tray_width, 0, "ffff", move="right only")
|
|
|
|
|
|
|
|
def explain(self, strings):
|
|
|
|
self.text(
|
|
|
|
str.join(
|
|
|
|
"\n",
|
|
|
|
strings,
|
|
|
|
),
|
|
|
|
fontsize=7,
|
|
|
|
align="bottom left",
|
|
|
|
)
|
|
|
|
|
|
|
|
def generate_sloped_wall_holes(self, side_wall_length, rad, sloped_wall_height):
|
|
|
|
cos = math.cos(rad)
|
|
|
|
tan = math.tan(rad)
|
|
|
|
sin = math.sin(rad)
|
|
|
|
posx_at_y0 = side_wall_length - sloped_wall_height * tan
|
|
|
|
posx = posx_at_y0 - cos * self.thickness / 2
|
|
|
|
posy = sin * self.thickness / 2
|
|
|
|
self.fingerHolesAt(posx, posy, sloped_wall_height, angle=90 - math.degrees(rad))
|
|
|
|
|
|
|
|
def generate_card_tray_sloped_wall_holes(
|
|
|
|
self, side_wall_length, sloped_wall_height, spacer_height, spacer_spacing, rad
|
|
|
|
):
|
|
|
|
# Spacer finger holes
|
|
|
|
self.fingerHolesAt(
|
|
|
|
side_wall_length - (spacer_spacing - self.thickness / 2),
|
|
|
|
# the sloped wall doesn't exactly touch the bottom of the spacer
|
|
|
|
-self.thickness * math.tan(rad),
|
|
|
|
spacer_height / math.cos(rad),
|
|
|
|
)
|
|
|
|
|
|
|
|
# Big hole to access "lost" space behind sloped wall
|
|
|
|
radius = 5
|
|
|
|
padding = 8
|
|
|
|
total_loss = 2 * radius + 2 * padding
|
|
|
|
self.moveTo(radius + padding, padding)
|
|
|
|
self.polyline(
|
|
|
|
side_wall_length - total_loss,
|
|
|
|
(90, radius),
|
|
|
|
sloped_wall_height - total_loss,
|
|
|
|
(90, radius),
|
|
|
|
side_wall_length - total_loss,
|
|
|
|
(90, radius),
|
|
|
|
sloped_wall_height - total_loss,
|
|
|
|
(90, radius),
|
|
|
|
)
|
|
|
|
|
|
|
|
def generate_paper_divider(self, width, height, slot_depth, spacer_spacing, move):
|
|
|
|
"""
|
|
|
|
A card separation made of paper, which moves freely in the card tray.
|
|
|
|
Takes less space and easy to manipulate, but won't block cards in place.
|
|
|
|
"""
|
|
|
|
if self.move(width, height, move, True):
|
|
|
|
return
|
|
|
|
|
|
|
|
margin = 0.5
|
|
|
|
actual_width = width - margin
|
|
|
|
self.polyline(
|
|
|
|
actual_width - spacer_spacing,
|
|
|
|
90,
|
|
|
|
height - slot_depth,
|
|
|
|
-90,
|
|
|
|
spacer_spacing,
|
|
|
|
90,
|
|
|
|
slot_depth,
|
|
|
|
90,
|
|
|
|
actual_width,
|
|
|
|
90,
|
|
|
|
height,
|
|
|
|
90,
|
|
|
|
)
|
|
|
|
|
|
|
|
# Move for next piece
|
|
|
|
self.move(width, height, move)
|
|
|
|
|
|
|
|
def generate_divider(self, width, height, slot_depth, spacer_spacing, move):
|
|
|
|
"""
|
|
|
|
A card separation made of wood which slides in the side slots.
|
|
|
|
Can be useful to do hard separations, but takes more space and
|
|
|
|
less movable than the paper ones.
|
|
|
|
"""
|
|
|
|
total_width = width + 2 * self.thickness
|
|
|
|
|
|
|
|
if self.move(total_width, height, move, True):
|
|
|
|
return
|
|
|
|
|
|
|
|
radius = 16
|
|
|
|
padding = 20
|
|
|
|
divider_notch_depth = 35
|
|
|
|
|
|
|
|
self.polyline(
|
|
|
|
self.thickness + spacer_spacing + padding - radius,
|
|
|
|
(90, radius),
|
|
|
|
divider_notch_depth - radius - radius,
|
|
|
|
(-90, radius),
|
|
|
|
width - 2 * radius - 2 * padding - spacer_spacing,
|
|
|
|
(-90, radius),
|
|
|
|
divider_notch_depth - radius - radius,
|
|
|
|
(90, radius),
|
|
|
|
self.thickness + padding - radius,
|
|
|
|
90,
|
|
|
|
slot_depth,
|
|
|
|
90,
|
|
|
|
self.thickness,
|
|
|
|
-90,
|
|
|
|
height - slot_depth,
|
|
|
|
90,
|
|
|
|
width - spacer_spacing,
|
|
|
|
90,
|
|
|
|
height - slot_depth,
|
|
|
|
-90,
|
|
|
|
self.thickness + spacer_spacing,
|
|
|
|
90,
|
|
|
|
slot_depth,
|
|
|
|
)
|
|
|
|
|
|
|
|
# Move for next piece
|
|
|
|
self.move(total_width, height, move)
|
|
|
|
|
2020-11-15 18:50:08 +01:00
|
|
|
def render_horse_box(self, player_box_height, row_width, width):
|
2020-09-19 23:47:58 +02:00
|
|
|
"""
|
|
|
|
Box for the horses on lower level. Same height as player boxes.
|
|
|
|
"""
|
|
|
|
length = 2 * row_width + 3 * self.thickness
|
|
|
|
self.render_simple_tray(width, length, player_box_height)
|
|
|
|
|
2020-11-15 18:50:08 +01:00
|
|
|
def render_moor_box(
|
|
|
|
self, bigger_box_inner_height, player_box_height, row_width, length
|
|
|
|
):
|
2020-09-19 23:47:58 +02:00
|
|
|
"""
|
|
|
|
Box for the moor/forest tiles, and the cardboard tokens with multiple
|
|
|
|
units of resources.
|
|
|
|
A corner is lowered (the one for the tokens) at the same height as player boxes
|
|
|
|
to store 2 levels of small boards there.
|
|
|
|
"""
|
|
|
|
self.ctx.save()
|
|
|
|
height = bigger_box_inner_height
|
|
|
|
lowered_height = player_box_height - self.thickness
|
|
|
|
lowered_corner_height = height - lowered_height
|
|
|
|
corner_length = 53.5
|
|
|
|
|
|
|
|
self.rectangularWall(
|
|
|
|
length,
|
|
|
|
2 * row_width + self.thickness,
|
|
|
|
"FfFf",
|
|
|
|
move="up",
|
|
|
|
callback=[
|
|
|
|
partial(
|
|
|
|
lambda: self.fingerHolesAt(
|
|
|
|
0, row_width + 0.5 * self.thickness, length, 0
|
|
|
|
)
|
|
|
|
)
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
for i in range(2):
|
|
|
|
self.rectangularWall(
|
|
|
|
length,
|
|
|
|
lowered_height,
|
|
|
|
[
|
|
|
|
"f",
|
|
|
|
"f",
|
|
|
|
MoorBoxSideEdge(
|
|
|
|
self, corner_length, lowered_corner_height, i % 2 == 0
|
|
|
|
),
|
|
|
|
"f",
|
|
|
|
],
|
|
|
|
move="up",
|
|
|
|
)
|
|
|
|
self.rectangularWall(length, height / 2, "ffef", move="up")
|
|
|
|
|
|
|
|
for i in range(2):
|
|
|
|
self.rectangularWall(
|
|
|
|
2 * row_width + self.thickness,
|
|
|
|
lowered_height,
|
|
|
|
[
|
|
|
|
"F",
|
|
|
|
"F",
|
|
|
|
MoorBoxHoleEdge(self, height, lowered_corner_height, i % 2 == 0),
|
|
|
|
"F",
|
|
|
|
],
|
|
|
|
move="up",
|
|
|
|
callback=[
|
|
|
|
partial(self.generate_side_finger_holes, row_width, height / 2)
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
self.ctx.restore()
|
|
|
|
self.rectangularWall(length, 0, "FFFF", move="right only")
|
|
|
|
|
|
|
|
def generate_side_finger_holes(self, row_width, height):
|
|
|
|
self.fingerHolesAt(row_width + 0.5 * self.thickness, 0, height)
|
|
|
|
|
|
|
|
def render_room_box(self, width, height, row_width):
|
|
|
|
"""
|
|
|
|
A box in which storing room/field tiles.
|
|
|
|
"""
|
|
|
|
border_height = 12
|
|
|
|
room_box_length = row_width * 2 + self.thickness
|
|
|
|
|
|
|
|
self.ctx.save()
|
|
|
|
|
|
|
|
self.rectangularWall(
|
|
|
|
room_box_length,
|
|
|
|
height,
|
|
|
|
"eFfF",
|
|
|
|
move="up",
|
|
|
|
callback=[partial(self.generate_side_finger_holes, row_width, height)],
|
|
|
|
)
|
|
|
|
|
|
|
|
self.rectangularWall(
|
|
|
|
room_box_length,
|
|
|
|
width,
|
|
|
|
"FFfF",
|
|
|
|
move="up",
|
|
|
|
callback=[partial(self.generate_side_finger_holes, row_width, width)],
|
|
|
|
)
|
|
|
|
|
|
|
|
self.rectangularWall(
|
|
|
|
room_box_length,
|
|
|
|
border_height,
|
|
|
|
"FFeF",
|
|
|
|
move="up",
|
|
|
|
callback=[
|
|
|
|
partial(self.generate_side_finger_holes, row_width, border_height)
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
for _ in range(3):
|
|
|
|
self.trapezoidWall(width, height, border_height, "ffef", move="up")
|
|
|
|
|
|
|
|
self.ctx.restore()
|
|
|
|
|
|
|
|
self.rectangularWall(room_box_length, 0, "FFFF", move="right only")
|
|
|
|
|
2020-11-15 18:50:08 +01:00
|
|
|
def render_player_box(self, player_box_height, player_box_inner_width):
|
2020-09-19 23:47:58 +02:00
|
|
|
"""
|
|
|
|
A box in which storing all the bits of a single player,
|
|
|
|
including (and designed for) the cardboard bed from Farmers Of The Moor.
|
|
|
|
"""
|
|
|
|
self.ctx.save()
|
|
|
|
bed_inner_height = player_box_height - self.thickness
|
|
|
|
bed_inner_length = 66.75
|
2020-11-15 18:50:08 +01:00
|
|
|
bed_inner_width = player_box_inner_width
|
2020-09-20 10:16:31 +02:00
|
|
|
cardboard_bed_foot_height = 6.5
|
|
|
|
cardboard_bed_hole_margin = 5
|
|
|
|
cardboard_bed_hole_length = 12
|
|
|
|
bed_head_length = 20
|
|
|
|
bed_foot_height = 18
|
|
|
|
support_length = 38
|
2020-09-19 23:47:58 +02:00
|
|
|
|
2020-09-20 10:16:31 +02:00
|
|
|
bed_edge = Bed2SidesEdge(
|
|
|
|
self, bed_inner_length, bed_head_length, bed_foot_height
|
|
|
|
)
|
2020-09-19 23:47:58 +02:00
|
|
|
noop_edge = NoopEdge(self)
|
2020-09-20 10:16:31 +02:00
|
|
|
self.ctx.save()
|
|
|
|
optim_180_x = (
|
|
|
|
bed_inner_length + self.thickness + bed_head_length + 2 * self.spacing
|
|
|
|
)
|
|
|
|
optim_180_y = 2 * bed_foot_height - player_box_height + 2 * self.spacing
|
2020-09-19 23:47:58 +02:00
|
|
|
for _ in range(2):
|
|
|
|
self.rectangularWall(
|
|
|
|
bed_inner_length,
|
|
|
|
bed_inner_height,
|
|
|
|
["F", bed_edge, noop_edge, "F"],
|
|
|
|
move="up",
|
|
|
|
)
|
2020-09-20 10:16:31 +02:00
|
|
|
self.moveTo(optim_180_x, optim_180_y, -180)
|
|
|
|
self.ctx.restore()
|
|
|
|
self.moveTo(0, bed_inner_height + self.thickness + self.spacing + optim_180_y)
|
2020-09-19 23:47:58 +02:00
|
|
|
|
|
|
|
self.rectangularWall(
|
|
|
|
bed_inner_length,
|
|
|
|
bed_inner_width,
|
|
|
|
"feff",
|
|
|
|
move="up",
|
|
|
|
callback=[
|
|
|
|
partial(
|
|
|
|
self.generate_bed_holes,
|
|
|
|
bed_inner_width,
|
2020-09-20 10:16:31 +02:00
|
|
|
cardboard_bed_hole_margin,
|
|
|
|
cardboard_bed_hole_length,
|
2020-09-19 23:47:58 +02:00
|
|
|
support_length,
|
|
|
|
)
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
self.ctx.save()
|
|
|
|
self.rectangularWall(
|
|
|
|
bed_inner_width,
|
|
|
|
bed_inner_height,
|
|
|
|
["F", "f", BedHeadEdge(self, bed_inner_height - 15), "f"],
|
|
|
|
move="right",
|
|
|
|
)
|
|
|
|
for _ in range(2):
|
|
|
|
self.rectangularWall(
|
2020-09-20 10:16:31 +02:00
|
|
|
cardboard_bed_foot_height - self.thickness,
|
2020-09-19 23:47:58 +02:00
|
|
|
support_length,
|
|
|
|
"efee",
|
|
|
|
move="right",
|
|
|
|
)
|
|
|
|
self.ctx.restore()
|
|
|
|
self.rectangularWall(
|
|
|
|
bed_inner_width,
|
|
|
|
bed_inner_height,
|
|
|
|
"Ffef",
|
|
|
|
move="up only",
|
|
|
|
)
|
|
|
|
|
|
|
|
self.ctx.restore()
|
|
|
|
self.rectangularWall(
|
2020-09-20 10:16:31 +02:00
|
|
|
bed_inner_length + bed_head_length + self.spacing - self.thickness,
|
2020-09-19 23:47:58 +02:00
|
|
|
0,
|
|
|
|
"FFFF",
|
|
|
|
move="right only",
|
|
|
|
)
|
|
|
|
|
|
|
|
def generate_bed_holes(self, width, margin, hole_length, support_length):
|
|
|
|
support_start = margin + hole_length
|
|
|
|
|
|
|
|
bed_width = 29.5
|
|
|
|
bed_space_to_wall = (width - bed_width) / 2
|
|
|
|
bed_feet_width = 3
|
|
|
|
|
|
|
|
posy_1 = bed_space_to_wall
|
|
|
|
posy_2 = width - bed_space_to_wall
|
|
|
|
|
|
|
|
for y, direction in [(posy_1, 1), (posy_2, -1)]:
|
|
|
|
bed_feet_middle_y = y + direction * bed_feet_width / 2
|
|
|
|
support_middle_y = y + direction * self.thickness / 2
|
|
|
|
self.rectangularHole(
|
|
|
|
margin,
|
|
|
|
bed_feet_middle_y,
|
|
|
|
hole_length,
|
|
|
|
bed_feet_width,
|
|
|
|
center_x=False,
|
|
|
|
)
|
|
|
|
self.fingerHolesAt(support_start, support_middle_y, support_length, angle=0)
|
|
|
|
self.rectangularHole(
|
|
|
|
support_start + support_length,
|
|
|
|
bed_feet_middle_y,
|
|
|
|
hole_length,
|
|
|
|
bed_feet_width,
|
|
|
|
center_x=False,
|
|
|
|
)
|
|
|
|
|
|
|
|
def render_upper_token_trays(self, tray_inner_height, box_width):
|
|
|
|
"""
|
|
|
|
Upper level : multiple trays for each ressource
|
|
|
|
(beside horses which are on the lower level)
|
|
|
|
"""
|
|
|
|
tray_height = tray_inner_height + self.thickness
|
|
|
|
upper_level_width = 196
|
|
|
|
upper_level_length = box_width
|
|
|
|
row_width = upper_level_width / 3
|
|
|
|
|
|
|
|
# Stone / Vegetable / Pig / Cow
|
|
|
|
self.render_simple_tray(row_width, upper_level_length, tray_height, 3)
|
|
|
|
# Reed / Grain / Sheep
|
|
|
|
self.render_simple_tray(row_width, upper_level_length * 2 / 3, tray_height, 2)
|
|
|
|
# Wood / Clay
|
|
|
|
self.render_simple_tray(row_width, upper_level_length * 2 / 3, tray_height, 1)
|
|
|
|
# Food / Fire
|
|
|
|
self.render_simple_tray(upper_level_length / 3, row_width * 2, tray_height, 1)
|
|
|
|
|
|
|
|
def render_simple_tray(self, outer_width, outer_length, outer_height, dividers=0):
|
|
|
|
"""
|
|
|
|
One of the upper level trays, with movable dividers.
|
|
|
|
"""
|
|
|
|
width = outer_width - 2 * self.thickness
|
|
|
|
length = outer_length - 2 * self.thickness
|
|
|
|
height = outer_height - self.thickness
|
|
|
|
self.ctx.save()
|
|
|
|
self.rectangularWall(width, length, "FFFF", move="up")
|
|
|
|
for _ in range(2):
|
|
|
|
self.rectangularWall(width, height, "ffef", move="up")
|
|
|
|
self.ctx.restore()
|
|
|
|
self.rectangularWall(width, length, "FFFF", move="right only")
|
|
|
|
for _ in range(2):
|
|
|
|
self.rectangularWall(height, length, "FfFe", move="right")
|
|
|
|
|
|
|
|
if dividers:
|
|
|
|
self.ctx.save()
|
|
|
|
for _ in range(dividers):
|
|
|
|
self.render_simple_tray_divider(width, height, "up")
|
|
|
|
self.ctx.restore()
|
|
|
|
self.render_simple_tray_divider(width, height, "right only")
|
|
|
|
|
|
|
|
def render_simple_tray_divider(self, width, height, move):
|
|
|
|
"""
|
|
|
|
Simple movable divider. A wall with small feet for a little more stability.
|
|
|
|
"""
|
|
|
|
|
|
|
|
if self.move(height, width, move, True):
|
|
|
|
return
|
|
|
|
|
|
|
|
t = self.thickness
|
|
|
|
self.polyline(
|
|
|
|
height - t,
|
|
|
|
90,
|
|
|
|
t,
|
|
|
|
-90,
|
|
|
|
t,
|
|
|
|
90,
|
|
|
|
width - 2 * t,
|
|
|
|
90,
|
|
|
|
t,
|
|
|
|
-90,
|
|
|
|
t,
|
|
|
|
90,
|
|
|
|
height - t,
|
|
|
|
90,
|
|
|
|
width,
|
|
|
|
90,
|
|
|
|
)
|
|
|
|
|
|
|
|
self.move(height, width, move)
|
|
|
|
|
|
|
|
self.render_simple_tray_divider_feet(width, height, move)
|
|
|
|
|
|
|
|
def render_simple_tray_divider_feet(self, width, height, move):
|
|
|
|
sqr2 = math.sqrt(2)
|
|
|
|
t = self.thickness
|
|
|
|
divider_foot_width = 2 * t
|
|
|
|
full_width = t + 2 * divider_foot_width
|
|
|
|
move_length = self.spacing + full_width / sqr2
|
|
|
|
move_width = self.spacing + max(full_width, height)
|
|
|
|
|
|
|
|
if self.move(move_width, move_length, move, True):
|
|
|
|
return
|
|
|
|
|
|
|
|
self.ctx.save()
|
|
|
|
self.polyline(
|
|
|
|
sqr2 * divider_foot_width,
|
|
|
|
135,
|
|
|
|
t,
|
|
|
|
-90,
|
|
|
|
t,
|
|
|
|
-90,
|
|
|
|
t,
|
|
|
|
135,
|
|
|
|
sqr2 * divider_foot_width,
|
|
|
|
135,
|
|
|
|
full_width,
|
|
|
|
135,
|
|
|
|
)
|
|
|
|
self.ctx.restore()
|
|
|
|
|
|
|
|
self.moveTo(-self.burn / sqr2, self.burn * (1 + 1 / sqr2), 45)
|
|
|
|
self.moveTo(full_width)
|
|
|
|
|
|
|
|
self.polyline(
|
|
|
|
0,
|
|
|
|
135,
|
|
|
|
sqr2 * divider_foot_width,
|
|
|
|
135,
|
|
|
|
t,
|
|
|
|
-90,
|
|
|
|
t,
|
|
|
|
-90,
|
|
|
|
t,
|
|
|
|
135,
|
|
|
|
sqr2 * divider_foot_width,
|
|
|
|
135,
|
|
|
|
)
|
|
|
|
|
|
|
|
self.move(move_width, move_length, move)
|
|
|
|
|
|
|
|
|
|
|
|
class MoorBoxSideEdge(edges.BaseEdge):
|
|
|
|
"""
|
|
|
|
Edge for the sides of the moor tiles box
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, boxes, corner_length, corner_height, lower_corner):
|
|
|
|
super(MoorBoxSideEdge, self).__init__(boxes, None)
|
|
|
|
self.corner_height = corner_height
|
|
|
|
self.lower_corner = lower_corner
|
|
|
|
self.corner_length = corner_length
|
|
|
|
|
|
|
|
def __call__(self, length, **kw):
|
|
|
|
radius = self.corner_height / 2
|
|
|
|
if self.lower_corner:
|
|
|
|
self.polyline(
|
|
|
|
length - self.corner_height - self.corner_length,
|
|
|
|
(90, radius),
|
|
|
|
0,
|
|
|
|
(-90, radius),
|
|
|
|
self.corner_length,
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
self.polyline(length)
|
|
|
|
|
|
|
|
def startwidth(self):
|
|
|
|
return self.corner_height
|
|
|
|
|
|
|
|
def endwidth(self):
|
|
|
|
return 0 if self.lower_corner else self.corner_height
|
|
|
|
|
|
|
|
|
|
|
|
class MoorBoxHoleEdge(edges.BaseEdge):
|
|
|
|
"""
|
|
|
|
Edge which does the notches for the moor tiles box
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, boxes, height, corner_height, lower_corner):
|
|
|
|
super(MoorBoxHoleEdge, self).__init__(boxes, None)
|
|
|
|
self.height = height
|
|
|
|
self.corner_height = corner_height
|
|
|
|
self.lower_corner = lower_corner
|
|
|
|
|
|
|
|
def __call__(self, length, **kw):
|
|
|
|
one_side_width = (length - self.thickness) / 2
|
|
|
|
notch_width = 20
|
|
|
|
radius = 6
|
|
|
|
upper_edge = (one_side_width - notch_width - 2 * radius) / 2
|
|
|
|
hole_start = 10
|
|
|
|
lowered_hole_start = 2
|
|
|
|
hole_depth = self.height - 2 * radius
|
|
|
|
lower_edge = notch_width - 2 * radius
|
|
|
|
|
|
|
|
one_side_polyline = lambda margin1, margin2: [
|
|
|
|
upper_edge,
|
|
|
|
(90, radius),
|
|
|
|
hole_depth - margin1,
|
|
|
|
(-90, radius),
|
|
|
|
lower_edge,
|
|
|
|
(-90, radius),
|
|
|
|
hole_depth - margin2,
|
|
|
|
(90, radius),
|
|
|
|
upper_edge,
|
|
|
|
]
|
|
|
|
|
|
|
|
normal_side_polyline = one_side_polyline(hole_start, hole_start)
|
|
|
|
corner_side_polyline = one_side_polyline(
|
|
|
|
lowered_hole_start, lowered_hole_start + self.corner_height
|
|
|
|
)
|
|
|
|
|
|
|
|
full_polyline = (
|
|
|
|
normal_side_polyline
|
|
|
|
+ [0, self.thickness, 0]
|
|
|
|
+ (corner_side_polyline if self.lower_corner else normal_side_polyline)
|
|
|
|
)
|
|
|
|
self.polyline(*full_polyline)
|
|
|
|
|
|
|
|
def startwidth(self):
|
|
|
|
return self.corner_height
|
|
|
|
|
|
|
|
def endwidth(self):
|
|
|
|
return 0 if self.lower_corner else self.corner_height
|
|
|
|
|
|
|
|
|
|
|
|
class BedHeadEdge(edges.BaseEdge):
|
|
|
|
"""
|
|
|
|
Edge which does the head side of the Agricola player box
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, boxes, hole_depth):
|
|
|
|
super(BedHeadEdge, self).__init__(boxes, None)
|
|
|
|
self.hole_depth = hole_depth
|
|
|
|
|
|
|
|
def __call__(self, length, **kw):
|
|
|
|
hole_length = 16
|
|
|
|
upper_corner = 10
|
|
|
|
lower_corner = 6
|
|
|
|
depth = self.hole_depth - upper_corner - lower_corner
|
|
|
|
upper_edge = (length - hole_length - 2 * upper_corner) / 2
|
|
|
|
lower_edge = hole_length - 2 * lower_corner
|
|
|
|
|
|
|
|
self.polyline(
|
|
|
|
upper_edge,
|
|
|
|
(90, upper_corner),
|
|
|
|
depth,
|
|
|
|
(-90, lower_corner),
|
|
|
|
lower_edge,
|
|
|
|
(-90, lower_corner),
|
|
|
|
depth,
|
|
|
|
(90, upper_corner),
|
|
|
|
upper_edge,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class Bed2SidesEdge(edges.BaseEdge):
|
|
|
|
"""
|
|
|
|
Edge which does a bed like shape, skipping the next corner.
|
|
|
|
The next edge should be a NoopEdge
|
|
|
|
"""
|
|
|
|
|
2020-09-20 10:16:31 +02:00
|
|
|
def __init__(self, boxes, bed_length, full_head_length, full_foot_height):
|
2020-09-19 23:47:58 +02:00
|
|
|
super(Bed2SidesEdge, self).__init__(boxes, None)
|
|
|
|
self.bed_length = bed_length
|
2020-09-20 10:16:31 +02:00
|
|
|
self.full_head_length = full_head_length
|
|
|
|
self.full_foot_height = full_foot_height
|
2020-09-19 23:47:58 +02:00
|
|
|
|
|
|
|
def __call__(self, bed_height, **kw):
|
|
|
|
foot_corner = 6
|
|
|
|
middle_corner = 3
|
|
|
|
head_corner = 10
|
2020-09-20 10:16:31 +02:00
|
|
|
foot_height = self.full_foot_height - self.thickness - foot_corner
|
|
|
|
head_length = self.full_head_length - head_corner - self.thickness
|
2020-09-19 23:47:58 +02:00
|
|
|
corners = foot_corner + middle_corner + head_corner
|
|
|
|
head_height = bed_height - foot_height - corners
|
|
|
|
middle_length = self.bed_length - head_length - corners
|
|
|
|
|
|
|
|
self.polyline(
|
|
|
|
foot_height,
|
|
|
|
(90, foot_corner),
|
|
|
|
middle_length,
|
|
|
|
(-90, middle_corner),
|
|
|
|
head_height,
|
|
|
|
(90, head_corner),
|
|
|
|
head_length,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class NoopEdge(edges.BaseEdge):
|
|
|
|
"""
|
|
|
|
Edge which does nothing, not even turn or move.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, boxes):
|
|
|
|
super(NoopEdge, self).__init__(boxes, None)
|
|
|
|
|
|
|
|
def __call__(self, length, **kw):
|
|
|
|
# cancel turn
|
|
|
|
self.corner(-90)
|