Skip to content

Commit

Permalink
Merge pull request #6 from FinemechanicPub/dev
Browse files Browse the repository at this point in the history
Update
  • Loading branch information
FinemechanicPub authored Aug 17, 2024
2 parents c4ce7bb + aa7df49 commit ae12b81
Show file tree
Hide file tree
Showing 20 changed files with 235 additions and 47 deletions.
9 changes: 7 additions & 2 deletions backend/app/api/endpoints/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ async def remove_game(
response_class=Response,
)
async def game_thumbnail(
game_id: int, session: AsyncSession = Depends(get_async_session)
game_id: int,
session: AsyncSession = Depends(get_async_session),
):
game = await game_extended_repository.get(session, game_id)
if not game:
Expand All @@ -114,4 +115,8 @@ async def game_thumbnail(
)
with BytesIO() as data:
image.save(data, "png")
return Response(content=data.getvalue(), media_type="image/png")
return Response(
content=data.getvalue(),
media_type="image/png",
headers={"Cache-Control": "max-age=604800"},
)
2 changes: 1 addition & 1 deletion backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ async def lifespan(app: FastAPI):
openapi_url="/api/v1/openapi.json",
docs_url="/api/v1/docs",
redoc_url="/api/v1/redoc",
description=f"Github commit {settings.branch}:{settings.commit[:7]}"
description=f"Github commit {settings.branch}:{settings.commit[:7]}",
)

# Подключение роутера
Expand Down
8 changes: 7 additions & 1 deletion backend/app/schemas/game.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
from pydantic import BaseModel, ConfigDict, Field, computed_field, field_validator
from pydantic import (
BaseModel,
ConfigDict,
Field,
computed_field,
field_validator,
)
from pydantic_core.core_schema import FieldValidationInfo


Expand Down
17 changes: 17 additions & 0 deletions backend/engine/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Система решения головоломок типа замощения плитками

Основная процедура решения головоломки находится в модуле [solver.py](solver.py) и реализвана в виде функции-генератора `solutions`.

Для получения решения функции передаются объект доски `board` и набор фигур `piece_set`.

Объект доски типа [Board](board.py) хранит размеры доски и двоичную маску занятых ячеек. Маска представляет собой целое число, при этом установленный бит соответствует занятой ячейке доски, а погашенный - свободной. Битовая маска получается построчной разверткой ячеек доски сверху вниз и слева направо. Младший бит соответствует правому нижнему углу доски. Ячейки могут быть заняты блоками, создаваемыми вместе с игрой, или установленными игроком фигуарми.

Набор фигур содержит все возможные маски всех фигур во всех доступных ориентациях. Маска фигуры представляет собой целое число, которое при совмещении с маской доски с помощью операции побитового **ИЛИ** отметит ячейки, в которые устанавливается фигура, как занятые.

Набор фигур организован в ввиде списка элементов по числу доступных для установки фигур. Каждый элемент в свою очередь представляет собой список длиной равной количеству ячеек на доске. Для каждой позиции в наборе хранится список масок фигур, которые можно установить в данную позицию (без учета наложения на уже установленные фигуры).

При такой схеме хранения могут быть заранее исключены из рассмотрения варианты установок фигур, при которых фигура выходит за границы доски или образует собой вместе с границей доски незаполняемую пустую область.

Алгоритм решения устроен по принципу последовательного перебора с ранним возвратом. На каждом шаге проверяется возможность установить очередную ориентацию очередной фигуры в первую свободную ячейку доски. В случае успеха, фигура фиксируется на доске и перебор продолжается для следующей свободной ячейки. Если никакую фигуру установить невозможно происходит возврат: последняя установленная фигура снимается с доски и перебор продолжается так, как будто её установка была неудачной.

Если после очередного шага алгоритма вся доска оказывается заполненной, список фигур с позициями их установки выдается в качестве решения.
27 changes: 26 additions & 1 deletion frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"dependencies": {
"@formkit/auto-animate": "^0.8.2",
"vue": "^3.4.15",
"vue-router": "^4.4.0"
"vue-router": "^4.4.0",
"vue3-tour": "^1.0.2"
},
"devDependencies": {
"@eslint/js": "^9.6.0",
Expand Down
2 changes: 1 addition & 1 deletion frontend/public/site.webmanifest
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"name":"Polimino puzzle game","short_name":"Puzzle","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
{"name":"Polimino puzzle game","short_name":"Puzzle","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"purple","background_color":"white","display":"standalone"}
3 changes: 3 additions & 0 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
</script>

<style src="vue3-tour/dist/vue3-tour.css">
</style>

<style scoped>
header{
display: flex;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ h1, h2 {
header {
background: hsla(160, 100%, 37%, 1);
box-shadow: 0 0 .2rem #0000001a,0 .2rem .4rem #0003;
margin: auto auto 20px auto;
/* margin: auto auto 20px auto; */
}

header > h1 {
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/GameCarousel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,12 @@
<div>
<img :src="thumbnailUrl(game)" alt="game thumbnail">
</div>
<button @click="router.push({name: 'game', params: {id: game.id}})" class="cta-button">
<button type="button" @click="router.push({name: 'game', params: {id: game.id}})" class="cta-button">
Играть
</button>
</div>
</div>
<button v-if="games.length > 1" class="carousel-button prev" @click="prevCard" :disabled="currentIndex === 0">&lt;</button>
<button v-if="games.length > 1" class="carousel-button next" @click="nextCard" :disabled="currentIndex === games.length - 1">&gt;</button>
<button type="button" v-if="games.length > 1" class="carousel-button prev" @click="prevCard" :disabled="currentIndex === 0">&lt;</button>
<button type="button" v-if="games.length > 1" class="carousel-button next" @click="nextCard" :disabled="currentIndex === games.length - 1">&gt;</button>
</div>
</template>
80 changes: 80 additions & 0 deletions frontend/src/components/GameTour.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<script setup>
import { computed } from 'vue';
const props = defineProps({
hasHint: Boolean,
hasPieces: Boolean
})
const options = {
useKeyboardNavigation: true,
highlight: true,
labels: {
buttonSkip: 'Пропустить',
buttonPrevious: 'Назад',
buttonNext: 'Вперед',
buttonStop: 'Закончить'
}
}
const steps = computed(() => [
{
target: '#board',
header: {
title: 'Доска',
},
content: "Все ячейки доски нужно закрыть фигурами без наложения одной на другую.",
},
{
target: '#palette',
header: {
title: 'Магазин фигур'
},
content: "Здесь лежат все доступные фигуры. Фигура устанавливается на доску перетаскиванием.",
params:{
highlight: props.hasPieces
}
},
{
target: 'div.palette-item, #palette',
content: "Если навести курсор на фигуру, появятся кнопки вращения и переворота. Вращение и переворот доступны не для всех фигур.",
params:{
highlight: props.hasPieces
}
},
{
target: 'div.square:nth-child(1)',
content: "Чтобы удалить фигуру с доски обратно в магазин, щелкните по любой её клетке.",
params: {
highlight: false
}
},
{
target: '#hintbox',
header: {
title: 'Робот'
},
content: "Робот умеет решать головоломки, он может подсказать хороший ход."
},
{
target: '#robotmove',
content: props.hasHint ? "Если нажать на эту кнопку, робот поставит подходящую фигуру на доску." : "Сейчас у робота нет подсказок, поэтому кнопка хода роботом 🆗 скрыта.",
params: {
highlight: props.hasHint
}
},
{
target: '#robotswitch',
content: "С помощью этой кнопки подсказки робота можно включать и отключать."
},
{
target: '#info',
content: "Чтобы посмотреть инструкции ещё раз, нажмите кнопку ℹ️ снова."
}
]
)
</script>

<template>
<v-tour name="gameTour" :steps="steps" :options="options"></v-tour>
</template>
37 changes: 21 additions & 16 deletions frontend/src/components/HintBox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,27 @@
gameId: Number,
installedPices: Array
})
const emit = defineEmits(['hint'])
const loading = ref(false)
const error = ref(null)
const hint = ref(null)
const info = ref(null)
const complete = ref(false)
const hintActive = ref(true)
const hasHint = computed(() => hint.value !== null)
const message = computed(
() => loading.value ? " ...запрашиваю Центр... " : (
hint.value ? "могу подсказать ход" : (
info.value ? info.value : error.value
)
hasHint.value ? "могу подсказать ход" : "безвыходная ситуация"
)
)
watchEffect(fetchHint)
async function fetchHint(){
hint.value = null
info.value = null
complete.value = false
error.value = null
if (!hintActive.value) return;
Expand All @@ -50,13 +49,12 @@
// progress = 1
// complete = 2
// deadlock = 3
if (data.status == 3) {
info.value = "безвыходная ситуация"
} else if (data.status == 2) {
error.value = ""
} else {
if (data.status == 1) {
hint.value = Object.values(data.hint)
} else if (data.status == 2) {
complete.value = true
}
error.value = ""
} catch (err) {
console.log("fetching a hint caused the error: ", err.toString())
if (err instanceof ApiError){
Expand All @@ -68,6 +66,10 @@
loading.value = false
}
}
defineExpose({
hasHint: hasHint
})
</script>

<style scoped>
Expand All @@ -78,11 +80,14 @@

<template>
<div class="hint-box">
<p @click="hintActive = !hintActive" class="hint-item transparent-button">🤖</p>
<p class="hint-item">
<button id="robotswitch" type="button" :title="complete ? 'отключено' : hintActive ? 'выключить' : 'включить'" @click="hintActive = !hintActive" :disabled="complete" class="hint-item transparent-button">
🤖
</button>
<p v-if="hintActive && !complete" class="hint-item">
{{ message }}
</p>
<button @click="emit('hint', hint)" class="hint-item transparent-button" v-if="hint">🆗</button>
<button @click="fetchHint" class="hint-item transparent-button" v-if="error">↩️</button>
<button id="robotmove" type="button" title="пусть ходит робот" @click="emit('hint', hint)" class="hint-item transparent-button" v-if="hasHint">🆗</button>
<p id="robotmove" v-else> </p>
<button type="button" title="повторить запрос" @click="fetchHint" class="hint-item transparent-button" v-if="error">↩️</button>
</div>
</template>
9 changes: 5 additions & 4 deletions frontend/src/components/PieceItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@
.container-row{
display: flex;
flex-direction: row;
align-items: center;
}
.piece-box{
display: flex;
Expand Down Expand Up @@ -201,7 +202,7 @@
<template>
<div class="hover" @mouseenter="hovering=true" @mouseleave="hovering=false">
<div class="container-row" >
<button class="transparent-button" :class="{invisible: !(hovering && canRotate)}" @click="emit('changeVersion', rotate(props.piece.base_version, 1))">
<button type="button" title="повернуть влево" class="transparent-button" :class="{invisible: !(hovering && canRotate)}" @click="emit('changeVersion', rotate(props.piece.base_version, 1))">
↪️
</button>
<div class="piece-box movable">
Expand All @@ -217,15 +218,15 @@
</div>
</div>
</div>
<button class="transparent-button" :class="{invisible: !(hovering && canRotate)}" @click="emit('changeVersion', rotate(props.piece.base_version, -1))">
<button type="button" title="повернуть вправо" class="transparent-button" :class="{invisible: !(hovering && canRotate)}" @click="emit('changeVersion', rotate(props.piece.base_version, -1))">
↩️
</button>
</div>
<div class="flex-center-content">
<button class="centered padded transparent-button" :class="{invisible: !(hovering && canFlip)}" @click="emit('changeVersion', flip(props.piece.base_version, false))">
<button type="button" title="перевернуть сверху вниз" class="centered padded transparent-button" :class="{invisible: !(hovering && canFlip)}" @click="emit('changeVersion', flip(props.piece.base_version, false))">
🔃
</button>
<button class="centered padded transparent-button" :class="{invisible: !(hovering && canFlip)}" @click="emit('changeVersion', flip(props.piece.base_version, true))">
<button type="button" title="перевернуть слева направо" class="centered padded transparent-button" :class="{invisible: !(hovering && canFlip)}" @click="emit('changeVersion', flip(props.piece.base_version, true))">
🔁
</button>
</div>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/PiecePalette.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<style scoped>
.palette-item{
align-content: center;
height: fit-content;
}
</style>

Expand Down
8 changes: 5 additions & 3 deletions frontend/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import './assets/main.css'
import { createApp } from 'vue'
import { autoAnimatePlugin } from '@formkit/auto-animate/vue'
import { OpenAPI } from './api/generated/core/OpenAPI';
import App from './App.vue'
import router from './routers/router'
import Vue3Tour from 'vue3-tour';

import App from './App.vue';
import router from './routers/router';

if (import.meta.env.DEV){
console.log(`base api url: ${import.meta.env.VITE_API_BASE_URI}`)
OpenAPI.BASE = `${import.meta.env.VITE_API_BASE_URI}`
}
createApp(App).use(autoAnimatePlugin).use(router).mount('#app')
createApp(App).use(autoAnimatePlugin).use(router).use(Vue3Tour).mount('#app')
Loading

0 comments on commit ae12b81

Please sign in to comment.