feat(core): add what the web redesign needs
ThemeManager takes its list of themes, PitAnimator marks animated pits with PitViewModel.highlight, getSowingPath lists the pits a move sows into, and new texts in English and Turkish.
This commit is contained in:
parent
61d169ecdc
commit
8ad833cdbb
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Work through the tasks in order; each ends with its checks passing and a commit. Tick a task when it is committed.
|
Work through the tasks in order; each ends with its checks passing and a commit. Tick a task when it is committed.
|
||||||
|
|
||||||
- [ ] 1. **Core**: `ThemeManager` takes its themes; `PitViewModel.highlight` set by `PitAnimator`; `getSowingPath`
|
- [x] 1. **Core**: `ThemeManager` takes its themes; `PitViewModel.highlight` set by `PitAnimator`; `getSowingPath`
|
||||||
with unit tests; new texts (en, tr).
|
with unit tests; new texts (en, tr).
|
||||||
- [ ] 2. **Backend**: `GET /games/popular?kind=finished`; test.
|
- [ ] 2. **Backend**: `GET /games/popular?kind=finished`; test.
|
||||||
- [ ] 3. **Web app shell**: the three themes, font, base styles, `AppHeader` (logo, theme swatches, account menu,
|
- [ ] 3. **Web app shell**: the three themes, font, base styles, `AppHeader` (logo, theme swatches, account menu,
|
||||||
|
|||||||
@ -120,28 +120,34 @@ export class PitAnimator {
|
|||||||
if (gameStep.type === GAME_STEP_GAME_MOVE) {
|
if (gameStep.type === GAME_STEP_GAME_MOVE) {
|
||||||
pitViewModel.stoneCount += 1;
|
pitViewModel.stoneCount += 1;
|
||||||
pitViewModel.pitColor = theme.pitGameMoveAnimateColor;
|
pitViewModel.pitColor = theme.pitGameMoveAnimateColor;
|
||||||
|
pitViewModel.highlight = "sow";
|
||||||
} else if (gameStep.type === GAME_STEP_LAST_STONE_IN_EMPTY_PIT) {
|
} else if (gameStep.type === GAME_STEP_LAST_STONE_IN_EMPTY_PIT) {
|
||||||
pitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor;
|
pitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor;
|
||||||
|
pitViewModel.highlight = "capture";
|
||||||
pitViewModel.stoneCount = 0;
|
pitViewModel.stoneCount = 0;
|
||||||
const oppositeIndex = this.mancalaGame.board.getPitIndexCircularly(
|
const oppositeIndex = this.mancalaGame.board.getPitIndexCircularly(
|
||||||
gameStep.data.oppositeIndex
|
gameStep.data.oppositeIndex
|
||||||
);
|
);
|
||||||
const oppositePitViewModel = boardViewModel.pits[oppositeIndex];
|
const oppositePitViewModel = boardViewModel.pits[oppositeIndex];
|
||||||
oppositePitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor;
|
oppositePitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor;
|
||||||
|
oppositePitViewModel.highlight = "capture";
|
||||||
oppositePitViewModel.stoneCount = 0;
|
oppositePitViewModel.stoneCount = 0;
|
||||||
} else if (gameStep.type === GAME_STEP_LAST_STONE_IN_BANK) {
|
} else if (gameStep.type === GAME_STEP_LAST_STONE_IN_BANK) {
|
||||||
pitViewModel.pitColor = theme.pitLastStoneInBankPitAnimateColor;
|
pitViewModel.pitColor = theme.pitLastStoneInBankPitAnimateColor;
|
||||||
|
pitViewModel.highlight = "store";
|
||||||
} else if (gameStep.type === GAME_STEP_BOARD_CLEARED) {
|
} else if (gameStep.type === GAME_STEP_BOARD_CLEARED) {
|
||||||
for (const index of gameStep.data.pitIndexesThatHasStone) {
|
for (const index of gameStep.data.pitIndexesThatHasStone) {
|
||||||
const oppositeIndex = this.mancalaGame.board.getPitIndexCircularly(index);
|
const oppositeIndex = this.mancalaGame.board.getPitIndexCircularly(index);
|
||||||
const oppositePitViewModel = boardViewModel.pits[oppositeIndex];
|
const oppositePitViewModel = boardViewModel.pits[oppositeIndex];
|
||||||
oppositePitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor;
|
oppositePitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor;
|
||||||
|
oppositePitViewModel.highlight = "capture";
|
||||||
oppositePitViewModel.stoneCount = 0;
|
oppositePitViewModel.stoneCount = 0;
|
||||||
}
|
}
|
||||||
} else if (gameStep.type === GAME_STEP_DOUBLE_STONE_IN_PIT) {
|
} else if (gameStep.type === GAME_STEP_DOUBLE_STONE_IN_PIT) {
|
||||||
const _index = this.mancalaGame.board.getPitIndexCircularly(index);
|
const _index = this.mancalaGame.board.getPitIndexCircularly(index);
|
||||||
const pitViewModel = boardViewModel.pits[_index];
|
const pitViewModel = boardViewModel.pits[_index];
|
||||||
pitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor;
|
pitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor;
|
||||||
|
pitViewModel.highlight = "capture";
|
||||||
pitViewModel.stoneCount = 0;
|
pitViewModel.stoneCount = 0;
|
||||||
}
|
}
|
||||||
pitViewModel.stoneColor = getColorByBrightness(
|
pitViewModel.stoneColor = getColorByBrightness(
|
||||||
|
|||||||
@ -64,6 +64,16 @@ export function describeMove(moves: PlayedMove[], moveNumber: number): MoveDescr
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Board indexes the stones of the given pit are dropped into, in order, as Board.move sows them: a single stone goes
|
||||||
|
// to the next pit; otherwise the first stone goes back into the played pit. Every pit and store is passed, the
|
||||||
|
// opponent's store too. The last index is where the last stone lands. Empty for an empty pit or a store.
|
||||||
|
export function getSowingPath(stones: number[], pitIndex: number): number[] {
|
||||||
|
const stoneCount = stones[pitIndex];
|
||||||
|
if (!stoneCount || referenceBoard.checkPitTypeIsBankByIndex(pitIndex)) return [];
|
||||||
|
const start = stoneCount === 1 ? pitIndex + 1 : pitIndex;
|
||||||
|
return Array.from({ length: stoneCount }, (_, step) => (start + step) % stones.length);
|
||||||
|
}
|
||||||
|
|
||||||
// The game as it was after the given move, for showing or animating that position (e.g. with PitAnimator,
|
// The game as it was after the given move, for showing or animating that position (e.g. with PitAnimator,
|
||||||
// whose last history entry is the move it animates). Only for display: the rebuilt game has no rules.
|
// whose last history entry is the move it animates). Only for display: the rebuilt game has no rules.
|
||||||
export function createGameAtMove(game: Game, moves: PlayedMove[], moveNumber: number): Game {
|
export function createGameAtMove(game: Game, moves: PlayedMove[], moveNumber: number): Game {
|
||||||
|
|||||||
@ -72,4 +72,38 @@ export const en = {
|
|||||||
WatchFromStart: "Watch from start",
|
WatchFromStart: "Watch from start",
|
||||||
Speed: "Speed",
|
Speed: "Speed",
|
||||||
Duration: "Duration",
|
Duration: "Duration",
|
||||||
|
You: "You",
|
||||||
|
Opponent: "Opponent",
|
||||||
|
StoreCount: "Store {{count}}",
|
||||||
|
Online: "Online",
|
||||||
|
Offline: "Offline",
|
||||||
|
ChooseTheme: "Choose theme",
|
||||||
|
ThemeWalnut: "Walnut theme",
|
||||||
|
ThemeNight: "Night theme",
|
||||||
|
ThemeJade: "Jade theme",
|
||||||
|
Account: "Account",
|
||||||
|
NewestFirst: "newest first",
|
||||||
|
BeingPlayedNow: "Being played now",
|
||||||
|
MostWatched: "Most watched",
|
||||||
|
YourGames: "Your games",
|
||||||
|
GameWithOpponent: "Your game with {{name}} is on",
|
||||||
|
StoreScores: "Your store {{mine}}, opponent's {{theirs}}",
|
||||||
|
Live: "Live",
|
||||||
|
Close: "Close",
|
||||||
|
Play: "Play",
|
||||||
|
Pause: "Pause",
|
||||||
|
FirstMove: "Go to start",
|
||||||
|
PreviousMove: "Previous move",
|
||||||
|
NextMove: "Next move",
|
||||||
|
LastMove: "Go to end",
|
||||||
|
MovePosition: "Move position",
|
||||||
|
PlaybackControls: "Playback controls",
|
||||||
|
PlaybackSpeed: "Playback speed",
|
||||||
|
GameSummary: "Game summary",
|
||||||
|
StartPosition: "4 stones in every pit",
|
||||||
|
MoveCount: "{{count}} moves",
|
||||||
|
PitLabel: "{{name}}, pit {{pit}}, {{count}} stones",
|
||||||
|
StoreLabel: "{{name}}'s store, {{count}} stones",
|
||||||
|
OpenGame: "Open the game",
|
||||||
|
Score: "Score",
|
||||||
}
|
}
|
||||||
@ -72,4 +72,38 @@ export const tr = {
|
|||||||
WatchFromStart: "Baştan izle",
|
WatchFromStart: "Baştan izle",
|
||||||
Speed: "Hız",
|
Speed: "Hız",
|
||||||
Duration: "Süre",
|
Duration: "Süre",
|
||||||
|
You: "Sen",
|
||||||
|
Opponent: "Rakip",
|
||||||
|
StoreCount: "Hazine {{count}}",
|
||||||
|
Online: "Çevrimiçi",
|
||||||
|
Offline: "Çevrimdışı",
|
||||||
|
ChooseTheme: "Tema seç",
|
||||||
|
ThemeWalnut: "Ceviz tema",
|
||||||
|
ThemeNight: "Gece tema",
|
||||||
|
ThemeJade: "Yeşim tema",
|
||||||
|
Account: "Hesap",
|
||||||
|
NewestFirst: "en yeni üstte",
|
||||||
|
BeingPlayedNow: "Şu an oynanıyor",
|
||||||
|
MostWatched: "En çok izlenenler",
|
||||||
|
YourGames: "Senin maçların",
|
||||||
|
GameWithOpponent: "{{name}} ile oyunun sürüyor",
|
||||||
|
StoreScores: "Hazinen {{mine}}, rakibin {{theirs}}",
|
||||||
|
Live: "Canlı",
|
||||||
|
Close: "Kapat",
|
||||||
|
Play: "Oynat",
|
||||||
|
Pause: "Duraklat",
|
||||||
|
FirstMove: "Başa dön",
|
||||||
|
PreviousMove: "Önceki hamle",
|
||||||
|
NextMove: "Sonraki hamle",
|
||||||
|
LastMove: "Sona git",
|
||||||
|
MovePosition: "Hamle konumu",
|
||||||
|
PlaybackControls: "Oynatma kontrolleri",
|
||||||
|
PlaybackSpeed: "Oynatma hızı",
|
||||||
|
GameSummary: "Oyun özeti",
|
||||||
|
StartPosition: "Her kuyuda 4 taş",
|
||||||
|
MoveCount: "{{count}} hamle",
|
||||||
|
PitLabel: "{{name}}, {{pit}}. kuyu, {{count}} taş",
|
||||||
|
StoreLabel: "{{name}} hazinesi, {{count}} taş",
|
||||||
|
OpenGame: "Oyunu aç",
|
||||||
|
Score: "Skor",
|
||||||
};
|
};
|
||||||
@ -14,10 +14,13 @@ export type ThemeManagerEvents = "themechange";
|
|||||||
export class ThemeManager extends TinyEmitter {
|
export class ThemeManager extends TinyEmitter {
|
||||||
private _theme: Theme;
|
private _theme: Theme;
|
||||||
private _storage: any;
|
private _storage: any;
|
||||||
constructor(storage: Storage) {
|
private _themes: Theme[];
|
||||||
|
// The first theme is the default.
|
||||||
|
constructor(storage: Storage, themeList: Theme[] = themes) {
|
||||||
super();
|
super();
|
||||||
this._storage = storage;
|
this._storage = storage;
|
||||||
this._theme = this.readFromLocalStorage() || lightTheme;
|
this._themes = themeList;
|
||||||
|
this._theme = this.readFromLocalStorage() || themeList[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
public get theme() {
|
public get theme() {
|
||||||
@ -36,12 +39,12 @@ export class ThemeManager extends TinyEmitter {
|
|||||||
|
|
||||||
private readFromLocalStorage(): Theme | undefined {
|
private readFromLocalStorage(): Theme | undefined {
|
||||||
const themeID = this._storage.getString(THEME_ID);
|
const themeID = this._storage.getString(THEME_ID);
|
||||||
const theme = themes.find((eachTheme: Theme) => themeID === eachTheme.id);
|
const theme = this._themes.find((eachTheme: Theme) => themeID === eachTheme.id);
|
||||||
return theme;
|
return theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get themes(): Theme[] {
|
public get themes(): Theme[] {
|
||||||
return themes;
|
return this._themes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public on(event: ThemeManagerEvents, callback: (...value: any[]) => void): this {
|
public on(event: ThemeManagerEvents, callback: (...value: any[]) => void): this {
|
||||||
|
|||||||
@ -1,18 +1,25 @@
|
|||||||
|
// Why a pit is marked during a move animation: a stone was sown into it, stones were captured from it, the last stone
|
||||||
|
// landed in a store, or the player picked it and the move is on its way to the server.
|
||||||
|
export type PitHighlight = "sow" | "capture" | "store" | "selected";
|
||||||
|
|
||||||
export class PitViewModel {
|
export class PitViewModel {
|
||||||
id: string;
|
id: string;
|
||||||
stoneCount: number;
|
stoneCount: number;
|
||||||
stoneColor: string;
|
stoneColor: string;
|
||||||
pitColor: string;
|
pitColor: string;
|
||||||
|
highlight?: PitHighlight;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
id: string,
|
id: string,
|
||||||
stoneCount: number,
|
stoneCount: number,
|
||||||
stoneColor: string,
|
stoneColor: string,
|
||||||
pitColor: string
|
pitColor: string,
|
||||||
|
highlight?: PitHighlight
|
||||||
) {
|
) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.stoneCount = stoneCount;
|
this.stoneCount = stoneCount;
|
||||||
this.stoneColor = stoneColor;
|
this.stoneColor = stoneColor;
|
||||||
this.pitColor = pitColor;
|
this.pitColor = pitColor;
|
||||||
|
this.highlight = highlight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { CommonMancalaGame, MancalaGame } from "mancala.js";
|
import { CommonMancalaGame, GAME_STEP_GAME_MOVE, MancalaGame } from "mancala.js";
|
||||||
import { createGameAtMove, describeMove, getBoardAfterMove, getInitialBoard, PlayedMove } from "../../src/history";
|
import { createGameAtMove, describeMove, getBoardAfterMove, getInitialBoard, getSowingPath, PlayedMove } from "../../src/history";
|
||||||
import { Game } from "../../src/models";
|
import { Game } from "../../src/models";
|
||||||
|
|
||||||
const P1 = "player-1";
|
const P1 = "player-1";
|
||||||
@ -136,3 +136,46 @@ describe("createGameAtMove", () => {
|
|||||||
expect(start.history).toHaveLength(0);
|
expect(start.history).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getSowingPath", () => {
|
||||||
|
it("starts with the played pit, or the next pit for a single stone", () => {
|
||||||
|
const stones = getInitialBoard();
|
||||||
|
expect(getSowingPath(stones, 2)).toEqual([2, 3, 4, 5]);
|
||||||
|
stones[4] = 1;
|
||||||
|
expect(getSowingPath(stones, 4)).toEqual([5]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes both stores and wraps around the board", () => {
|
||||||
|
const stones = getInitialBoard();
|
||||||
|
stones[12] = 4;
|
||||||
|
expect(getSowingPath(stones, 12)).toEqual([12, 13, 0, 1]);
|
||||||
|
stones[5] = 10;
|
||||||
|
expect(getSowingPath(stones, 5)).toEqual([5, 6, 7, 8, 9, 10, 11, 12, 13, 0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is empty for an empty pit or a store", () => {
|
||||||
|
const stones = getInitialBoard();
|
||||||
|
stones[3] = 0;
|
||||||
|
expect(getSowingPath(stones, 3)).toEqual([]);
|
||||||
|
expect(getSowingPath(stones, 6)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches the pits the engine sows into", () => {
|
||||||
|
const random = seededRandom(7);
|
||||||
|
for (let gameNumber = 0; gameNumber < 20; gameNumber++) {
|
||||||
|
const game = newGame();
|
||||||
|
while (game.state !== "ended") {
|
||||||
|
const before = game.board.getStoneArray();
|
||||||
|
const playable = [0, 1, 2, 3, 4, 5].filter((pit) =>
|
||||||
|
before[game.getBoardIndexByPlayerId(game.turnPlayerId, pit)] > 0);
|
||||||
|
const pit = playable[Math.floor(random() * playable.length)];
|
||||||
|
const boardIndex = game.getBoardIndexByPlayerId(game.turnPlayerId, pit);
|
||||||
|
game.moveByPlayerPit(game.turnPlayerId, pit);
|
||||||
|
const sown = game.history[game.history.length - 1].gameSteps
|
||||||
|
.filter((step) => step.type === GAME_STEP_GAME_MOVE)
|
||||||
|
.map((step) => game.board.getPitIndexCircularly(step.index));
|
||||||
|
expect(getSowingPath(before, boardIndex)).toEqual(sown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user