Skip to content

Commit

Permalink
Merge branch 'release/1.1.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
web-flow committed Oct 28, 2024
2 parents dedbba7 + 3b0dc9b commit ece1cc5
Show file tree
Hide file tree
Showing 17 changed files with 299 additions and 5 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@brdgm/civolution-solo-helper",
"version": "1.0.1",
"version": "1.1.0",
"private": true,
"description": "Civolution Solo Helper",
"appDeployName": "civolution",
Expand Down
Binary file added src/assets/icons/map-tile/a.webp
Binary file not shown.
Binary file added src/assets/icons/map-tile/b.webp
Binary file not shown.
Binary file added src/assets/icons/map-tile/c.webp
Binary file not shown.
Binary file added src/assets/icons/map-tile/d.webp
Binary file not shown.
Binary file added src/assets/icons/map-tile/e.webp
Binary file not shown.
Binary file added src/assets/icons/map-tile/f.webp
Binary file not shown.
Binary file added src/assets/icons/map-tile/g.webp
Binary file not shown.
Binary file added src/assets/icons/map-tile/h.webp
Binary file not shown.
102 changes: 102 additions & 0 deletions src/components/setup/RandomMap.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<template>
<h3 class="mt-4 mb-3">{{t('setup.randomMap.title')}}</h3>

<p v-if="!show">
<button class="btn btn-sm btn-outline-secondary" @click="showRandomizer">{{t('setup.randomMap.show')}}</button>
</p>
<div v-if="show">
<div class="map">
<div v-for="item of tilePlacements" :key="item.tile.name"
:style="{position:'absolute',left:`${item.x*100}px`,top:`${item.y*100}px`}">
<div class="letter">{{item.tile.name}}</div>
<AppIcon type="map-tile" :name="item.tile.name"
:style="{width:'100px','transform-origin':'50px 50px',transform:getTransform(item)}"/>
</div>
</div>

<div class="d-flex mt-2">
<button class="btn btn-sm btn-outline-secondary" @click="randomizeMapTiles">{{t('setup.randomMap.randomize')}}</button>
<div class="form-check ms-2">
<label class="form-check-label small">
<input class="form-check-input" type="checkbox" :value="true" v-model="state.setup.mapFirstPlay" @change="randomizeMapTiles">
{{t('setup.randomMap.firstPlay')}}
</label>
</div>
</div>
</div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import AppIcon from '../structure/AppIcon.vue'
import { useStateStore } from '@/store/state';
import MapRandomizer, { Rotation, TilePlacement } from '@/services/MapRandomizer';
export default defineComponent({
name: 'RandomMap',
components: {
AppIcon
},
setup() {
const { t } = useI18n()
const state = useStateStore()
const tilePlacements = ref([] as readonly TilePlacement[])
const randomizeMapTiles = function() {
const mapRandomizer = new MapRandomizer(state.setup.mapFirstPlay ?? true)
mapRandomizer.randomize()
tilePlacements.value = mapRandomizer.tiles
}
return { t, state, tilePlacements, randomizeMapTiles }
},
data() {
return {
show: false
}
},
methods: {
showRandomizer() : void {
this.randomizeMapTiles()
this.show = true
},
getTransform(tilePlacement: TilePlacement) : string {
let x = 0
let y = 0
if (tilePlacement.rotation == Rotation.RIGHT) {
x = 100
}
else if (tilePlacement.rotation == Rotation.DOWN) {
y = 100
}
return `translate(${x}px,${y}px) rotate(${tilePlacement.rotation}deg)`
}
}
})
</script>

<style lang="scss" scoped>
.map {
position: relative;
border: 1px solid #aaa;
width: 501px;
max-width: 100%;
height: 301px;
overflow-x: auto;
overflow-y: hidden;
border-radius: 10px;
background-color: #eee;
.letter {
position: absolute;
top: 50px;
left: 50%;
transform: translate(-50%, -50%);
text-transform: uppercase;
z-index: 100;
font-size: 20px;
font-weight: bold;
text-shadow: -2px -2px 0 #fff, 2px -2px 0 #fff, -2px 2px 0 #fff, 2px 2px 0 #fff;
}
}
</style>
6 changes: 6 additions & 0 deletions src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
"scoringTiles": {
"title": "Wertungsplättchen",
"randomize": "Neu wählen"
},
"randomMap": {
"title": "Kontinent",
"show": "Zufälliger Kontinent (optional)",
"firstPlay": "Erste Partie",
"randomize": "New wählen"
}
},
"setupBot": {
Expand Down
6 changes: 6 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
"scoringTiles": {
"title": "Scoring Tiles",
"randomize": "Randomize"
},
"randomMap": {
"title": "Continent",
"show": "Random Continent (optional)",
"firstPlay": "First Play",
"randomize": "Randomize"
}
},
"setupBot": {
Expand Down
159 changes: 159 additions & 0 deletions src/services/MapRandomizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import rollDice from '@brdgm/brdgm-commons/src/util/random/rollDice'

/**
* Manages the bot actions.
*/
export default class MapRandomizer {

readonly _firstPlay : boolean
private _tiles : TilePlacement[] = []

public constructor(firstPlay : boolean) {
this._firstPlay = firstPlay
}

public randomize() : void {
let retryCount = 0
while (retryCount < MAX_RETRIES) {
this._tiles = []
if (this._firstPlay) {
this._tiles.push({ tile: MAP_TILES[0], x: 1, y: 1, rotation: Rotation.UP })
this._tiles.push({ tile: MAP_TILES[1], x: 2, y: 0, rotation: Rotation.UP })
this._tiles.push({ tile: MAP_TILES[2], x: 1, y: 2, rotation: Rotation.RIGHT })
}
let failed = false
for (let i=this._firstPlay ? 3 : 0; i<MAP_TILES.length; i++) {
const positions = this.findValidPositions(MAP_TILES[i])
if (positions.length > 0) {
const index = rollDice(positions.length) - 1
this._tiles.push(positions[index])
}
else {
failed = true
break
}
}
if (failed) {
retryCount++
}
else {
return
}
}
throw new Error('Unable to find valid map composition.')
}

private findValidPositions(tile: MapTile) : TilePlacement[] {
if (tile.length == 1) {
return this.findValidPositions_1Length(tile)
}
else {
return this.findValidPositions_2Length(tile)
}
}

private findValidPositions_1Length(tile: MapTile) : TilePlacement[] {
const positions : TilePlacement[] = []
const matrix = getMatrix(this._tiles)
for (let x=0; x<5; x++) {
for (let y=0;y<3;y++) {
if (!matrix[x][y]) {
positions.push({ tile, x, y, rotation: Rotation.UP })
}
}
}
return positions
}

private findValidPositions_2Length(tile: MapTile) : TilePlacement[] {
const positions : TilePlacement[] = []
const matrix = getMatrix(this._tiles)
// horizontal
for (let x=0; x<4; x++) {
for (let y=0;y<3;y++) {
if (!matrix[x][y] && !matrix[x+1][y]) {
positions.push({ tile, x, y, rotation: Rotation.RIGHT })
positions.push({ tile, x, y, rotation: Rotation.LEFT })
}
}
}
// vertical
for (let x=0; x<5; x++) {
for (let y=0;y<2;y++) {
if (!matrix[x][y] && !matrix[x][y+1]) {
positions.push({ tile, x, y, rotation: Rotation.UP })
positions.push({ tile, x, y, rotation: Rotation.DOWN })
}
}
}
return positions
}

public get tiles() : readonly TilePlacement[] {
return this._tiles
}

}

const MAX_RETRIES = 1000

const MAP_TILES : MapTile[] = [
{ name: 'a', length: 1 },
{ name: 'b', length: 2 },
{ name: 'c', length: 2 },
{ name: 'd', length: 2 },
{ name: 'e', length: 2 },
{ name: 'f', length: 2 },
{ name: 'g', length: 2 },
{ name: 'h', length: 2 },
]

export interface MapTile {
name: string
length: number
}

export interface TilePlacement {
tile: MapTile
x: number
y: number
rotation: Rotation
}

export enum Rotation {
UP = 0,
RIGHT = 90,
DOWN = 180,
LEFT = 270
}

function getMatrix(tilePlacements : TilePlacement[]) : boolean[][] {
const matrix : boolean[][] = []
for (let x=0; x<5; x++) {
matrix[x] = [false,false,false]
}
for (const tilePlacement of tilePlacements) {
const { x1, x2, y1, y2 } = getCoordinates(tilePlacement)
matrix[x1][y1] = true
matrix[x1][y2] = true
matrix[x2][y1] = true
matrix[x2][y2] = true
}
return matrix
}

function getCoordinates(tilePlacement : TilePlacement) : { x1: number, x2: number, y1: number, y2: number } {
const x1 = tilePlacement.x
const y1 = tilePlacement.y
let x2 = x1
let y2 = y1
if (tilePlacement.tile.length == 2) {
if (tilePlacement.rotation == Rotation.LEFT || tilePlacement.rotation == Rotation.RIGHT) {
x2 = x1 + 1
}
else {
y2 = y2 + 1
}
}
return { x1, x2, y1, y2 }
}
4 changes: 3 additions & 1 deletion src/store/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export const useStateStore = defineStore(`${name}.state`, {
setup: {
difficultyLevel: DifficultyLevel.BEGINNER,
eraScoringTiles: [],
finalScoringTiles: []
finalScoringTiles: [],
mapFirstPlay: true
},
rounds: []
} as State
Expand Down Expand Up @@ -50,6 +51,7 @@ export interface Setup {
difficultyLevel: DifficultyLevel
eraScoringTiles: ScoringCategory[]
finalScoringTiles: ScoringCategory[]
mapFirstPlay?: boolean
initialCardDeck?: CardDeckPersistence
debugMode?: boolean
}
Expand Down
5 changes: 4 additions & 1 deletion src/views/SetupGame.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

<DifficultyLevel/>
<ScoringTiles/>
<RandomMap/>

<button class="btn btn-primary btn-lg mt-4" @click="setupBot()">
{{t('setupBot.title')}}
Expand All @@ -18,13 +19,15 @@ import { useStateStore } from '@/store/state'
import FooterButtons from '@/components/structure/FooterButtons.vue'
import DifficultyLevel from '@/components/setup/DifficultyLevel.vue'
import ScoringTiles from '@/components/setup/ScoringTiles.vue'
import RandomMap from '@/components/setup/RandomMap.vue'
export default defineComponent({
name: 'SetupGame',
components: {
FooterButtons,
DifficultyLevel,
ScoringTiles
ScoringTiles,
RandomMap
},
setup() {
const { t } = useI18n()
Expand Down
16 changes: 16 additions & 0 deletions tests/unit/services/MapRandomizer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { expect } from 'chai'
import MapRandomizer from '@/services/MapRandomizer'

describe('services/MapRandomizer', () => {
it('randomizeFirstPlay', () => {
const mapRandomizer = new MapRandomizer(true)
mapRandomizer.randomize()
expect(mapRandomizer.tiles.length).to.eq(8)
})

it('randomize', () => {
const mapRandomizer = new MapRandomizer(false)
mapRandomizer.randomize()
expect(mapRandomizer.tiles.length).to.eq(8)
})
})

0 comments on commit ece1cc5

Please sign in to comment.