CoolFace
Apppublic

Yamari/Knapsack

sourceHugging Faceupdated 7mo agoView on Hugging Face
0likes
app.py264 linesDownload Raw Back to root
1from fastapi import FastAPI, HTTPException, Request2from fastapi.responses import HTMLResponse, JSONResponse3from fastapi.templating import Jinja2Templates4from fastapi.staticfiles import StaticFiles5from pydantic import BaseModel, Field6from typing import List, Tuple, Optional7import uvicorn8import os9import json10 11import shapes  # наш модуль с полимино12 13from shapes import ROWS, COLS, ORIENTATION_BY_ID14from solvers import Solver, PolyominoSolverWithBonusZ315 16app = FastAPI(title="Polyomino Knapsack — Two Boxes")17templates = Jinja2Templates(directory="templates")18app.mount("/static", StaticFiles(directory="static"), name="static")19 20# ------------------------------------------------------------21# Параметры сетки22# ------------------------------------------------------------23 24def cell_to_bit(r, c):25    return 1 << (r * COLS + c)26 27def pack_items(items: List[dict]) -> Tuple[List[List[int]], List[dict]]:28    """29    Упаковывает предметы последовательно в сетку 5x7.30    items: список с ключами 'clientId', 'orientation_id', 'value'31    Возвращает (grid, placed_items)32    """33    grid = [[0] * COLS for _ in range(ROWS)]34    placed = []35    free = [(r, c) for r in range(ROWS) for c in range(COLS)]36 37    for it in items:38        orient = shapes.ORIENTATION_BY_ID[it['orientation_id']]39        coords = orient['coords']40        found = False41        for idx, (r0, c0) in enumerate(free):42            ok = True43            positions = []44            for dy, dx in coords:45                r, c = r0 + dy, c0 + dx46                if r >= ROWS or c >= COLS or grid[r][c] != 0:47                    ok = False48                    break49                positions.append((r, c))50            if ok:51                for r, c in positions:52                    grid[r][c] = it['clientId']53                placed.append({54                    'clientId': it['clientId'],55                    'orientation_id': it['orientation_id'],56                    'value': it['value'],57                    'positions': positions58                })59                free.pop(idx)60                found = True61                break62        # если не найдено место — предмет не помещается, пропускаем63    return grid, placed64 65 66# ------------------------------------------------------------67# Pydantic модели68# ------------------------------------------------------------69class Item(BaseModel):70    clientId: Optional[int] = None   # для обратной связи с клиентом71    orientation_id: int = Field(..., ge=1, le=len(shapes.ALL_ORIENTATIONS))72    value: int = Field(..., ge=1)73    type_id: int = Field(..., ge=1, le=4, description="1-оружие, 2-антиквариат, 3-еда, 4-stone")74    special: bool = Field(False, description="Особый предмет, дающий бонус за соседство")75 76class UsedItem(BaseModel):77    clientId: int78    orientation_id: int79    value: int80    color: str81    positions: List[Tuple[int, int]]82    type_id: int83    special: bool84    85class ItemsRequest(BaseModel):86    items: List[Item]87 88class PlacedItem(BaseModel):89    clientId: int90    orientation_id: int91    value: int92    positions: List[Tuple[int, int]]93 94class OptimizeResponse(BaseModel):95    usedGrid: List[List[int]]96    unusedGrid: List[List[int]]97    usedItems: List[PlacedItem]98    unusedItems: List[PlacedItem]99    totalValue: int100 101# ------------------------------------------------------------102# API эндпоинты103# ------------------------------------------------------------104 105visitor_counter = 0106 107@app.get("/api/visitor")108async def visitor():109    global visitor_counter110    visitor_counter += 1111    return JSONResponse(content={"count": visitor_counter})112    113 114@app.get("/", response_class=HTMLResponse)115async def root(request: Request):116    return templates.TemplateResponse("index.html", {"request": request})117 118@app.get("/orientations")119async def get_orientations():120    """Возвращает список всех доступных ориентаций с координатами"""121    result = []122    for o in shapes.ALL_ORIENTATIONS:123        result.append({124            "id": o["id"],125            "name": o["name"],126            "coords": o["coords"],127            "size": o["size"],128            "base_id": o["base_id"]129        })130    return result131 132@app.post("/optimize")133async def optimize(request: Request):134    # Получаем JSON в виде словаря135    data = await request.json()136    items_data = data.get("items", [])137    138    if not items_data:139        raise HTTPException(status_code=400, detail="Нет предметов")140 141    # Преобразуем во внутренний формат для солвера142    items_for_solver = []143    client_id_map = {}144    for idx, it in enumerate(items_data):145        # Проверяем наличие обязательных полей146        required_fields = ["clientId", "orientation_id", "value", "type_id", "special"]147        for field in required_fields:148            if field not in it:149                raise HTTPException(status_code=400, detail=f"Отсутствует поле {field}")150        151        items_for_solver.append({152            "id": idx,153            "client_id": it["clientId"],154            "orientation_id": it["orientation_id"],155            "value": it["value"],156            "type_id": it["type_id"],157            "special": it["special"]158        })159        client_id_map[idx] = it["clientId"]160 161    # Запуск солвера (используем класс с бонусами)162    solver = PolyominoSolverWithBonusZ3(items_for_solver, time_limit = 10)163    total_value, taken, stats = solver.solve()164 165    # Построение сетки и формирование usedItems166    grid = [[0] * COLS for _ in range(ROWS)]167    used_items = []168    taken_indices = set()169 170    for item_idx, placement_mask in taken:171        taken_indices.add(item_idx)172        item = items_for_solver[item_idx]173        orient = ORIENTATION_BY_ID[item['orientation_id']]174        positions = []175        for r in range(ROWS):176            for c in range(COLS):177                if placement_mask & cell_to_bit(r, c):178                    positions.append((r, c))179                    grid[r][c] = item['orientation_id']180        # Цвет на основе client_id181        hue = (item['client_id'] * 30) % 360182        color = f'hsl({hue}, 70%, 60%)'183 184        used_items.append({185            "clientId": item['client_id'],186            "orientation_id": item['orientation_id'],187            "value": item['value'],188            "color": color,189            "positions": positions,190            "type_id": item['type_id'],191            "special": item['special']192        })193 194    # Формирование unusedItems195    unused_items = []196    for idx, item in enumerate(items_for_solver):197        if idx not in taken_indices:198            unused_items.append({199                "clientId": item['client_id'],200                "orientation_id": item['orientation_id'],201                "value": item['value'],202                "color": "#cccccc",203                "positions": [],204                "type_id": item['type_id'],205                "special": item['special']206            })207 208    return {209        "grid": grid,210        "totalValue": total_value,211        "usedItems": used_items,212        "unusedItems": unused_items213    }214    215# ---------- Эндпоинты для редактирования форм ----------216@app.get("/forms-definition")217async def get_forms_definition():218    """Возвращает текущий JSON с базовыми формами."""219    try:220        with open(shapes.FORMS_FILE, "r") as f:221            data = json.load(f)222        return JSONResponse(content=data)223    except FileNotFoundError:224        # Если файл не найден, создаём дефолтный и возвращаем его225        shapes.load_base_forms()  # пересоздаст файл226        with open(shapes.FORMS_FILE, "r") as f:227            data = json.load(f)228        return JSONResponse(content=data)229    except Exception as e:230        raise HTTPException(status_code=500, detail=str(e))231 232@app.post("/forms-definition")233async def update_forms_definition(request: Request):234    """Принимает новый JSON с формами, сохраняет и перезагружает ориентации."""235    try:236        new_data = await request.json()237    except Exception:238        raise HTTPException(status_code=400, detail="Неверный JSON")239 240    if not isinstance(new_data, dict):241        raise HTTPException(status_code=400, detail="Данные должны быть объектом")242 243    for key, value in new_data.items():244        if not isinstance(key, str) or not key.isdigit():245            raise HTTPException(status_code=400, detail="Ключи должны быть числовыми строками")246        if not isinstance(value, dict) or "name" not in value or "matrix" not in value:247            raise HTTPException(status_code=400, detail="Каждая форма должна содержать name и matrix")248        if not isinstance(value["matrix"], list) or not all(isinstance(row, list) for row in value["matrix"]):249            raise HTTPException(status_code=400, detail="matrix должен быть списком списков")250 251    # Сохраняем в файл252    with open(shapes.FORMS_FILE, "w") as f:253        json.dump(new_data, f, indent=2, ensure_ascii=False)254 255    # Перезагружаем базовые формы и ориентации256    shapes.BASE_POLYOMINOS = shapes.load_base_forms()257    shapes.reload_orientations()258 259    return {"status": "ok", "message": "Формы обновлены"}260 261if __name__ == "__main__":262    port = int(os.environ.get("PORT", 8000))263    uvicorn.run("app:app", host="0.0.0.0", port=port, reload=True)264