fix(mera/лендинг): итог игры «Проверьте себя» говорит, что квартиры взяты из отобранной полосы (#2903)
All checks were successful
CI Trade-In / frontend-checks (pull_request) Successful in 2m32s
CI Trade-In / backend-tests (pull_request) Successful in 7m1s
CI Trade-In / changes (pull_request) Successful in 14s
CI / changes (pull_request) Successful in 17s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
All checks were successful
CI Trade-In / frontend-checks (pull_request) Successful in 2m32s
CI Trade-In / backend-tests (pull_request) Successful in 7m1s
CI Trade-In / changes (pull_request) Successful in 14s
CI / changes (pull_request) Successful in 17s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Раунды игры — строки витрины, а витрина с 12.09 отобрана по ошибке МЕРЫ (полоса -5..+20 %). Плитка «ошибка МЕРЫ на этих же квартирах» выходила без оговорки и читалась как точность расчёта: на проде 17.09 медиана |ошибки| двадцати строк 6,5 %. Лента и таблица «Точность» полосу называли, игра — нет. Теперь итог печатает медиану по всей сверке (landing-facts) и, если все строки витрины лежат в полосе (allWithinBand, как у ленты и таблицы), говорит, что квартиры из отобранной полосы, а не из всей сверки. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
71d60ff13e
commit
4dffb2d1fb
5 changed files with 94 additions and 7 deletions
|
|
@ -216,12 +216,12 @@ describe("карточка игры выбирает карту по налич
|
|||
});
|
||||
|
||||
it("схема есть — показана улица", () => {
|
||||
const { container } = render(<GuessGameV3 rounds={[toGameRound(deal(SCHEME), 0)]} />);
|
||||
const { container } = render(<GuessGameV3 rounds={[toGameRound(deal(SCHEME), 0)]} bandSelected />);
|
||||
expect(container.textContent).toContain("ДОМ НЕ ИЗВЕСТЕН");
|
||||
});
|
||||
|
||||
it("схемы нет — остаётся район, а не пустое место", () => {
|
||||
const { container } = render(<GuessGameV3 rounds={[toGameRound(deal(null), 0)]} />);
|
||||
const { container } = render(<GuessGameV3 rounds={[toGameRound(deal(null), 0)]} bandSelected />);
|
||||
expect(container.textContent).toContain("ЛЕНИНСКИЙ");
|
||||
expect(container.textContent).not.toContain("ДОМ НЕ ИЗВЕСТЕН");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@
|
|||
* ИСЧЕЗАЕТ, а не показывает ноль, прочерк или подпись без числа. Отказ ручки
|
||||
* `/stats` в проде выглядит именно так: пустой объект.
|
||||
*/
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { AccuracyV3 } from "../_components/v3/AccuracyV3";
|
||||
import { DealsTickerV3 } from "../_components/v3/DealsTickerV3";
|
||||
import { GuessGameV3 } from "../_components/v3/GuessGameV3";
|
||||
import { toGameRound } from "../_components/v3/deal-view";
|
||||
import { CostOfErrorV3 } from "../_components/v3/CostOfErrorV3";
|
||||
import { HeroV3 } from "../_components/v3/HeroV3";
|
||||
import { BACKTEST } from "../landing-facts";
|
||||
|
|
@ -315,3 +317,57 @@ describe("лента сделок: контекст разброса", () => {
|
|||
expect(note.textContent).toContain("75,7 %");
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* ИТОГ ИГРЫ НАЗЫВАЕТ ОТБОР (#2903).
|
||||
*
|
||||
* Раунды — строки витрины, а витрина с 12.09.2026 отобрана по ошибке МЕРЫ
|
||||
* (полоса −5 %..+20 %). Плитка «ошибка МЕРЫ на этих же квартирах» без
|
||||
* оговорки читается как точность расчёта, хотя это лучший отобранный кусок:
|
||||
* на проде 17.09 медиана |ошибки| двадцати строк 6,5 %, по всей сверке
|
||||
* кратно больше. Проверяется по тексту итога, до которого доходим кликами.
|
||||
*/
|
||||
describe("игра: итог называет отбор раундов", () => {
|
||||
const rounds = [0.58, -3.2, 17.9].map((err_pct, i) =>
|
||||
toGameRound(
|
||||
{
|
||||
...SHOWCASE.deals[0],
|
||||
rooms: i,
|
||||
err_pct,
|
||||
predicted_rub: Math.round(SHOWCASE.deals[0].fact_rub * (1 + err_pct / 100)),
|
||||
},
|
||||
i,
|
||||
),
|
||||
);
|
||||
|
||||
const finish = () => {
|
||||
for (let i = 0; i < rounds.length; i += 1) {
|
||||
fireEvent.click(screen.getByRole("button", { name: "Ответить" }));
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", {
|
||||
name: i === rounds.length - 1 ? "Показать итог" : "Следующая квартира →",
|
||||
}),
|
||||
);
|
||||
}
|
||||
return screen.getByText(/Медиана расхождения МЕРЫ по всей сверке/u);
|
||||
};
|
||||
|
||||
it("строки витрины в полосе → итог говорит, что квартиры из отобранной полосы, и печатает медиану сверки", () => {
|
||||
render(<GuessGameV3 rounds={rounds} bandSelected />);
|
||||
const note = finish();
|
||||
expect(screen.getByText("ошибка МЕРЫ на этих же квартирах")).toBeTruthy();
|
||||
expect(
|
||||
note.textContent,
|
||||
"итог сравнивает игрока с МЕРОЙ на отобранных по её ошибке квартирах и молчит об отборе",
|
||||
).toMatch(/отобранной полосы.*от -5 % до \+20 %.*не вся сверка/u);
|
||||
expect(note.textContent).toContain(BACKTEST.priceError.text);
|
||||
expect(note.textContent).toContain(String(BACKTEST.priceError.sampleN));
|
||||
});
|
||||
|
||||
it("строки витрины вне полосы → про полосу молчит, медиана сверки остаётся", () => {
|
||||
render(<GuessGameV3 rounds={rounds} bandSelected={false} />);
|
||||
const note = finish();
|
||||
expect(note.textContent).not.toMatch(/полос/u);
|
||||
expect(note.textContent).toContain(BACKTEST.priceError.text);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,12 +27,22 @@
|
|||
* — той же формулой, что и ошибка МЕРЫ на этих же сделках (`err_pct` витрины).
|
||||
* Обе величины меряют одно и то же на одной выборке, поэтому их и можно
|
||||
* ставить рядом.
|
||||
*
|
||||
* НО ВЫБОРКА ОТОБРАНА ПО ОШИБКЕ МЕРЫ (#2903). С 12.09.2026 на витрину, а
|
||||
* значит и в раунды, попадают только сделки с расхождением от −5 % до +20 %
|
||||
* (`select_rows` в app/tasks/landing_showcase_deals.py). «Ошибка МЕРЫ на этих
|
||||
* же квартирах» без оговорки читается как точность расчёта, а это лучший
|
||||
* отобранный кусок. Поэтому итог называет полосу и держит рядом медиану по
|
||||
* всей сверке — как подписи ленты и таблицы. Про полосу говорим, только если
|
||||
* её подтверждают сами строки витрины (`bandSelected` = `allWithinBand`,
|
||||
* считает родитель: раунды — лишь три из этих строк).
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { BACKTEST } from "../../landing-facts";
|
||||
import styles from "../../landing-v3.module.css";
|
||||
import type { GameRound } from "./deal-view";
|
||||
import { BAND_MAX_PCT, BAND_MIN_PCT, count, errPct, type GameRound } from "./deal-view";
|
||||
import { DealMapV3 } from "./DealMapV3";
|
||||
import { StreetMapV3 } from "./StreetMapV3";
|
||||
|
||||
|
|
@ -49,7 +59,14 @@ function median(xs: number[]): number {
|
|||
return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
|
||||
}
|
||||
|
||||
export function GuessGameV3({ rounds }: { rounds: readonly GameRound[] }) {
|
||||
export function GuessGameV3({
|
||||
rounds,
|
||||
bandSelected,
|
||||
}: {
|
||||
rounds: readonly GameRound[];
|
||||
/** Все строки витрины лежат в полосе — значит раунды взяты из отобранной полосы. */
|
||||
bandSelected: boolean;
|
||||
}) {
|
||||
const [idx, setIdx] = useState(0);
|
||||
const [phase, setPhase] = useState<Phase>("playing");
|
||||
const [guess, setGuess] = useState(rounds[0].start);
|
||||
|
|
@ -188,6 +205,13 @@ export function GuessGameV3({ rounds }: { rounds: readonly GameRound[] }) {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className={styles.gameFinalNote}>
|
||||
{`${
|
||||
bandSelected
|
||||
? `Квартиры для игры взяты из отобранной полосы, где МЕРА разошлась с ценой ДКП от ${errPct(BAND_MIN_PCT)} до ${errPct(BAND_MAX_PCT)}: это не вся сверка. `
|
||||
: ""
|
||||
}Медиана расхождения МЕРЫ по всей сверке — ${BACKTEST.priceError.text} (${count(BACKTEST.priceError.sampleN)} сделок).`}
|
||||
</p>
|
||||
<div className={styles.gameVerdict}>
|
||||
Рынок сложно угадать на глаз — для этого и существует расчёт
|
||||
по данным.
|
||||
|
|
|
|||
|
|
@ -1920,6 +1920,11 @@
|
|||
.gameFinal {
|
||||
text-align: center;
|
||||
}
|
||||
/* Оговорка итога игры (#2903): отбор раундов и медиана всей сверки. */
|
||||
.gameFinalNote {
|
||||
composes: accFootnote;
|
||||
margin: 0 0 18px;
|
||||
}
|
||||
.gameFinalGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { StepsV3 } from "./_components/v3/StepsV3";
|
|||
import { StickyCtaV3 } from "./_components/v3/StickyCtaV3";
|
||||
import { TwoPathsV3 } from "./_components/v3/TwoPathsV3";
|
||||
import { WhatYouGetV3 } from "./_components/v3/WhatYouGetV3";
|
||||
import { pickVariedDeals, toGameRound } from "./_components/v3/deal-view";
|
||||
import { allWithinBand, pickVariedDeals, toGameRound } from "./_components/v3/deal-view";
|
||||
import { serviceNode } from "./_lib/schema";
|
||||
import { fetchLandingStats, fetchShowcase } from "./public-api";
|
||||
import styles from "./landing.module.css";
|
||||
|
|
@ -120,7 +120,9 @@ export default async function MeraPublicPage() {
|
|||
<AccuracyV3 stats={stats} showcase={showcase} />
|
||||
<SourcesV3 />
|
||||
<WhatYouGetV3 />
|
||||
{rounds.length >= MIN_ROUNDS && <GuessGameV3 rounds={rounds} />}
|
||||
{rounds.length >= MIN_ROUNDS && (
|
||||
<GuessGameV3 rounds={rounds} bandSelected={allWithinBand(deals)} />
|
||||
)}
|
||||
<CostOfErrorV3 stats={stats} />
|
||||
<TwoPathsV3 />
|
||||
<ObjectionsV3 />
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue