Yamari/Knapsack
0
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 