package game import ( "strings" "unicode/utf8" "thehouseoficarus/internal/color" "thehouseoficarus/internal/net" "thehouseoficarus/internal/world" ) type mapGlyphs struct { topLeft, topRight rune bottomLeft, bottomRight rune side rune topFill rune connectorH, connectorV rune upArrow, downArrow rune leftArrow, rightArrow rune upRight, upLeft rune downRight, downLeft rune connectorNE, connectorNW rune } func mapGlyphsForPlayer(unicode bool) mapGlyphs { if unicode { return mapGlyphs{ topLeft: '╔', topRight: '╗', bottomLeft: '╚', bottomRight: '╝', side: '║', topFill: '═', connectorH: '-', connectorV: '│', upArrow: '↑', downArrow: '↓', leftArrow: '←', rightArrow: '→', upRight: '↗', upLeft: '↖', downRight: '↘', downLeft: '↙', connectorNE: '/', connectorNW: '\\', } } return mapGlyphs{ topLeft: '.', topRight: '.', bottomLeft: ':', bottomRight: ':', side: ':', topFill: '.', connectorH: '-', connectorV: '|', upArrow: '^', downArrow: 'v', leftArrow: '<', rightArrow: '>', upRight: '/', upLeft: '\\', downRight: '\\', downLeft: '/', connectorNE: '/', connectorNW: '\\', } } type mapCell struct { char rune spec color.ColorSpec } type mapGraph struct { posToRoom map[[2]int]int roomToPos map[int][2]int dist map[int]int } func buildGraph(g *Game, startRoomID int, visited map[int]bool) *mapGraph { mg := &mapGraph{ posToRoom: make(map[[2]int]int), roomToPos: make(map[int][2]int), dist: make(map[int]int), } type node struct { roomID int x, y int } queue := []node{{startRoomID, 0, 0}} mg.posToRoom[[2]int{0, 0}] = startRoomID mg.roomToPos[startRoomID] = [2]int{0, 0} mg.dist[startRoomID] = 0 for len(queue) > 0 { n := queue[0] queue = queue[1:] room, ok := loadRoom(g, n.roomID) if !ok { continue } if visited != nil && !visited[n.roomID] { continue } for dir, delta := range world.DirectionDeltas { targetID, ok := exitTarget(room, dir) if !ok { continue } if _, seen := mg.roomToPos[targetID]; seen { continue } nx, ny := n.x+delta[0], n.y+delta[1] mg.posToRoom[[2]int{nx, ny}] = targetID mg.roomToPos[targetID] = [2]int{nx, ny} mg.dist[targetID] = mg.dist[n.roomID] + 1 queue = append(queue, node{targetID, nx, ny}) } } return mg } func renderMapCells(grid [][]mapCell, colorMode string, startRow, endRow int, border rune) []string { lines := make([]string, 0, endRow-startRow) for row := startRow; row < endRow; row++ { var sb strings.Builder if border != 0 { sb.WriteRune(border) } for _, cell := range grid[row] { if cell.char == ' ' { sb.WriteRune(' ') } else if !cell.spec.Empty() { sb.WriteString(color.Render(colorMode, cell.spec, string(cell.char))) } else { sb.WriteRune(cell.char) } } if border != 0 { sb.WriteRune(border) } lines = append(lines, sb.String()) } return lines } func buildTinyMap(g *Game, sess *net.Session, roomID int, mg mapGlyphs) []string { visited := roomsVisited(sess) bg := buildGraph(g, roomID, visited) colorMode := colorModeFor(sess) atSpec := resolveMapAt(g, sess) dimSpec := resolveDim(g, sess) ctx := &mapRenderCtx{ g: g, sess: sess, bg: bg, visited: visited, currentRoom: roomID, atSpec: atSpec, dimSpec: dimSpec, blockedSpec: resolveMapBlocked(g, sess), mg: mg, diagPairs: make(map[[2]int][2]int), } grid := make([][]mapCell, 5) for i := range grid { grid[i] = make([]mapCell, 5) for j := range grid[i] { grid[i][j] = mapCell{char: ' '} } } for y := -1; y <= 1; y++ { for x := -1; x <= 1; x++ { pos := [2]int{x, y} rid, ok := bg.posToRoom[pos] if !ok { continue } gr := (y + 1) * 2 gc := (x + 1) * 2 if rid == roomID { grid[gr][gc] = mapCell{char: '@', spec: atSpec} } else { unvisited := visited != nil && !visited[rid] ch, spec := roomMapSymbol(g, sess, rid, unvisited) if unvisited { spec = dimSpec } grid[gr][gc] = mapCell{char: ch, spec: spec} } } } for y := -1; y <= 1; y++ { for x := -1; x <= 0; x++ { leftPos := [2]int{x, y} rightPos := [2]int{x + 1, y} leftRoom, leftOK := bg.posToRoom[leftPos] rightRoom, rightOK := bg.posToRoom[rightPos] if !leftOK || !rightOK { continue } if cell, ok := ctx.connectorCell(leftRoom, rightRoom, world.East, world.West); ok { grid[(y+1)*2][(x+1)*2+1] = cell } } } for y := -1; y <= 0; y++ { for x := -1; x <= 1; x++ { topPos := [2]int{x, y} bottomPos := [2]int{x, y + 1} topRoom, topOK := bg.posToRoom[topPos] bottomRoom, bottomOK := bg.posToRoom[bottomPos] if !topOK || !bottomOK { continue } if cell, ok := ctx.connectorCell(topRoom, bottomRoom, world.South, world.North); ok { grid[(y+1)*2+1][(x+1)*2] = cell } } } // NE connectors: (x,y) -> (x+1, y-1), connector at grid[2*y+1][2*x+3] for y := 0; y <= 1; y++ { for x := -1; x <= 0; x++ { aPos, bPos := [2]int{x, y}, [2]int{x + 1, y - 1} aRoom, aOK := bg.posToRoom[aPos] bRoom, bOK := bg.posToRoom[bPos] if !aOK || !bOK { continue } if cell, ok := ctx.connectorCell(aRoom, bRoom, world.Northeast, world.Southwest); ok { gr, gc := 2*y+1, 2*x+3 key := [2]int{gr, gc} if isDiagonalGlyph(grid[gr][gc].char) { if grid[gr][gc].char != cell.char { prev := ctx.diagPairs[key] spec := color.Average( color.Average(ctx.nodeSpec(prev[0]), ctx.nodeSpec(prev[1])), color.Average(ctx.nodeSpec(aRoom), ctx.nodeSpec(bRoom)), ) grid[gr][gc] = mapCell{char: 'X', spec: spec} } } else { grid[gr][gc] = cell ctx.diagPairs[key] = [2]int{aRoom, bRoom} } } } } // NW connectors: (x,y) -> (x-1, y-1), connector at grid[2*y+1][2*x+1] for y := 0; y <= 1; y++ { for x := 0; x <= 1; x++ { aPos, bPos := [2]int{x, y}, [2]int{x - 1, y - 1} aRoom, aOK := bg.posToRoom[aPos] bRoom, bOK := bg.posToRoom[bPos] if !aOK || !bOK { continue } if cell, ok := ctx.connectorCell(aRoom, bRoom, world.Northwest, world.Southeast); ok { gr, gc := 2*y+1, 2*x+1 key := [2]int{gr, gc} if isDiagonalGlyph(grid[gr][gc].char) { if grid[gr][gc].char != cell.char { prev := ctx.diagPairs[key] spec := color.Average( color.Average(ctx.nodeSpec(prev[0]), ctx.nodeSpec(prev[1])), color.Average(ctx.nodeSpec(aRoom), ctx.nodeSpec(bRoom)), ) grid[gr][gc] = mapCell{char: 'X', spec: spec} } } else { grid[gr][gc] = cell ctx.diagPairs[key] = [2]int{aRoom, bRoom} } } } } // SE connectors: (x,y) -> (x+1, y+1), connector at grid[2*y+3][2*x+3] for y := -1; y <= 0; y++ { for x := -1; x <= 0; x++ { aPos, bPos := [2]int{x, y}, [2]int{x + 1, y + 1} aRoom, aOK := bg.posToRoom[aPos] bRoom, bOK := bg.posToRoom[bPos] if !aOK || !bOK { continue } if cell, ok := ctx.connectorCell(aRoom, bRoom, world.Southeast, world.Northwest); ok { gr, gc := 2*y+3, 2*x+3 key := [2]int{gr, gc} if isDiagonalGlyph(grid[gr][gc].char) { if grid[gr][gc].char != cell.char { prev := ctx.diagPairs[key] spec := color.Average( color.Average(ctx.nodeSpec(prev[0]), ctx.nodeSpec(prev[1])), color.Average(ctx.nodeSpec(aRoom), ctx.nodeSpec(bRoom)), ) grid[gr][gc] = mapCell{char: 'X', spec: spec} } } else { grid[gr][gc] = cell ctx.diagPairs[key] = [2]int{aRoom, bRoom} } } } } // SW connectors: (x,y) -> (x-1, y+1), connector at grid[2*y+3][2*x+1] for y := -1; y <= 0; y++ { for x := 0; x <= 1; x++ { aPos, bPos := [2]int{x, y}, [2]int{x - 1, y + 1} aRoom, aOK := bg.posToRoom[aPos] bRoom, bOK := bg.posToRoom[bPos] if !aOK || !bOK { continue } if cell, ok := ctx.connectorCell(aRoom, bRoom, world.Southwest, world.Northeast); ok { gr, gc := 2*y+3, 2*x+1 key := [2]int{gr, gc} if isDiagonalGlyph(grid[gr][gc].char) { if grid[gr][gc].char != cell.char { prev := ctx.diagPairs[key] spec := color.Average( color.Average(ctx.nodeSpec(prev[0]), ctx.nodeSpec(prev[1])), color.Average(ctx.nodeSpec(aRoom), ctx.nodeSpec(bRoom)), ) grid[gr][gc] = mapCell{char: 'X', spec: spec} } } else { grid[gr][gc] = cell ctx.diagPairs[key] = [2]int{aRoom, bRoom} } } } } cur, _ := loadRoom(g, roomID) if cur != nil { if upTarget, hasUp := exitTarget(cur, world.Up); hasUp { spec := color.NoColor() if exitStateTo(g, sess, roomID, world.Up, upTarget) == exitBlocked { spec = ctx.blockedSpec } switch { case grid[1][3].char == ' ': grid[1][3] = mapCell{char: mg.upArrow, spec: spec} case grid[1][1].char == ' ': grid[1][1] = mapCell{char: mg.upArrow, spec: spec} case grid[1][2].char == ' ': grid[1][2] = mapCell{char: mg.upArrow, spec: spec} } } if downTarget, hasDown := exitTarget(cur, world.Down); hasDown { spec := color.NoColor() if exitStateTo(g, sess, roomID, world.Down, downTarget) == exitBlocked { spec = ctx.blockedSpec } if grid[3][1].char == ' ' { grid[3][1] = mapCell{char: mg.downArrow, spec: spec} } else if grid[3][3].char == ' ' { grid[3][3] = mapCell{char: mg.downArrow, spec: spec} } } } topFill := strings.Repeat(string(mg.topFill), 5) lines := make([]string, 7) lines[0] = string(mg.topLeft) + topFill + string(mg.topRight) inner := renderMapCells(grid, colorMode, 0, 5, mg.side) copy(lines[1:], inner) botFill := strings.Repeat(string(mg.topFill), 5) lines[6] = string(mg.bottomLeft) + botFill + string(mg.bottomRight) return lines } func buildFullMap(g *Game, sess *net.Session, roomID, mapWidth, mapHeight int, mg mapGlyphs) []string { visited := roomsVisited(sess) bg := buildGraph(g, roomID, visited) colorMode := colorModeFor(sess) atSpec := resolveMapAt(g, sess) dimSpec := resolveDim(g, sess) ctx := &mapRenderCtx{ g: g, sess: sess, bg: bg, visited: visited, currentRoom: roomID, atSpec: atSpec, dimSpec: dimSpec, blockedSpec: resolveMapBlocked(g, sess), mg: mg, diagPairs: make(map[[2]int][2]int), } grid := make([][]mapCell, mapHeight) for i := range grid { grid[i] = make([]mapCell, mapWidth) for j := range grid[i] { grid[i][j] = mapCell{char: ' '} } } cx := mapWidth / 2 cy := mapHeight / 2 for pos, rid := range bg.posToRoom { gr := cy + pos[1]*2 gc := cx + pos[0]*2 if gr < 0 || gr >= mapHeight || gc < 0 || gc >= mapWidth { continue } if rid == roomID { grid[gr][gc] = mapCell{char: '@', spec: atSpec} } else { unvisited := visited != nil && !visited[rid] ch, spec := roomMapSymbol(g, sess, rid, unvisited) if unvisited { spec = dimSpec } grid[gr][gc] = mapCell{char: ch, spec: spec} } } for pos, rid := range bg.posToRoom { x, y := pos[0], pos[1] if rightID, exists := bg.posToRoom[[2]int{x + 1, y}]; exists { gr := cy + y*2 gc := cx + x*2 + 1 if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth { if cell, ok := ctx.connectorCell(rid, rightID, world.East, world.West); ok { grid[gr][gc] = cell } } } if bottomID, exists := bg.posToRoom[[2]int{x, y + 1}]; exists { gr := cy + y*2 + 1 gc := cx + x*2 if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth { if cell, ok := ctx.connectorCell(rid, bottomID, world.South, world.North); ok { grid[gr][gc] = cell } } } if neID, exists := bg.posToRoom[[2]int{x + 1, y - 1}]; exists { gr := cy + y*2 - 1 gc := cx + x*2 + 1 if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth { if cell, ok := ctx.connectorCell(rid, neID, world.Northeast, world.Southwest); ok { key := [2]int{gr, gc} if isDiagonalGlyph(grid[gr][gc].char) { if grid[gr][gc].char != cell.char { prev := ctx.diagPairs[key] spec := color.Average( color.Average(ctx.nodeSpec(prev[0]), ctx.nodeSpec(prev[1])), color.Average(ctx.nodeSpec(rid), ctx.nodeSpec(neID)), ) grid[gr][gc] = mapCell{char: 'X', spec: spec} } } else { grid[gr][gc] = cell ctx.diagPairs[key] = [2]int{rid, neID} } } } } if nwID, exists := bg.posToRoom[[2]int{x - 1, y - 1}]; exists { gr := cy + y*2 - 1 gc := cx + x*2 - 1 if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth { if cell, ok := ctx.connectorCell(rid, nwID, world.Northwest, world.Southeast); ok { key := [2]int{gr, gc} if isDiagonalGlyph(grid[gr][gc].char) { if grid[gr][gc].char != cell.char { prev := ctx.diagPairs[key] spec := color.Average( color.Average(ctx.nodeSpec(prev[0]), ctx.nodeSpec(prev[1])), color.Average(ctx.nodeSpec(rid), ctx.nodeSpec(nwID)), ) grid[gr][gc] = mapCell{char: 'X', spec: spec} } } else { grid[gr][gc] = cell ctx.diagPairs[key] = [2]int{rid, nwID} } } } } if seID, exists := bg.posToRoom[[2]int{x + 1, y + 1}]; exists { gr := cy + y*2 + 1 gc := cx + x*2 + 1 if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth { if cell, ok := ctx.connectorCell(rid, seID, world.Southeast, world.Northwest); ok { key := [2]int{gr, gc} if isDiagonalGlyph(grid[gr][gc].char) { if grid[gr][gc].char != cell.char { prev := ctx.diagPairs[key] spec := color.Average( color.Average(ctx.nodeSpec(prev[0]), ctx.nodeSpec(prev[1])), color.Average(ctx.nodeSpec(rid), ctx.nodeSpec(seID)), ) grid[gr][gc] = mapCell{char: 'X', spec: spec} } } else { grid[gr][gc] = cell ctx.diagPairs[key] = [2]int{rid, seID} } } } } if swID, exists := bg.posToRoom[[2]int{x - 1, y + 1}]; exists { gr := cy + y*2 + 1 gc := cx + x*2 - 1 if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth { if cell, ok := ctx.connectorCell(rid, swID, world.Southwest, world.Northeast); ok { key := [2]int{gr, gc} if isDiagonalGlyph(grid[gr][gc].char) { if grid[gr][gc].char != cell.char { prev := ctx.diagPairs[key] spec := color.Average( color.Average(ctx.nodeSpec(prev[0]), ctx.nodeSpec(prev[1])), color.Average(ctx.nodeSpec(rid), ctx.nodeSpec(swID)), ) grid[gr][gc] = mapCell{char: 'X', spec: spec} } } else { grid[gr][gc] = cell ctx.diagPairs[key] = [2]int{rid, swID} } } } } } return renderMapCells(grid, colorMode, 0, mapHeight, 0) } func roomsVisited(sess *net.Session) map[int]bool { if sess != nil && sess.Player != nil { return sess.Player.Stats.RoomsVisited } return nil } func colorModeFor(sess *net.Session) string { if sess != nil && sess.Player != nil { return sess.Player.OptionString("color") } return "none" } func resolveMapAt(g *Game, sess *net.Session) color.ColorSpec { if sess != nil { return g.resolveColor(sess, "map_at") } return color.Parse("0F") } func resolveDim(g *Game, sess *net.Session) color.ColorSpec { if sess != nil { return g.resolveColor(sess, "dim") } return color.Parse("F3 dim") } func resolveMapBlocked(g *Game, sess *net.Session) color.ColorSpec { if sess != nil { return g.resolveColor(sess, "map_blocked") } return color.Parse("C4") } // mapRenderCtx bundles the per-render state shared by node and connector drawing // so the tiny and full maps build cells the same way. type mapRenderCtx struct { g *Game sess *net.Session bg *mapGraph visited map[int]bool currentRoom int atSpec color.ColorSpec dimSpec color.ColorSpec blockedSpec color.ColorSpec mg mapGlyphs diagPairs map[[2]int][2]int } // nodeSpec returns the effective color a room's node is drawn with, mirroring // the logic used when placing room glyphs. func (c *mapRenderCtx) nodeSpec(roomID int) color.ColorSpec { if roomID == c.currentRoom { return c.atSpec } if c.visited != nil && !c.visited[roomID] { return c.dimSpec } _, spec := roomMapSymbol(c.g, c.sess, roomID, false) return spec } // connectorCell builds the link cell between two grid-adjacent rooms based on // the per-direction traversability of the two exits joining them. ok is false // when there is no link at all, so the caller draws nothing: // - both directions open -> bidirectional bar (- / |) // - outward direction open -> arrow pointing outward // - outward direction blocked -> blocked 'X' // - only inward direction open -> arrow pointing inward // - none traversable -> blocked 'X' // - neither exit exists -> ok == false (no cell) // // "Outward" is the exit from the room nearer the player (smaller BFS distance) // toward the farther one — i.e. the link as reached along the shortest path. // Since each BFS hop is a unit grid step, grid-adjacent rooms always differ in // distance parity, so there is never a tie to break. // // Bars and arrows use the normal link coloring (dim if an endpoint is unvisited, // otherwise the gradient average); only 'X' uses the blocked color. func (c *mapRenderCtx) connectorCell(roomA, roomB int, dirAB, dirBA world.ExitDir) (mapCell, bool) { fwd := exitStateTo(c.g, c.sess, roomA, dirAB, roomB) // A -> B bwd := exitStateTo(c.g, c.sess, roomB, dirBA, roomA) // B -> A switch { case fwd == exitAbsent && bwd == exitAbsent: return mapCell{}, false case fwd == exitOpen && bwd == exitOpen: return c.coloredCell(roomA, roomB, barGlyph(c.mg, dirAB)) } // Orient the link outward, from near room to far room. outDir, inDir, out, in := dirAB, dirBA, fwd, bwd if c.bg.dist[roomB] < c.bg.dist[roomA] { outDir, inDir, out, in = dirBA, dirAB, bwd, fwd } switch { case out == exitOpen: return c.coloredCell(roomA, roomB, arrowGlyph(c.mg, outDir)) case out == exitAbsent && in == exitOpen: return c.coloredCell(roomA, roomB, arrowGlyph(c.mg, inDir)) default: return mapCell{char: 'X', spec: c.blockedSpec}, true } } // coloredCell applies the shared link coloring logic for bar/arrow glyphs // (dim if either endpoint is unvisited, otherwise the gradient average). func (c *mapRenderCtx) coloredCell(roomA, roomB int, glyph rune) (mapCell, bool) { if c.visited != nil && (!c.visited[roomA] || !c.visited[roomB]) { return mapCell{char: glyph, spec: c.dimSpec}, true } return mapCell{char: glyph, spec: color.Average(c.nodeSpec(roomA), c.nodeSpec(roomB))}, true } type exitState int const ( exitAbsent exitState = iota exitOpen exitBlocked ) // exitStateTo reports whether the exit from `from` in `dir` leads to `neighbor` // and, if so, whether it is currently traversable for this player. A missing // session/player (e.g. in tests or background renders) treats conditional exits // as open so rendering never depends on player evaluation. func exitStateTo(g *Game, sess *net.Session, from int, dir world.ExitDir, neighbor int) exitState { room, ok := loadRoom(g, from) if !ok { return exitAbsent } exit, ok := room.Exits[dir] if !ok || exit.Room != neighbor { return exitAbsent } if exit.Condition == nil || sess == nil || sess.Player == nil { return exitOpen } if sess.Player.GodMode || g.checkCondition(sess, exit.Condition) { return exitOpen } return exitBlocked } func isDiagonalGlyph(ch rune) bool { return ch == '\\' || ch == '/' || ch == '↗' || ch == '↖' || ch == '↘' || ch == '↙' } // barGlyph returns the bidirectional connector glyph for a link's orientation. func barGlyph(mg mapGlyphs, dir world.ExitDir) rune { switch dir { case world.East, world.West: return mg.connectorH case world.North, world.South: return mg.connectorV case world.Northeast, world.Southwest: return mg.connectorNE case world.Northwest, world.Southeast: return mg.connectorNW } return mg.connectorH } // arrowGlyph returns the one-way arrow pointing along the direction of travel. func arrowGlyph(mg mapGlyphs, dir world.ExitDir) rune { switch dir { case world.East: return mg.rightArrow case world.West: return mg.leftArrow case world.South: return mg.downArrow case world.North: return mg.upArrow case world.Northeast: return mg.upRight case world.Northwest: return mg.upLeft case world.Southeast: return mg.downRight case world.Southwest: return mg.downLeft } return mg.connectorH } // roomDefaultColorSpec resolves the room's own default map color (feature 3). // Empty/unset returns NoColor. func roomDefaultColorSpec(g *Game, roomID int) color.ColorSpec { if room, ok := loadRoom(g, roomID); ok && room.Color != "" { return color.Parse(room.Color) } return color.NoColor() } func roomMapSymbol(g *Game, sess *net.Session, roomID int, unvisited bool) (rune, color.ColorSpec) { roomSpec := roomDefaultColorSpec(g, roomID) if sess != nil && sess.Player != nil { if data, ok := sess.Player.MapSymbols[roomID]; ok { r, size := utf8.DecodeRuneInString(data.Char) if size > 0 && r != utf8.RuneError { // precedence: player symbol color > room default color > none spec := roomSpec if data.Color != "" { spec = color.Parse(data.Color) } // non-ASCII custom symbols fall back to 'o' // when unicode mode is off, but preserve the color. if !sess.Player.OptionBool("unicode") && r > 127 { return 'o', spec } return r, spec } } if sess.Player.OptionBool("unicode") { if unvisited { return '□', roomSpec } return '■', roomSpec } return 'o', roomSpec } return '■', roomSpec } func exitTarget(room *world.Room, dir world.ExitDir) (int, bool) { if room == nil { return 0, false } exit, ok := room.Exits[dir] if !ok { return 0, false } return exit.Room, true } func loadRoom(g *Game, roomID int) (*world.Room, bool) { if roomID == 0 { return nil, false } room, err := g.World.LoadRoom(roomID) if err != nil { return nil, false } return room, true } func stripBlankRows(lines []string) []string { var out []string for _, line := range lines { if strings.TrimSpace(line) != "" { out = append(out, line) } } return out } func leftTrimCommon(lines []string) []string { min := -1 for _, line := range lines { if strings.TrimSpace(line) == "" { continue } n := 0 for _, r := range line { if r == ' ' { n++ } else { break } } if min < 0 || n < min { min = n } } if min <= 0 { return lines } result := make([]string, len(lines)) for i, line := range lines { if len(line) <= min { result[i] = "" } else { result[i] = line[min:] } } return result }