Browse Source

Merge branch 'sksp2022-strategicka' of gitea.ks.matfyz.cz:KSP/ksp into sksp2022-strategicka

master
David Klement 2 years ago
parent
commit
e4fc5b4a5b
  1. 12
      server/bin/create_game
  2. 5
      server/hra/game.py
  3. 2
      server/hra/web/__init__.py
  4. 137
      server/hra/web/game.py
  5. 16
      server/hra/web/html.py
  6. 104
      server/hra/web/pages.py
  7. 20
      server/static/ksp-mhd.css
  8. 8
      server/static/occupy.css

12
server/bin/create_game

@ -12,14 +12,14 @@ ses = db.get_session()
name="Hlavní hra" name="Hlavní hra"
mode = "occupy" mode = "occupy"
teams_count = 16 teams_count = 12
configuration = { configuration = {
"teams_width": 4, "teams_width": 4,
"teams_height": 4, "teams_height": 3,
"width_per_team": 30, "width_per_team": 24,
"height_per_team": 30, "height_per_team": 24,
"born_per_round": [1], # "born_per_round": [1],
"initial_remaining_rounds": 1000, "initial_remaining_rounds": 540,
"spawn_price": 10, "spawn_price": 10,
"last_spawn": 100, "last_spawn": 100,
# seed=5 # seed=5

5
server/hra/game.py

@ -27,6 +27,7 @@ class Logic:
@add_logic @add_logic
class Occupy(Logic): class Occupy(Logic):
MOVE_VECTORS = { MOVE_VECTORS = {
None: (0, 0),
"stay": (0, 0), "stay": (0, 0),
"left": (0, -1), "left": (0, -1),
"right": (0, 1), "right": (0, 1),
@ -60,8 +61,10 @@ class Occupy(Logic):
"remaining_rounds": 1 "remaining_rounds": 1
} }
# zero_state spawnuje jednoho vojáka, poslední (použitelný, takže
# předposlední) spawn umožňuje budget na `last_spawn` nových vojáků
def budget_for_round(self, round_id): def budget_for_round(self, round_id):
return (self.last_spawn * self.spawn_price * (round_id + 1)**2) // (self.rounds_total**2) return self.spawn_price + ((round_id * self.spawn_price * self.last_spawn) // self.rounds_total)
# Počáteční stav # Počáteční stav
def zero_state(self) -> Any: def zero_state(self) -> Any:

2
server/hra/web/__init__.py

@ -83,7 +83,7 @@ def init_request():
g.menu += [ g.menu += [
MenuItem(app.url_for(pages.web_org_games.__name__), "Hry"), MenuItem(app.url_for(pages.web_org_games.__name__), "Hry"),
MenuItem(app.url_for(pages.web_org_users.__name__), "Uživatelé"), MenuItem(app.url_for(pages.web_org_users.__name__), "Uživatelé"),
MenuItem(app.url_for(pages.web_org_logs.__name__), "Logy"), MenuItem(app.url_for(pages.web_org_logs.__name__)+"#main_table" , "Logy"),
] ]
else: else:
g.menu += [ g.menu += [

137
server/hra/web/game.py

@ -1,5 +1,5 @@
from flask import Flask, redirect, flash, session, g, request, get_flashed_messages, Markup from flask import Flask, redirect, flash, session, g, request, get_flashed_messages, Markup
from wtforms import Form, BooleanField, StringField, PasswordField, validators, SubmitField, IntegerField, DateTimeField from wtforms import Form, BooleanField, StringField, PasswordField, validators, SubmitField, IntegerField, DateTimeField, RadioField
from wtforms.validators import ValidationError from wtforms.validators import ValidationError
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask_bootstrap import Bootstrap from flask_bootstrap import Bootstrap
@ -11,6 +11,7 @@ import wtforms
from wtforms.fields import EmailField from wtforms.fields import EmailField
from wtforms.widgets import NumberInput from wtforms.widgets import NumberInput
from typing import Optional, Any, List from typing import Optional, Any, List
from datetime import datetime
import hra.config as config import hra.config as config
import hra.web.html as html import hra.web.html as html
@ -18,7 +19,7 @@ import hra.db as db
from hra.web import app from hra.web import app
import hra.web.jinja_mac as jinja_mac import hra.web.jinja_mac as jinja_mac
from hra.util import hash_passwd from hra.util import hash_passwd
from hra.web.pages import BasePage from hra.web.pages import BasePage, web_game_view
wlogic_by_mode = {} wlogic_by_mode = {}
@ -39,17 +40,49 @@ class WLogic:
self.game = game self.game = game
self.logic = game.get_logic() self.logic = game.get_logic()
class OccupyViewConfig(FlaskForm):
refresh_none = "Bez automatické aktualizace"
refresh_meta = "Aktualizace pomocí HTML META"
refresh_js = "Aktualizace pomocí java scriptu"
font_size = IntegerField("Velikost fontu", default=8)
refresh = RadioField("Aktualizace", choices=(refresh_none, refresh_meta), default=refresh_none)
not_clickable = BooleanField("Vypnout podrobnosti kliknutím (zmenší nároky na sít a procesor)")
big_fields = BooleanField("Široká políčka (trojciferné počty)")
submit = SubmitField("Refresh/Potvrdit")
@add_wlogic @add_wlogic
class Occupy(WLogic): class Occupy(WLogic):
def view(self, state: db.State, team: Optional[db.Team], teams: List[db.Team]): def view(self, state: db.State, team: Optional[db.Team], teams: List[db.Team]):
meta_refresh_without_reaming = 15
game = state.game
if game.step_mode == db.StepMode.automatic:
time_reaming = (state.create_time - datetime.now()).total_seconds() + game.step_every_s
else:
time_reaming = None
s = state.get_state() s = state.get_state()
if team is not None: conff = OccupyViewConfig(formdata=request.args, csrf_enabled=False)
s = self.logic.personalize_state(s, team.team_id, state.round) q = db.get_session().query(db.Log).order_by(db.Log.log_id.desc())
conff.validate()
max_num = 999 if conff.big_fields.data else 99
b = BasePage() b = BasePage()
b.h2(f"Hra {self.game.print()} kolo {state.round}") b.h2(f"Hra {self.game.print()} kolo {state.round}")
b.p().b(_class=f"game_team_{team.team_id}")(f"Pohled týmu {team.print()}") if not conff.not_clickable.data:
with b.table(_class="game_table"): b.p("Po kliknutí na buňku se zobrazí další informace.")
if conff.refresh.data == conff.refresh_meta and time_reaming is None:
b.p(f"Není známo, kdy nastane další kolo, proto může být aktualizace až o {meta_refresh_without_reaming} sekund opožděna.")
with b.p():
b.line().b(_class=f"game_team_{team.team_id}")(f"Pohled týmu {team.print()}")
with b.line().b(_class="pull-right"):
if time_reaming is not None:
b("Čas do konce kola: ", b._span(id="time")(int(time_reaming)), " sekund")
else:
b("Konec kola není stanoven")
with b.p(style="text-align:center;").table(_class="game_table"):
for i, row in enumerate(s["world"]): for i, row in enumerate(s["world"]):
with b.tr(): with b.tr():
for j, x in enumerate(row): for j, x in enumerate(row):
@ -58,7 +91,7 @@ class Occupy(WLogic):
members = x["members"] members = x["members"]
with b.td(): with b.td():
classes = [] classes = []
with b.a(href=f"#cell_{i}_{j}"): with b.a(href=f"#cell_{i}_{j}") if not conff.not_clickable.data else b.bucket():
if x["protected_for_team"] is not None: if x["protected_for_team"] is not None:
classes.append("game_protected") classes.append("game_protected")
if x["hill"]: if x["hill"]:
@ -70,38 +103,80 @@ class Occupy(WLogic):
if occupied_by_team is not None: if occupied_by_team is not None:
classes.append(f'game_occupied') classes.append(f'game_occupied')
classes.append(f'game_occupied_by_{occupied_by_team}') classes.append(f'game_occupied_by_{occupied_by_team}')
if len(members): num = len(members)
b(len(members)) if num:
b(num if num <= max_num else ("+++" if conff.big_fields.data else "++"))
else: else:
b(Markup("&nbsp;")) b(Markup("&nbsp;"))
b(_class=" ".join(classes)) b(_class=" ".join(classes))
for i, row in enumerate(s["world"]): if not conff.not_clickable.data:
for j, x in enumerate(row): for i, row in enumerate(s["world"]):
occupied_by_team = x["occupied_by_team"] for j, x in enumerate(row):
protected_for_team = x["protected_for_team"] occupied_by_team = x["occupied_by_team"]
home_for_team = x["home_for_team"] protected_for_team = x["protected_for_team"]
members = x["members"] home_for_team = x["home_for_team"]
with b.div(id=f"cell_{i}_{j}", _class="game_tab"): members = x["members"]
b.h4(f"Políčko {i} {j}") with b.div(id=f"cell_{i}_{j}", _class="game_tab form-frame"):
if x["hill"]: b.h4(f"Políčko {i} {j}", b._a(href=f"#", _class="btn btn-primary pull-right")("Skrýt"))
b.p().b("Pohoří") if x["hill"]:
else: b.p().b("Pohoří")
if occupied_by_team is not None: else:
b.p(_class=f"game_team_{occupied_by_team}").b(f"Obsazeno týmem: {teams[occupied_by_team].print()}") if occupied_by_team is not None:
if protected_for_team is not None: b.p(_class=f"game_team_{occupied_by_team}").b(f"Obsazeno týmem: {teams[occupied_by_team].print()}")
b.p(_class=f"game_team_{protected_for_team}").b(f"Ochranné území týmu: {teams[protected_for_team].print()}") if protected_for_team is not None:
if home_for_team is not None: b.p(_class=f"game_team_{protected_for_team}").b(f"Ochranné území týmu: {teams[protected_for_team].print()}")
b.p(_class=f"game_team_{home_for_team}").b(f"Domov týmu: {teams[home_for_team].print()}") if home_for_team is not None:
b.p().b(f"Počet osob: {len(members)}") b.p(_class=f"game_team_{home_for_team}").b(f"Domov týmu: {teams[home_for_team].print()}")
with b.ul(): b.p().b(f"Počet osob: {len(members)}")
for m in members: with b.ul():
b.li(_class=f"game_team_{home_for_team}")(f"Voják {m['id']} týmu {teams[m['team']].print()}") for m in members:
b.li(_class=f"game_team_{home_for_team}")(f"Voják {m['id']} týmu {teams[m['team']].print()}")
del conff.csrf_token
b.p()
b.div(_class="form-frame")(jinja_mac.quick_form(conff, form_type="horizontal", method="GET"))
b.script(Markup(f"""
var intervalID = window.setInterval(update_time, 100);
var t = { time_reaming*1000 };
function update_time() {{
document.getElementById("time").innerHTML = Math.floor(t/1000);;
t -= 100;
}}
"""))
def head(x):
refresh_time = time_reaming + 1 if time_reaming is not None else 15
font_size = int(conff.font_size.data)
box_size = int(font_size * 1.3 + 1)
box_w = box_size
if conff.big_fields.data:
box_w *= 1.5
table_w = len(s["world"][0]) * box_w
margin_cmd = ""
if table_w > 1000:
margin_cmd = f"margin-left: {(1000-table_w) / 2}px;"
if conff.refresh.data == conff.refresh_meta:
b.meta(**{"HTTP-EQUIV": "refresh", "CONTENT": f"{refresh_time};{app.url_for(web_game_view.__name__, game_id=game.game_id, team_id=team.team_id, **request.args)}"})
b.link(rel="stylesheet", href=app.url_for('static', filename='occupy.css'), type='text/css', media="all")
b.style(Markup(f"""
table.game_table tr td {{
width: {box_w}px;
max-width: {box_w}px;
height: {box_size}px;
font-size: {font_size}px;
}}
table.game_table {{
width: {table_w}px;
{margin_cmd}
}}
"""))
b.wrap( b.wrap(
limited_size=False, limited_size=False,
sticky_head=False, sticky_head=False,
head=lambda x:x.link(rel="stylesheet", href=app.url_for('static', filename='occupy.css'), type='text/css', media="all") head=head,
) )
return b.print_file() return b.print_file()

16
server/hra/web/html.py

@ -11,7 +11,7 @@ class EscapeError(RuntimeError):
def escape_attribute(x:str) -> str: def escape_attribute(x:str) -> str:
return x.replace("&", "&ampersand;").replace('"', "&quot;") return str(x).replace("&", "&amp;").replace('"', "&quot;")
def escape_attribute_name(x:str) -> str: def escape_attribute_name(x:str) -> str:
@ -72,6 +72,14 @@ class Bucket:
def _line(self): def _line(self):
return Line(self) return Line(self)
def bucket(self):
t = Bucket(self.builder)
self.add(t)
return t
def _bucket(self):
return Bucket(self)
def print(self): def print(self):
out = [] out = []
self.serialize_append_to_list(out, 0) self.serialize_append_to_list(out, 0)
@ -160,6 +168,12 @@ class Builder:
def _line(self): def _line(self):
return Line(self) return Line(self)
def bucket(self):
return self.current_tag.bucket()
def _bucket(self):
return Bucket(self)
def __call__(self, *arg, **kvarg): def __call__(self, *arg, **kvarg):
self.current_tag(*arg, **kvarg) self.current_tag(*arg, **kvarg)
return self return self

104
server/hra/web/pages.py

@ -1,5 +1,5 @@
from flask import Flask, redirect, flash, session, g, request, get_flashed_messages from flask import Flask, redirect, flash, session, g, request, get_flashed_messages
from wtforms import Form, BooleanField, StringField, PasswordField, validators, SubmitField, IntegerField, DateTimeField from wtforms import Form, BooleanField, StringField, PasswordField, validators, SubmitField, IntegerField, DateTimeField, SelectMultipleField
from wtforms.validators import ValidationError from wtforms.validators import ValidationError
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask_bootstrap import Bootstrap from flask_bootstrap import Bootstrap
@ -84,6 +84,12 @@ def user_link(user):
return b.root_tag return b.root_tag
def ip_link(ip):
b = html.Builder()
with b.line():
b.a(href=app.url_for(web_org_logs.__name__, ip=ip)+"#main_table")(ip)
return b.root_tag
def right_for_game(game): def right_for_game(game):
if g.org: if g.org:
return True return True
@ -261,14 +267,16 @@ def web_game(game_id):
with b.div(_class="btn-group", role="group"): with b.div(_class="btn-group", role="group"):
if right_for_team(team): if right_for_team(team):
b.a(_class="btn btn-xs btn-primary", href=app.url_for(web_game_view.__name__, game_id=game.game_id, team_id=team.team_id))("Zobrazit hru") b.a(_class="btn btn-xs btn-primary", href=app.url_for(web_game_view.__name__, game_id=game.game_id, team_id=team.team_id))("Zobrazit hru")
b.a(_class="btn btn-xs btn-default", href=app.url_for(web_org_logs.__name__, game_id=game_id, team_id=team.team_id)+"#main_table")("Log")
if g.org: if g.org:
b.a(_class="btn btn-xs btn-default", href=app.url_for(web_org_game_userchange.__name__, game_id=game.game_id, team_id=team.team_id))("Změnit uživatele") b.a(_class="btn btn-xs btn-default", href=app.url_for(web_org_game_userchange.__name__, game_id=game.game_id, team_id=team.team_id))("Změnit uživatele")
with b.div(_class="btn-gruser_idoup", role="group"): with b.div(_class="btn-group", role="group"):
if right_for_step(game): if right_for_step(game):
with b.form(method="POST", _class="btn-group", action=app.url_for(web_game_step.__name__, game_id=game_id)): with b.form(method="POST", _class="btn-group", action=app.url_for(web_game_step.__name__, game_id=game_id)):
b.button(_class="btn btn-primary", type="submit", name="su", value="yes")("Krok") b.button(_class="btn btn-primary", type="submit", name="su", value="yes")("Krok")
if g.org: if g.org:
b.a(_class="btn btn-primary", href=app.url_for(web_org_logs.__name__, game_id=game_id)+"#main_table")("Log")
b.a(_class="btn btn-default", href=app.url_for(web_org_game_round_inspect.__name__, game_id=game_id))("Inspekce kola") b.a(_class="btn btn-default", href=app.url_for(web_org_game_round_inspect.__name__, game_id=game_id))("Inspekce kola")
if g.org: if g.org:
@ -379,13 +387,18 @@ def web_org_game_round_inspect(game_id, round_id=None):
b.line().th()("Bodů") b.line().th()("Bodů")
b.line().th()("Stažení") b.line().th()("Stažení")
b.line().th()("Nahrání") b.line().th()("Nahrání")
b.line().th()("Akce")
for team, move in zip(teams, moves): for team, move in zip(teams, moves):
with b.tr(): with b.tr():
b.line().td()(team.team_id) b.line().td()(team.team_id)
b.line().td()(user_link(team.user),": ", team.name) b.line().td()(user_link(team.user),": ", team.name)
b.line().td()(move.points) b.line().td()(move.points)
b.line().td()(move.reads_count) b.line().td()(move.reads_count)
b.line().td()(f"{move.ok_pushs_count} {move.warnings_pushs_count} {move.err_pushs_count}") b.line().td()(f"ok: {move.ok_pushs_count} w: {move.warnings_pushs_count} e: {move.err_pushs_count}")
with b.td():
with b.div(_class="btn-group", role="group"):
if right_for_team(team):
b.a(_class="btn btn-xs btn-primary", href=app.url_for(web_org_logs.__name__, game_id=game_id, team_id=team.team_id, round_id=round_id)+"#main_table")("Log")
return b.print_file() return b.print_file()
@ -482,6 +495,7 @@ def web_org_user(user_id):
b.line().h2("Uživatel ", user.print()) b.line().h2("Uživatel ", user.print())
with b.div(_class="btn-group", role="group"): with b.div(_class="btn-group", role="group"):
with b.form(method="POST", _class="btn-group", action=app.url_for(web_org_user_su.__name__, user_id=user.id)): with b.form(method="POST", _class="btn-group", action=app.url_for(web_org_user_su.__name__, user_id=user.id)):
b.a(_class="btn btn-primary", href=app.url_for(web_org_logs.__name__, user_id=user.id)+"#main_table")("Log")
b.button(_class="btn btn-default", type="submit", name="su", value="yes")("Převtělit") b.button(_class="btn btn-default", type="submit", name="su", value="yes")("Převtělit")
b.h3("Týmy") b.h3("Týmy")
with b.p().table(_class="data full"): with b.p().table(_class="data full"):
@ -503,8 +517,8 @@ def web_org_user_su(user_id):
flash("Uživatel vtělen!") flash("Uživatel vtělen!")
return redirect('/') return redirect('/')
@app.route("/org/log/<int:log_id>", methods=['GET']) @app.route("/org/log/<int:log_id>/data", methods=['GET'])
def web_org_log(log_id): def web_org_log_data(log_id):
l = db.get_session().query(db.Log).filter_by(log_id=log_id).one_or_none() l = db.get_session().query(db.Log).filter_by(log_id=log_id).one_or_none()
if l is None: if l is None:
raise werkzeug.exceptions.NotFound() raise werkzeug.exceptions.NotFound()
@ -513,31 +527,74 @@ def web_org_log(log_id):
b.p().pre(pprint.pformat(l.get_data())) b.p().pre(pprint.pformat(l.get_data()))
return b.print_file() return b.print_file()
class LogsFindForm(FlaskForm):
username = StringField('Jméno uživatele')
user_id = OptionalIntField('ID uživatele')
game_id = OptionalIntField('ID hry')
team_id = OptionalIntField('Tým')
round_id = OptionalIntField('Kolo')
limit = OptionalIntField('Limit')
ip = StringField('IP')
status = SelectMultipleField('Status', choices=[("", "*")]+[(x,x) for x in ["ok", "warning", "too_early", "too_late", "error", "http-error"]], render_kw={"size":7})
endpoint = SelectMultipleField('Stránky', choices=db.Endpoint.choices())
submit = SubmitField("Najít")
def validate_captcha(form, field):
if field.data != config.CAPTCHA:
raise ValidationError("Chyba!")
@app.route("/org/logs", methods=['GET']) @app.route("/org/logs", methods=['GET'])
def web_org_logs(): def web_org_logs():
logs = db.get_session().query(db.Log).order_by(db.Log.log_id.desc()).all() f = LogsFindForm(formdata=request.args, csrf_enabled=False)
q = db.get_session().query(db.Log).order_by(db.Log.log_id.desc())
if request.args:
f.validate()
if f.username.data:
q = q.filter(db.User.username.ilike(f"%{f.username.data}%"))
if f.user_id.data is not None:
q = q.filter_by(user_id=f.user_id.data or None)
if f.game_id.data is not None:
q = q.filter_by(game_id=f.game_id.data or None)
if f.team_id.data is not None:
q = q.filter_by(team_id=f.team_id.data or None)
if f.round_id.data is not None:
q = q.filter_by(round=f.round_id.data)
if f.ip.data:
q = q.filter(db.Log.source_ip.ilike(f"%{f.ip.data}%"))
if f.status.data and "" not in f.status.data:
q = q.filter(db.Log.status.in_(f.status.data))
if f.endpoint.data:
q = q.filter(db.Log.endpoint.in_(f.endpoint.data))
limit = f.limit.data or 20
logs = q.limit(limit).all()
count = q.count()
b = BasePage() b = BasePage()
del f.csrf_token
b.line().h2("Logy") b.line().h2("Logy")
b(jinja_mac.quick_form(f, form_type='horizontal', method="GET"))
with b.p().table(_class="data full"): with b.p().table(_class="data full"):
with b.thead(): with b.thead(id="main_table"):
b.line().th()("Čas") b.line().th("Čas")
b.line().th()("Uživatel") b.line().th("Uživatel")
b.line().th()("URL") b.line().th("URL")
b.line().th()("Hra") b.line().th("Hra")
b.line().th()("Tým") b.line().th("Status")
b.line().th()("Status") b.line().th("Popis")
b.line().th()("Popis") b.line().th("GET")
b.line().th()("GET") b.line().th("Akce")
b.line().th()("Akce")
for l in logs: for l in logs:
with b.tr(): with b.tr(_class="log_"+l.status):
b.line().td(l.time.strftime("%H:%M:%S")) b.line().td(l.time.strftime("%H:%M:%S"))
b.line().td(l.user.print() if l.user else None, b._br(), l.source_ip) b.line().td(user_link(l.user) if l.user else None, b._br(), ip_link(l.source_ip))
b.line().td(l.endpoint._name_, b._br(), l.url) b.line().td(l.endpoint._name_, b._br(), l.url)
b.line().td(l.game.print() if l.game else None) b.line().td(l.game.print() if l.game else None, b._br(), "tým: ", l.team_id, " kolo: ", l.round)
b.line().td(l.team_id)
b.line().td(l.status) b.line().td(l.status)
b.line().td(l.text) b.line().td(l.text)
with b.line().td(): with b.line().td():
@ -545,7 +602,12 @@ def web_org_logs():
b(f"{k}: {v}").br() b(f"{k}: {v}").br()
with b.line().td(): with b.line().td():
with b.div(_class="btn-group", role="group"): with b.div(_class="btn-group", role="group"):
b.a(href=app.url_for(web_org_log.__name__, log_id=l.log_id), _class="btn btn-xs btn-primary")("Podrobnosti") if l.data is not None:
b.a(href=app.url_for(web_org_log_data.__name__, log_id=l.log_id), _class="btn btn-xs btn-primary")("Data")
if count <= limit:
b.line().p(f"Toť vše ({count})")
else:
b.line().p().b(f"Zobrazeno prvních {limit} logů z {count}")
return b.print_file() return b.print_file()
from hra.web.game import get_wlogic, wlogic_by_mode from hra.web.game import get_wlogic, wlogic_by_mode

20
server/static/ksp-mhd.css

@ -12,6 +12,7 @@ body {
margin: 0 auto; margin: 0 auto;
padding: 0 1em; padding: 0 1em;
width: 100%; width: 100%;
max-width: 1000px;
} }
.content_limited, main { .content_limited, main {
@ -300,3 +301,22 @@ nav#main-menu a.active {
.collapsible input[type="checkbox"].toggle:checked ~ .collapsible-inner { .collapsible input[type="checkbox"].toggle:checked ~ .collapsible-inner {
max-height: 100vh; max-height: 100vh;
} }
.log_ok {
background-color: #BBFFBB;
}
.log_warning {
background-color: #FFFFBB;
}
.log_error {
background-color: #FFBBBB;
}
.log_http-error {
background-color: #FF0000;
}
.log_too_late {
background-color: #BBFFFF;
}
.log_too_early {
background-color: #BBBBFF;
}

8
server/static/occupy.css

@ -1,15 +1,17 @@
table.game_table{
margin-left: auto;
margin-right: auto;
}
table.game_table, table.game_table tr, table.game_table tr td { table.game_table, table.game_table tr, table.game_table tr td {
border: thin solid black; border: thin solid black;
border-collapse: collapse; border-collapse: collapse;
table-layout: fixed; table-layout: fixed;
overflow: hidden;
} }
table.game_table tr td { table.game_table tr td {
width: 10pt;
height: 10px;
font-size: 8px;
text-align: end; text-align: end;
} }

Loading…
Cancel
Save