preparing for the new tic-tac-toe tutorial

This commit is contained in:
Jakob Pinterits
2024-04-12 14:20:11 +02:00
parent 87c43f0311
commit 8c705c09e0
32 changed files with 520 additions and 519 deletions

View File

@@ -70,12 +70,12 @@ class Sidebar(component.Component):
rio.Text(
self.session.app.name,
style="heading2",
justify='left',
justify="left",
),
rio.Text(
"TODO: Subtext",
style="dim",
justify='left',
justify="left",
),
align_y=0,
),

View File

@@ -102,7 +102,7 @@ class LabeledColumn(Component):
def build(self) -> Component:
rows = [
[
rio.Text(label, justify='right'),
rio.Text(label, justify="right"),
child,
]
for label, child in self.content.items()

View File

@@ -195,7 +195,7 @@ class SimpleListItem(Component):
text_children = [
rio.Text(
self.text,
justify='left',
justify="left",
)
]
@@ -205,7 +205,7 @@ class SimpleListItem(Component):
self.secondary_text,
wrap=True,
style="dim",
justify='left',
justify="left",
)
)

View File

@@ -106,7 +106,7 @@ class ComponentDetails(rio.Component):
rio.Text(
text,
style="dim" if is_label else "text",
justify='right' if is_label else 'left',
justify="right" if is_label else "left",
),
row_index,
column_index,
@@ -126,7 +126,7 @@ class ComponentDetails(rio.Component):
rio.Text(
target.key,
style="dim",
justify='left',
justify="left",
),
]
@@ -160,7 +160,7 @@ class ComponentDetails(rio.Component):
rio.Text(
f"{file} line {line}",
style="dim",
justify='left',
justify="left",
),
row_index,
0,
@@ -227,8 +227,8 @@ class ComponentDetails(rio.Component):
row_index += 1
# Header
result.add(rio.Text("width", style="dim", justify='left'), row_index, 1)
result.add(rio.Text("height", style="dim", justify='left'), row_index, 2)
result.add(rio.Text("width", style="dim", justify="left"), row_index, 1)
result.add(rio.Text("height", style="dim", justify="left"), row_index, 2)
row_index += 1
# The size as specified in Python

View File

@@ -8,7 +8,7 @@ class DeployPage(rio.Component):
"Deploy",
style="heading2",
margin=1,
justify='left',
justify="left",
),
rio.Column(
rio.Icon(

View File

@@ -8,7 +8,7 @@ class DocsPage(rio.Component):
"Documentation",
style="heading2",
margin=1,
justify='left',
justify="left",
),
rio.Column(
rio.Text(

View File

@@ -162,9 +162,11 @@ class IconsPage(rio.Component):
async def _on_select_icon_by_name(self, icon_name: str) -> None:
# Parse the icon name
icon_set, icon_name, icon_variant = (
rio.icon_registry.IconRegistry.parse_icon_name(icon_name)
)
(
icon_set,
icon_name,
icon_variant,
) = rio.icon_registry.IconRegistry.parse_icon_name(icon_name)
# Find all available variants
for (
@@ -210,7 +212,7 @@ class IconsPage(rio.Component):
rio.Text(
"Configure",
style="heading3",
justify='left',
justify="left",
)
)
@@ -285,7 +287,7 @@ class IconsPage(rio.Component):
rio.Text(
"Example Code",
style="heading3",
justify='left',
justify="left",
margin_top=1,
)
)

View File

@@ -36,13 +36,13 @@ class ProjectPage(rio.Component):
rio.Text(
project_name,
style="heading2",
justify='left',
justify="left",
),
rio.Text(
str(self.project.project_directory),
style="dim",
margin_bottom=2,
justify='left',
justify="left",
),
rio.Text(
"To launch your project, Rio needs to know the name of your python module and in which variable you've stored your app. You can configure those here.",

View File

@@ -201,7 +201,7 @@ class PalettePicker(rio.Component): #
fill=palette.foreground,
),
selectable=False,
justify='left',
justify="left",
),
rio.Text(
f"#{palette.background.hex}",
@@ -209,7 +209,7 @@ class PalettePicker(rio.Component): #
font_size=1,
fill=palette.foreground.replace(opacity=0.5),
),
justify='left',
justify="left",
),
spacing=0.2,
margin_x=1,
@@ -419,7 +419,7 @@ class ThemePickerPage(rio.Component):
style="heading3",
margin_top=1,
margin_bottom=1,
justify='left',
justify="left",
),
radius_sliders,
# Theme Variants
@@ -428,7 +428,7 @@ class ThemePickerPage(rio.Component):
style="heading3",
margin_top=1,
margin_bottom=1,
justify='left',
justify="left",
),
rio.Row(
rio.Spacer(),
@@ -451,7 +451,7 @@ class ThemePickerPage(rio.Component):
style="heading3",
margin_top=1,
# margin_bottom=1, Not used for now, since markdown has an oddly large margin anyway
justify='left',
justify="left",
),
rio.Markdown(
f"""

View File

@@ -1,13 +0,0 @@
import rio
from . import pages
app = rio.App(
name="Biography",
pages=[
rio.Page("", pages.BiographyPage),
],
)
fastapi_app = app.as_fastapi()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

View File

@@ -1,5 +0,0 @@
from .about_me import AboutMe
from .contact import Contact
from .history import History
from .projects import Projects
from .skill_bars import SkillBars

View File

@@ -1,41 +0,0 @@
from pathlib import Path
from typing import * # type: ignore
import rio
class AboutMe(rio.Component):
def build(self) -> rio.Component:
return rio.Row(
rio.Image(
Path("me.jpg"),
width=11,
height=11,
corner_radius=99999,
fill_mode="zoom",
margin_right=2,
),
rio.Column(
rio.Text(
"Jane Doe",
style="heading1",
justify='left',
),
rio.Text(
"Data Analyst",
justify='left',
),
rio.Text(
"AI Researcher",
justify='left',
),
rio.Text(
"Python Developer",
justify='left',
),
spacing=0.5,
align_y=0,
width="grow",
),
width="grow",
)

View File

@@ -1,50 +0,0 @@
from dataclasses import KW_ONLY
from typing import * # type: ignore
import rio
class Contact(rio.Component):
text: str
icon_name_or_url: str | rio.URL
_: KW_ONLY
link: rio.URL | None = None
def build(self) -> rio.Component:
# Prepare the image. This varies depending on whether an icon or an
# image should be displayed.
if isinstance(self.icon_name_or_url, str):
image = rio.Icon(
self.icon_name_or_url,
width=2,
height=2,
margin_x=0.5,
)
else:
image = rio.Image(
self.icon_name_or_url,
width=2,
height=2,
margin_x=0.5,
)
# Combine everything
result = rio.Row(
image,
rio.Text(
self.text,
justify='left',
width="grow",
),
width="grow",
margin=0.2,
)
# If a link is provided, wrap the result in a link
if self.link is not None:
result = rio.Link(
result,
self.link,
)
return result

View File

@@ -1,114 +0,0 @@
from typing import * # type: ignore
import rio
class HistoryItem(rio.Component):
from_string: str
to_string: str
title: str
organization: str
details: str
icon: str
is_open: bool = False
def on_press(self) -> None:
self.is_open = not self.is_open
def build(self) -> rio.Component:
return rio.Card(
rio.Row(
rio.Icon(
self.icon,
width=3,
height=3,
margin_right=2,
align_x=0,
align_y=0,
),
rio.Column(
rio.Text(
f"{self.from_string} - {self.to_string}",
justify='left',
style=rio.TextStyle(fill=rio.Color.GREY),
),
rio.Text(
f"{self.title} at {self.organization}",
style="heading3",
justify='left',
),
rio.Revealer(
None,
rio.Text(
self.details,
wrap=True,
justify='left',
),
is_open=self.is_open,
),
spacing=0.3,
width="grow",
align_y=0,
),
),
on_press=self.on_press,
)
class History(rio.Component):
def build(self) -> rio.Component:
return rio.Column(
rio.Text(
"Professional Experience",
style="heading2",
margin_top=3,
),
HistoryItem(
"2021",
"Present",
"AI Researcher",
"DataWorks Inc.",
"Developed and deployed machine learning models to improve the "
"accuracy of the company's recommender system. Collaborated "
"with a team of 10 researchers to implement a novel algorithm "
"for predicting user behavior.",
"science",
),
HistoryItem(
"2018",
"2021",
"Data Analyst",
"Insight Technologies",
"Conducted in-depth data analysis and implemented machine "
"learning algorithms to improve predictive models, resulting "
"in a 15% increase in accuracy. Collaborated with "
"cross-functional teams to develop and deploy scalable AI "
"solutions for optimizing business operations.",
"monitoring",
),
rio.Text(
"Education",
style="heading2",
margin_top=3,
),
HistoryItem(
"2015",
"2018",
"Master in Data Science",
"Carnegie Mellon University, Pittsburgh, PA",
'Thesis: "Predictive Modeling in Financial Markets using '
'Machine Learning Techniques"',
"school",
),
HistoryItem(
"2012",
"2015",
"Bachelor in Software Engineering",
"University of Texas, Austin, TX",
'Thesis: "Design and Implementation of an Intelligent Traffic '
'Control System"',
"school",
),
spacing=2,
)

View File

@@ -1,70 +0,0 @@
from typing import * # type: ignore
import rio
class Project(rio.Component):
name: str
details: str
link: rio.URL
def on_press(self) -> None:
self.session.navigate_to(self.link)
def build(self) -> rio.Component:
return rio.Card(
rio.Column(
rio.Text(
self.name,
style="heading3",
),
rio.Text(
self.details,
wrap=True,
),
spacing=0.5,
),
on_press=self.on_press,
)
class Projects(rio.Component):
def build(self) -> rio.Component:
return rio.Grid(
(
Project(
"uniserde",
"Pure Python, easy to use Serializer",
rio.URL("https://gitlab.com/Vivern/uniserde"),
),
Project(
"uniserde",
"Pure Python, easy to use Serializer",
rio.URL("https://gitlab.com/Vivern/uniserde"),
),
Project(
"uniserde",
"Pure Python, easy to use Serializer",
rio.URL("https://gitlab.com/Vivern/uniserde"),
),
),
(
Project(
"uniserde",
"Pure Python, easy to use Serializer",
rio.URL("https://gitlab.com/Vivern/uniserde"),
),
Project(
"uniserde",
"Pure Python, easy to use Serializer",
rio.URL("https://gitlab.com/Vivern/uniserde"),
),
Project(
"uniserde",
"Pure Python, easy to use Serializer",
rio.URL("https://gitlab.com/Vivern/uniserde"),
),
),
row_spacing=2,
column_spacing=2,
)

View File

@@ -1,30 +0,0 @@
from typing import * # type: ignore
import rio
class SkillBars(rio.Component):
skills: dict[str, int]
def build(self) -> rio.Component:
rows = []
for name, level in self.skills.items():
rows.append(
rio.Column(
rio.Text(
name,
justify='left',
),
rio.ProgressBar(
level / 10,
),
)
)
return rio.Card(
rio.Column(
*rows,
spacing=0.5,
),
)

View File

@@ -1 +0,0 @@
from .biography_page import BiographyPage

View File

@@ -1,125 +0,0 @@
from typing import * # type: ignore
import rio
from .. import components as comps
class BiographyPage(rio.Component):
def build(self) -> rio.Component:
thm = self.session.theme
return rio.Column(
rio.Rectangle(
fill=rio.LinearGradientFill(
(thm.primary_palette.background, 0),
(thm.primary_palette.background.brighter(0.2), 1),
),
height=1.0,
),
rio.Column(
comps.AboutMe(),
rio.Grid(
(
comps.Contact(
"123 Main Street, Anytown, USA 12345",
"location-on",
),
comps.Contact(
"Gitlab",
rio.URL(
"https://about.gitlab.com/images/press/logo/svg/gitlab-logo-500.svg"
),
link=rio.URL("https://gitlab.com"),
),
),
(
comps.Contact(
"(123) 456-7890",
"call:fill",
),
comps.Contact(
"GitHub",
rio.URL(
"https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png"
),
link=rio.URL("https://github.com"),
),
),
(
comps.Contact(
"janedoe@email.com",
"mail",
link=rio.URL("mailto:janedoe@email.com"),
),
comps.Contact(
"LinkedIn",
rio.URL(
"https://upload.wikimedia.org/wikipedia/commons/thumb/c/ca/LinkedIn_logo_initials.png/768px-LinkedIn_logo_initials.png"
),
link=rio.URL("https://linkedin.com"),
),
),
row_spacing=1.5,
column_spacing=5,
width="grow",
margin_y=2,
),
rio.Row(
comps.SkillBars(
{
"Python": 5,
"Rust": 3,
"SQL": 4,
"R": 3,
},
width="grow",
align_y=0,
),
comps.SkillBars(
{
"Pandas": 5,
"Numpy": 5,
"SciPy": 3,
"Matplotlib": 4,
"Seaborn": 4,
},
width="grow",
align_y=0,
),
comps.SkillBars(
{
"English": 5,
"Spanish": 3,
"French": 2,
},
width="grow",
align_y=0,
),
width="grow",
spacing=5,
),
comps.History(margin_x=10),
rio.Text(
"Personal Projects",
style="heading2",
margin_x=8,
margin_top=3,
),
comps.Projects(),
rio.Overlay(
rio.IconButton(
icon="material/mail:fill",
margin_right=3,
margin_bottom=3,
align_x=1,
align_y=1,
)
),
margin_y=4,
spacing=2,
width=40,
align_x=0.5,
),
align_y=0,
)

View File

@@ -1,10 +0,0 @@
from typing import * # type: ignore
import rio
from .. import components as comps
class BiographyPage(rio.Component):
def build(self) -> rio.Component:
return comps.AboutMe()

View File

@@ -0,0 +1 @@
from .field import Field as Field

View File

@@ -0,0 +1,64 @@
from __future__ import annotations
from typing import * # type: ignore
import rio
# <component>
class Field(rio.Component):
"""
This component represents a single field in the Tic Tac Toe game. It can
contain an X, an O, or be empty. If the field is empty, the player can press
on it to set their respective mark.
"""
# The current value of the field.
value: Literal["X", "O", ""]
# If this is `True`, the field will dim its content. This allows the game to
# dim any fields that are not part of the winning combination when a player
# has won.
dim: bool
# The game needs to know when a player presses on a field, so it can update
# the board accordingly. This custom event handler serves that purpose.
on_press: rio.EventHandler[[]] = None
def build(self) -> rio.Component:
# We'll need to specify the size of several components. This variable
# makes it easier to adjust the size of the fields.
field_size = 3
# If the field is empty, allow the player to press on it. Since buttons
# would look out of place here, cards are a nice alternative.
if self.value == "":
return rio.Card(
content=rio.Spacer(
width=field_size,
height=field_size,
),
on_press=self.on_press,
)
# For fields that already contain an X or O, show the respective
# icon.
#
# Vary the color based on the player
color = rio.Color.RED if self.value == "X" else rio.Color.BLUE
# If a player has won, and this field isn't part of the winning
# combination, dim it.
if self.dim:
color = color.replace(opacity=0.2)
# Create the icon to represent the field
return rio.Icon(
"material/close" if self.value == "X" else "material/circle",
fill=color,
width=field_size,
height=field_size,
)
# </component>

View File

@@ -2,11 +2,12 @@ from __future__ import annotations
# <additional-imports>
import functools
from dataclasses import field
from typing import * # type: ignore
import rio
from .. import components as comps
# </additional-imports>
@@ -21,8 +22,12 @@ class TicTacToePage(rio.Component):
Everything is contained right in this class.
"""
# The contents of all fields
fields: list[Literal["X", "O", ""]] = field(default_factory=lambda: [""] * 9)
# The contents of all fields. Each field can contain an X, an O, or be
# empty. The initial state is an empty board.
#
# The first value is the top-left field. The remaining fields follow from
# left to right, top to bottom.
fields: list[Literal["X", "O", ""]] = [""] * 9
# The player who is currently on turn
player: Literal["X", "O"] = "X"
@@ -32,7 +37,7 @@ class TicTacToePage(rio.Component):
# If there is a winner, these are the indices of the fields which made them
# win
winning_indices: set[int] = field(default_factory=set)
winning_indices: set[int] = set()
def find_winner(self) -> None:
"""
@@ -104,41 +109,15 @@ class TicTacToePage(rio.Component):
def build(self) -> rio.Component:
# Spawn components for the fields
field_components: list[rio.Component] = []
field_size = 3
for index, field in enumerate(self.fields):
# Allow the player to press on empty fields. Since buttons would
# look out of place here, cards are a nice alternative.
if field == "":
field_components.append(
rio.Card(
content=rio.Spacer(
width=field_size,
height=field_size,
),
on_press=functools.partial(self.on_press, index),
)
)
# For fields that already contain an X or O, show the respective
# icon
else:
# Vary the color based on the player
color = rio.Color.RED if field == "X" else rio.Color.BLUE
# If a player has won highlight the winning fields
if self.winner in ("X", "O") and index not in self.winning_indices:
color = color.replace(opacity=0.3)
# Create the icon to represent the field
field_components.append(
rio.Icon(
"material/close" if field == "X" else "material/circle",
fill=color,
width=field_size,
height=field_size,
)
field_components.append(
comps.Field(
value=field,
dim=self.winner is not None and index not in self.winning_indices,
on_press=functools.partial(self.on_press, index),
)
)
# Come up with a status message
if self.winner in ("X", "O"):

View File

@@ -0,0 +1,32 @@
from __future__ import annotations
from typing import * # type: ignore
import rio
# <code>
class TicTacToePage(rio.Component):
# The contents of all fields. Each field can contain an X, an O, or be
# empty. The initial state is an empty board.
fields: list[Literal["X", "O", ""]] = [""] * 9
def build(self) -> rio.Component:
# Spawn components for the fields
field_components: list[rio.Component] = []
for index, field in enumerate(self.fields):
field_components.append(rio.Text(f"Field {index}"))
# Arrange all components in a grid
return rio.Grid(
field_components[0:3],
field_components[3:6],
field_components[6:9],
row_spacing=1,
column_spacing=1,
align_x=0.5,
)
# </code>

View File

@@ -0,0 +1,43 @@
from __future__ import annotations
from typing import * # type: ignore
import rio
# <code>
class Field(rio.Component):
value: Literal["X", "O", ""]
on_press: rio.EventHandler[[]] = None
def build(self) -> rio.Component:
# If the field is empty, allow the player to press on it. Since buttons
# would look out of place here, cards are a nice alternative.
if self.value == "":
return rio.Card(
content=rio.Spacer(
width=3,
height=3,
),
on_press=self.on_press,
)
# For fields that already contain an X or O, show the respective icon.
# Also vary the color based on the player.
if self.value == "X":
color = rio.Color.RED
icon = "material/close"
else:
color = rio.Color.BLUE
icon = "material/circle"
return rio.Icon(
icon=icon,
fill=color,
width=3,
height=3,
)
# </code>

View File

@@ -0,0 +1,36 @@
from __future__ import annotations
from typing import * # type: ignore
import rio
from . import field as comps
# <code>
class TicTacToePage(rio.Component):
# The contents of all fields. Each field can contain an X, an O, or be
# empty. The initial state is an empty board.
fields: list[Literal["X", "O", ""]] = [""] * 9
def build(self) -> rio.Component:
# Spawn components for the fields
field_components: list[rio.Component] = []
for index, field in enumerate(self.fields):
comps.Field(
value=field,
)
# Arrange all components in a grid
return rio.Grid(
field_components[0:3],
field_components[3:6],
field_components[6:9],
row_spacing=1,
column_spacing=1,
align_x=0.5,
)
# </code>

View File

@@ -0,0 +1,40 @@
from __future__ import annotations
from typing import * # type: ignore
import rio
# <code>
class Field(rio.Component):
value: Literal["X", "O", ""]
on_press: rio.EventHandler[[]] = None
def build(self) -> rio.Component:
# If the field is empty, allow the player to press on it. Since buttons
# would look out of place here, cards are a nice alternative.
if self.value == "":
return rio.Card(
content=rio.Spacer(
width=3,
height=3,
),
on_press=self.on_press,
)
# For fields that already contain an X or O, show the respective
# icon.
#
# Vary the color based on the player
color = rio.Color.RED if self.value == "X" else rio.Color.BLUE
return rio.Icon(
"material/close" if self.value == "X" else "material/circle",
fill=color,
width=3,
height=3,
)
# </code>

View File

@@ -0,0 +1,78 @@
from __future__ import annotations
import functools
from typing import * # type: ignore
import rio
from . import field as comps
# <code>
class TicTacToePage(rio.Component):
# The contents of all fields. Each field can contain an X, an O, or be
# empty. The initial state is an empty board.
fields: list[Literal["X", "O", ""]] = [""] * 9
# The player who is currently on turn
player: Literal["X", "O"] = "X"
async def on_press(self, index: int) -> None:
"""
This function reacts to presses on the fields, and updates the game
state accordingly.
"""
# Set the field on that index
self.fields[index] = self.player
# Next player
self.player = "X" if self.player == "O" else "O"
def on_reset(self) -> None:
"""
Reset the game to its initial state.
"""
self.fields = [""] * 9
self.player = "X"
def build(self) -> rio.Component:
# Spawn components for the fields
field_components: list[rio.Component] = []
for index, field in enumerate(self.fields):
field_components.append(
comps.Field(
value=field,
on_press=functools.partial(self.on_press, index),
)
)
# Come up with a status message
message = f"{self.player}'s turn"
# Arrange all components in a grid
return rio.Column(
rio.Text(message, style="heading1"),
rio.Grid(
field_components[0:3],
field_components[3:6],
field_components[6:9],
row_spacing=1,
column_spacing=1,
align_x=0.5,
),
rio.Button(
"Reset",
icon="material/refresh",
style="plain",
on_press=self.on_reset,
),
spacing=2,
margin=2,
align_x=0.5,
align_y=0.0,
)
# </code>

View File

@@ -0,0 +1,46 @@
from __future__ import annotations
from typing import * # type: ignore
import rio
# <code>
class Field(rio.Component):
value: Literal["X", "O", ""]
dim: bool
on_press: rio.EventHandler[[]] = None
def build(self) -> rio.Component:
# If the field is empty, allow the player to press on it. Since buttons
# would look out of place here, cards are a nice alternative.
if self.value == "":
return rio.Card(
content=rio.Spacer(
width=3,
height=3,
),
on_press=self.on_press,
)
# For fields that already contain an X or O, show the respective
# icon.
#
# Vary the color based on the player
color = rio.Color.RED if self.value == "X" else rio.Color.BLUE
# If a player has won, and this field isn't part of the winning
# combination, dim it.
if self.dim:
color = color.replace(opacity=0.2)
return rio.Icon(
"material/close" if self.value == "X" else "material/circle",
fill=color,
width=3,
height=3,
)
# </code>

View File

@@ -0,0 +1,139 @@
from __future__ import annotations
import functools
from typing import * # type: ignore
import rio
from . import field as comps
# <code>
class TicTacToePage(rio.Component):
# The contents of all fields. Each field can contain an X, an O, or be
# empty. The initial state is an empty board.
fields: list[Literal["X", "O", ""]] = [""] * 9
# The player who is currently on turn
player: Literal["X", "O"] = "X"
# The winner of the game, if any
winner: Literal["X", "O", "draw"] | None = None
# If there is a winner, these are the indices of the fields which made them
# win
winning_indices: set[int] = set()
def find_winner(self) -> None:
"""
Look if there is a winner on the board, and stores it in the component's
state. Also updates the winning indices accordingly.
"""
# Create a list of all possible winning combinations. If all of these
# indices have the same value, then that value is the winner.
winning_combinations = [
[0, 1, 2], # Top row
[3, 4, 5], # Middle row
[6, 7, 8], # Bottom row
[0, 3, 6], # Left column
[1, 4, 7], # Middle column
[2, 5, 8], # Right column
[0, 4, 8], # Top-left to bottom-right diagonal
[2, 4, 6], # Top-right to bottom-left diagonal
]
# Look for winners
for combination in winning_combinations:
values = [self.fields[i] for i in combination]
if values.count("X") == 3:
self.winner = "X"
self.winning_indices = set(combination)
return
if values.count("O") == 3:
self.winner = "O"
self.winning_indices = set(combination)
return
# If no fields are empty, it's a draw
if "" not in self.fields:
self.winner = "draw"
return
# There is no winner yet
pass
async def on_press(self, index: int) -> None:
"""
This function reacts to presses on the fields, and updates the game
state accordingly.
"""
# If the game is already over, don't do anything
if self.winner:
return
# Set the field on that index
self.fields[index] = self.player
# Next player
self.player = "X" if self.player == "O" else "O"
# See if there is a winner
self.find_winner()
def on_reset(self) -> None:
"""
Reset the game to its initial state.
"""
self.fields = [""] * 9
self.player = "X"
self.winner = None
self.winning_indices = set()
def build(self) -> rio.Component:
# Spawn components for the fields
field_components: list[rio.Component] = []
for index, field in enumerate(self.fields):
field_components.append(
comps.Field(
value=field,
dim=self.winner is not None and index not in self.winning_indices,
on_press=functools.partial(self.on_press, index),
)
)
# Come up with a status message
if self.winner in ("X", "O"):
message = f"{self.winner} has won!"
elif self.winner == "draw":
message = "It's a draw!"
else:
message = f"{self.player}'s turn"
# Arrange all components in a grid
return rio.Column(
rio.Text(message, style="heading1"),
rio.Grid(
field_components[0:3],
field_components[3:6],
field_components[6:9],
row_spacing=1,
column_spacing=1,
align_x=0.5,
),
rio.Button(
"Reset",
icon="material/refresh",
style="plain",
on_press=self.on_reset,
),
spacing=2,
margin=2,
align_x=0.5,
align_y=0.0,
)
# </code>