Muitas declarações 'if'?

263

O código a seguir funciona como eu preciso, mas é feio, excessivo ou várias outras coisas. Examinei as fórmulas e tentei escrever algumas soluções, mas acabo com uma quantidade semelhante de instruções.

Existe um tipo de fórmula matemática que me beneficiaria nesse caso ou tem 16 se as instruções forem aceitáveis?

Para explicar o código, é para um tipo de jogo baseado em turnos simultâneos. Dois jogadores têm quatro botões de ação cada e os resultados vêm de uma matriz (0-3), mas as variáveis ​​'um' e 'dois' podem ser atribuído qualquer coisa, se isso ajudar. O resultado é: 0 = nenhuma vitória, 1 = p1 vence, 2 = p2 vence, 3 = ambos vencem.

public int fightMath(int one, int two) {

    if(one == 0 && two == 0) { result = 0; }
    else if(one == 0 && two == 1) { result = 0; }
    else if(one == 0 && two == 2) { result = 1; }
    else if(one == 0 && two == 3) { result = 2; }
    else if(one == 1 && two == 0) { result = 0; }
    else if(one == 1 && two == 1) { result = 0; }
    else if(one == 1 && two == 2) { result = 2; }
    else if(one == 1 && two == 3) { result = 1; }
    else if(one == 2 && two == 0) { result = 2; }
    else if(one == 2 && two == 1) { result = 1; }
    else if(one == 2 && two == 2) { result = 3; }
    else if(one == 2 && two == 3) { result = 3; }
    else if(one == 3 && two == 0) { result = 1; }
    else if(one == 3 && two == 1) { result = 2; }
    else if(one == 3 && two == 2) { result = 3; }
    else if(one == 3 && two == 3) { result = 3; }

    return result;
}
TomFirth
fonte
1
@waqaslam: - Isso pode ajudar o Java a mudar a instrução para lidar com duas variáveis?
Rahul Tripathi
9
Certamente, existe alguma lógica aqui que pode ser generalizada em vez de forçada bruta? Certamente há alguma função f(a, b)que produz a resposta no caso geral? Você não explicou a lógica do cálculo, portanto, todas as respostas são apenas batom em um porco. Eu começaria repensando seriamente a lógica do seu programa, o uso de intsinalizadores para ações está muito desatualizado. enums podem conter lógica e são descritivos, isso permitiria escrever seu código de uma maneira mais moderna.
Boris the Spider
Depois de ler as respostas que Steve Benett forneceu em sua pergunta alternativa vinculada acima, posso assumir que não há uma resposta direta à fórmula, pois ela é essencialmente a mesma que um banco de dados. Tentei explicar na pergunta original que eu estava criando um jogo simples (lutador) e os usuários têm uma seleção de 4 botões: blockHigh (0), blockLow (1), attackHigh (2) e attackLow (3). Esses números são mantidos em uma matriz até serem necessários. Posteriormente, eles são usados ​​pela função 'fightMath ()' que chama as seleções de playerOne contra playerTwos para fornecer o resultado. Nenhuma detecção de colisão real.
TomFirth # 19/14
9
Se você tiver uma resposta, publique-a como tal. A discussão extensa nos comentários é difícil de seguir, especialmente quando o código está envolvido. Se você quiser conversar sobre se essa pergunta deveria ter sido migrada para a Revisão de Código, há uma discussão Meta sobre isso.
George Stocker
1
O que você quer dizer com "o mesmo que um banco de dados"? Se esses valores estiverem no banco de dados, retire-os de lá. Caso contrário, se for realmente tão complexo, eu deixaria como você o adicionaria e adicionaria comentários de lógica de negócios após cada linha para que as pessoas entendessem o que está acontecendo. É melhor (para mim) longo e explícito - alguém no futuro poderá entender o que está acontecendo. Se você o coloca em um mapa ou tenta salvar 8 linhas de código, a vantagem é realmente pequena e o tamanho menor é maior: você o torna cada vez mais confuso para alguém que precisa ler seu código um dia.
skaz

Respostas:

600

Se você não conseguir criar uma fórmula, poderá usar uma tabela para um número tão limitado de resultados:

final int[][] result = new int[][] {
  { 0, 0, 1, 2 },
  { 0, 0, 2, 1 },
  { 2, 1, 3, 3 },
  { 1, 2, 3, 3 }
};
return result[one][two];
Laalto
fonte
7
Isso é interessante, porque eu nunca vi essa solução antes. Não tenho muita certeza de entender o resultado do retorno, mas gostarei de testar isso.
TomFirth
4
Você não precisa da afirmação, o Java lançará um IndexOutOfBoundsExceptionassim mesmo se um ou mais índices estiverem fora dos limites.
JAB
43
@JoeHarper Se você quiser algo fácil de ler, não usará números mágicos em primeiro lugar e terá um comentário explicando o mapeamento. Sendo assim, prefiro esta versão à original, mas, para algo que possa ser mantido em longo prazo, eu usaria uma abordagem envolvendo tipos enumerados ou pelo menos constantes nomeadas.
JAB
13
@JoeHarper "Teoricamente" é uma coisa, "praticamente" é outra. É claro que tento usar nomes descritivos (com exceção da convenção i/ j/ kpara variáveis ​​de loop), constantes nomeadas, organizando meu código de forma legível etc., mas quando os nomes de variáveis ​​e funções começam a ocupar mais de 20 caracteres cada um acho que realmente leva a um código menos legível. Minha abordagem usual é tentar um código compreensível, mas sucinto, com comentários aqui e ali, para explicar por que o código está estruturado da maneira que é (versus como). Colocar o porquê nos nomes apenas confunde tudo.
JAB
13
Eu gosto dessa solução para esse problema específico, porque os resultados SÃO ditados por uma matriz de resultados.
Tentando
201

Como seu conjunto de dados é muito pequeno, você pode compactar tudo em um inteiro longo e transformá-lo em uma fórmula

public int fightMath(int one,int two)
{
   return (int)(0xF9F66090L >> (2*(one*4 + two)))%4;
}

Variante mais bit a bit:

Isso faz uso do fato de que tudo é um múltiplo de 2

public int fightMath(int one,int two)
{
   return (0xF9F66090 >> ((one << 3) | (two << 1))) & 0x3;
}

A origem da constante mágica

O que posso dizer? O mundo precisa de mágica, às vezes a possibilidade de algo exige sua criação.

A essência da função que resolve o problema do OP é um mapa de 2 números (um, dois), domínio {0,1,2,3} até o intervalo {0,1,2,3}. Cada uma das respostas abordou como implementar esse mapa.

Além disso, você pode ver em várias respostas uma reafirmação do problema como um mapa de 1 número base de 2 dígitos 4 número N (um, dois) em que um é o dígito 1, dois é o dígito 2 e N = 4 * um + dois; N = {0,1,2, ..., 15} - dezesseis valores diferentes, isso é importante. A saída da função é um número base de 1 dígito 4 {0,1,2,3} - 4 valores diferentes, também importantes.

Agora, um número base 4 de 1 dígito pode ser expresso como um número base 2 de 2 dígitos; {0,1,2,3} = {00,01,10,11} e, portanto, cada saída pode ser codificada com apenas 2 bits. Acima, existem apenas 16 saídas diferentes possíveis; portanto, 16 * 2 = 32 bits é tudo o que é necessário para codificar o mapa inteiro; tudo isso pode caber em 1 inteiro.

A constante M é uma codificação do mapa m onde m (0) é codificado nos bits M [0: 1], m (1) é codificado nos bits M [2: 3] e m (n) é codificado em bits M [n * 2: n * 2 + 1].

Tudo o que resta é indexar e retornar a parte correta da constante; nesse caso, você pode deslocar M para a direita 2 * N vezes e pegar os 2 bits menos significativos, ou seja (M >> 2 * N) e 0x3. As expressões (um << 3) e (dois << 1) estão apenas multiplicando as coisas, observando que 2 * x = x << 1 e 8 * x = x << 3.

waTeim
fonte
79
inteligente, mas ninguém mais lendo o código terá a chance de entendê-lo.
Aran Mulholland
106
Eu acho que essa é uma prática extremamente ruim. Ninguém mais, exceto o autor, entenderá isso. Você deseja examinar um pedaço de código e entendê-lo rapidamente. Mas isso é apenas uma perda de tempo.
Balázs Németh
14
Estou com @ BalázsMáriaNémeth sobre isso. Embora muito impressionante, você deve codificar psicopatas violentos!
OrhanC1
90
Todos os que criticam negativamente pensam que esse é um cheiro horrível de código. Todos os promotores pensam o mesmo, mas admiram a esperteza por trás disso. +1 (nunca use este código.) #
2100 usr
4
Que exemplo adorável de escrever apenas código !
lealand
98

Não gosto de nenhuma das soluções apresentadas, exceto as da JAB. Nenhum dos outros facilita a leitura do código e o entendimento do que está sendo calculado .

Aqui está como eu escreveria esse código - só sei C #, não Java, mas você entendeu:

const bool t = true;
const bool f = false;
static readonly bool[,] attackResult = {
    { f, f, t, f }, 
    { f, f, f, t },
    { f, t, t, t },
    { t, f, t, t }
};
[Flags] enum HitResult 
{ 
    Neither = 0,
    PlayerOne = 1,
    PlayerTwo = 2,
    Both = PlayerOne | PlayerTwo
}
static HitResult ResolveAttack(int one, int two)
{
    return 
        (attackResult[one, two] ? HitResult.PlayerOne : HitResult.Neither) | 
        (attackResult[two, one] ? HitResult.PlayerTwo : HitResult.Neither);
}    

Agora é muito mais claro o que está sendo computado aqui: isso enfatiza que estamos computando quem é atingido por qual ataque e retornando os dois resultados.

No entanto, isso poderia ser ainda melhor; essa matriz booleana é um pouco opaca. Eu gosto da abordagem de pesquisa de tabela, mas gostaria de escrevê-la de maneira a deixar claro qual era a semântica de jogo pretendida. Ou seja, em vez de "um ataque de zero e uma defesa de um resultar em nenhum acerto", em vez disso, encontre uma maneira de tornar o código mais claro implica "um ataque de chute baixo e uma defesa de bloco baixa resulta em nenhum acerto". Faça o código refletir a lógica comercial do jogo.

Eric Lippert
fonte
66
Absurdo. A maioria dos programas com pouca experiência poderá apreciar os conselhos que estão sendo fornecidos aqui e aplicar o estilo de codificação ao seu próprio idioma. A questão era como evitar uma série de ifs. Isso mostra como.
GreenAsJade
6
@ user3414693: Estou ciente de que é uma pergunta sobre Java. Se você ler atentamente a resposta, isso ficará claro. Se você acha que minha resposta é imprudente, encorajo-o a escrever sua própria resposta que você mais gosta.
precisa
1
@EricLippert Também gosto da solução da JAB. IMHO, tipo de enum em C # deixa muito a desejar. Ele não segue o poço da filosofia do sucesso, como o restante dos recursos. Por exemplo, stackoverflow.com/a/847353/92414 A equipe do c # tem algum plano para criar um novo tipo de enumeração (para evitar a quebra do código existente) que foi projetado melhor?
SolutionYogi
@SolutionYogi: Também não gosto muito de enumerações em C #, embora sejam do jeito que são por boas razões históricas. (. Principalmente para compatibilidade com enums COM existentes) Não tenho conhecimento de quaisquer planos para adicionar novos equipamentos para enums em C # 6.
Eric Lippert
3
@ Lista não, os comentários não são executados. O OP fez exatamente o que deveria ser feito; converter comentários para limpar o código. Veja, por exemplo, Steve McConnell Code Complete stevemcconnell.com/cccntnt.htm
djechlin
87

Você pode criar uma matriz que contenha resultados

int[][] results = {{0, 0, 1, 2}, {0, 0, 2, 1},{2, 1, 3, 3},{2, 1, 3, 3}};

Quando você deseja obter valor, você usará

public int fightMath(int one, int two) {
  return this.results[one][two]; 
}
djm.im
fonte
69

Outras pessoas já sugeriram minha ideia inicial, o método da matriz, mas, além de consolidar as instruções if, você pode evitar parte do que possui, certificando-se de que os argumentos fornecidos estejam no intervalo esperado e usando retornos no local (algumas codificações padrões que vi impor um ponto de saída para funções, mas descobri que vários retornos são muito úteis para evitar a codificação de seta e com a prevalência de exceções em Java, não há muito sentido em impor estritamente essa regra de qualquer maneira como qualquer exceção não capturada lançada dentro do método é um possível ponto de saída). Aninhar instruções de chave é uma possibilidade, mas, para o pequeno intervalo de valores que você está verificando aqui, acho que as instruções são mais compactas e provavelmente não resultam em muita diferença de desempenho,

public int fightMath(int one, int two) {
    if (one > 3 || one < 0 || two > 3 || two < 0) {
        throw new IllegalArgumentException("Result is undefined for arguments outside the range [0, 3]");
    }

    if (one <= 1) {
        if (two <= 1) return 0;
        if (two - one == 2) return 1;
        return 2; // two can only be 3 here, no need for an explicit conditional
    }

    // one >= 2
    if (two >= 2) return 3;
    if (two == 1) return 1;
    return 2; // two can only be 0 here
}

Isso acaba sendo menos legível do que poderia ser devido à irregularidade de partes do mapeamento de entrada-> resultado. Eu prefiro o estilo da matriz devido à sua simplicidade e como você pode configurá-la para fazer sentido visualmente (embora isso seja em parte influenciado pelas minhas memórias dos mapas de Karnaugh):

int[][] results = {{0, 0, 1, 2},
                   {0, 0, 2, 1},
                   {2, 1, 3, 3},
                   {2, 1, 3, 3}};

Atualização: dada a sua menção de bloqueio / batida, aqui está uma mudança mais radical na função que utiliza tipos enumerados apropriados / contendo atributos para entradas e o resultado e também modifica um pouco o resultado para levar em conta o bloqueio, o que deve resultar em mais função legível.

enum MoveType {
    ATTACK,
    BLOCK;
}

enum MoveHeight {
    HIGH,
    LOW;
}

enum Move {
    // Enum members can have properties/attributes/data members of their own
    ATTACK_HIGH(MoveType.ATTACK, MoveHeight.HIGH),
    ATTACK_LOW(MoveType.ATTACK, MoveHeight.LOW),
    BLOCK_HIGH(MoveType.BLOCK, MoveHeight.HIGH),
    BLOCK_LOW(MoveType.BLOCK, MoveHeight.LOW);

    public final MoveType type;
    public final MoveHeight height;

    private Move(MoveType type, MoveHeight height) {
        this.type = type;
        this.height = height;
    }

    /** Makes the attack checks later on simpler. */
    public boolean isAttack() {
        return this.type == MoveType.ATTACK;
    }
}

enum LandedHit {
    NEITHER,
    PLAYER_ONE,
    PLAYER_TWO,
    BOTH;
}

LandedHit fightMath(Move one, Move two) {
    // One is an attack, the other is a block
    if (one.type != two.type) {
        // attack at some height gets blocked by block at same height
        if (one.height == two.height) return LandedHit.NEITHER;

        // Either player 1 attacked or player 2 attacked; whoever did
        // lands a hit
        if (one.isAttack()) return LandedHit.PLAYER_ONE;
        return LandedHit.PLAYER_TWO;
    }

    // both attack
    if (one.isAttack()) return LandedHit.BOTH;

    // both block
    return LandedHit.NEITHER;
}

Você nem precisa alterar a função se quiser adicionar bloqueios / ataques de mais alturas, apenas as enumerações; adicionar tipos adicionais de movimentos provavelmente exigirá modificação da função. Além disso, EnumSets pode ser mais extensível do que usar enumerações extras como propriedades da enumeração principal, por exemplo, EnumSet<Move> attacks = EnumSet.of(Move.ATTACK_HIGH, Move.ATTACK_LOW, ...);e então em attacks.contains(move)vez de move.type == MoveType.ATTACK, embora usar EnumSets provavelmente seja um pouco mais lento do que as verificações diretas de igual.


No caso de um bloco bem-sucedido resultar em um contador, você pode substituir if (one.height == two.height) return LandedHit.NEITHER;por

if (one.height == two.height) {
    // Successful block results in a counter against the attacker
    if (one.isAttack()) return LandedHit.PLAYER_TWO;
    return LandedHit.PLAYER_ONE;
}

Além disso, a substituição de algumas ifinstruções pelo uso do operador ternário ( boolean_expression ? result_if_true : result_if_false) poderia tornar o código mais compacto (por exemplo, o código no bloco anterior se tornaria return one.isAttack() ? LandedHit.PLAYER_TWO : LandedHit.PLAYER_ONE;), mas isso pode levar a oneliners mais difíceis de ler, então eu não faria ' não recomendo para ramificações mais complexas.

JAB
fonte
Definitivamente vou investigar isso, mas meu código atual me permite usar o valor int onee twoser reutilizado como pontos de partida na minha planilha. Embora não exija muito código adicional para permitir isso.
19414 TomFirth
2
@ TomFirth84 Há uma EnumMapclasse que você pode usar para mapear os enums para seus deslocamentos inteiros (você também pode usar os valores ordinais dos membros enum diretamente, por exemplo, Move.ATTACK_HIGH.ordinal()seria 0, Move.ATTACK_LOW.ordinal()seria 1, etc., mas isso é mais frágil / menos flexível do que explicitamente associando cada membro com um valor como adicionar valores enum entre os existentes jogaria fora a contagem, o que não seria o caso com um EnumMap).
JAB
7
Essa é a solução mais legível, pois converte o código em algo significativo para a pessoa que está lendo o código.
David Stanley
Seu código, pelo menos aquele que usa enumerações, está errado. De acordo com as instruções if no OP, um bloqueio bem-sucedido leva a um golpe no atacante. Mas +1 para obter um código completo.
Taemyr
2
Você pode até adicionar um attack(against)método ao Moveenum, retornando HIT quando o movimento é um ataque bem-sucedido, BACKFIRE quando o movimento é um ataque bloqueado e NADA quando não é um ataque. Desta forma, você pode implementá-lo em geral ( public boolean attack(Move other) { if this.isAttack() return (other.isAttack() || other.height != this.height) ? HIT : BACKFIRE; return NOTHING; }), e substituí-lo em movimentos específicos quando necessário (movimentos fracos que qualquer bloco podem bloquear, ataques que nunca mais sair pela culatra etc.)
reescrito
50

Por que não usar uma matriz?

Eu vou começar do começo. Eu vejo um padrão, os valores vão de 0 a 3 e você deseja capturar todos os valores possíveis. Esta é a sua mesa:

0 & 0 = 0
0 & 1 = 0
0 & 2 = 1
0 & 3 = 2
1 & 0 = 0
1 & 1 = 0
1 & 2 = 2
1 & 3 = 1
2 & 0 = 2
2 & 1 = 1
2 & 2 = 3
2 & 3 = 3
3 & 0 = 2
3 & 1 = 1
3 & 2 = 3
3 & 3 = 3

quando olhamos para a mesma tabela binária, vemos os seguintes resultados:

00 & 00 = 00
00 & 01 = 00
00 & 10 = 01
00 & 11 = 10
01 & 00 = 00
01 & 01 = 00
01 & 10 = 10
01 & 11 = 01
10 & 00 = 10
10 & 01 = 01
10 & 10 = 11
10 & 11 = 11
11 & 00 = 10
11 & 01 = 01
11 & 10 = 11
11 & 11 = 11

Agora talvez você já veja algum padrão, mas quando eu combino os valores um e dois, vejo que você está usando todos os valores 0000, 0001, 0010, ..... 1110 e 1111. Agora vamos combinar os valores um e dois para criar um único Número inteiro de 4 bits.

0000 = 00
0001 = 00
0010 = 01
0011 = 10
0100 = 00
0101 = 00
0110 = 10
0111 = 01
1000 = 10
1001 = 01
1010 = 11
1011 = 11
1100 = 10
1101 = 01
1110 = 11
1111 = 11

Quando convertemos isso de volta em valores decimais, vemos uma matriz muito possível de valores em que um e dois combinados podem ser usados ​​como índice:

0 = 0
1 = 0
2 = 1
3 = 2
4 = 0
5 = 0
6 = 2
7 = 1
8 = 2
9 = 1
10 = 3
11 = 3
12 = 2
13 = 1
14 = 3
15 = 3

A matriz é então {0, 0, 1, 2, 0, 0, 2, 1, 2, 1, 3, 3, 2, 1, 3, 3}, onde seu índice é simplesmente um e dois combinados.

Eu não sou um programador Java, mas você pode se livrar de todas as instruções if e anotá-las da seguinte forma:

int[] myIntArray = {0, 0, 1, 2, 0, 0, 2, 1, 2, 1, 3, 3, 2, 1, 3, 3};
result = myIntArray[one * 4 + two]; 

Não sei se um deslocamento de bits por 2 é mais rápido que a multiplicação. Mas pode valer a pena tentar.

DJ Bazzie Wazzie
fonte
2
Um deslocamento de bits de 2 é quase definitivamente mais rápido que uma multiplicação de 4. Na melhor das hipóteses, a multiplicação por 4 reconheceria que 4 é 2 ^ 2 e realizaria um deslocamento de bits (traduzido pelo compilador potencialmente). Francamente, para mim, a mudança é mais legível.
Cruncher
Eu gosto da sua abordagem! Nivela essencialmente uma matriz 4x4 em uma disposição de 16 elementos.
Cameron Tinker
6
Hoje em dia, se não me engano, o compilador sem dúvida reconhecerá que você está multiplicando por um poder de dois e o otimizará de acordo. Portanto, para você, o programador, a mudança de bits e a multiplicação devem ter exatamente o mesmo desempenho.
precisa
24

Isso usa um pouco de bitmagic (você já está fazendo isso mantendo dois bits de informação (baixo / alto e ataque / bloco) em um único número inteiro):

Eu não o executei, apenas digitei aqui, por favor, verifique novamente. A ideia certamente funciona. EDIT: Agora é testado para cada entrada, funciona bem.

public int fightMath(int one, int two) {
    if(one<2 && two<2){ //both players blocking
        return 0; // nobody hits
    }else if(one>1 && two>1){ //both players attacking
        return 3; // both hit
    }else{ // some of them attack, other one blocks
        int different_height = (one ^ two) & 1; // is 0 if they are both going for the same height - i.e. blocker wins, and 1 if height is different, thus attacker wins
        int attacker = one>1?1:0; // is 1 if one is the attacker, two is the blocker, and 0 if one is the blocker, two is the attacker
        return (attacker ^ different_height) + 1;
    }
}

Ou devo sugerir separar os dois bits de informação em variáveis ​​separadas? Código baseado principalmente em operações de bits como esta acima é geralmente muito difícil de manter.

elias
fonte
2
Concordo com esta solução, parece muito com o que eu tinha em mente no meu comentário sobre a questão principal. Eu preferiria dividi-lo em variáveis ​​separadas, o que tornaria mais fácil adicionar um ataque do meio, por exemplo, no futuro.
1933 Yoh
2
Eu apenas corrigi alguns bugs no código acima depois de testá-lo, agora ele funciona bem. Indo mais longe no caminho do manipulador de bits, eu também criei uma solução de uma linha, que ainda não é tão mística quanto a máscara de bits em outras respostas, mas ainda é complicada o suficiente para estragar sua mente:return ((one ^ two) & 2) == 0 ? (one & 2) / 2 * 3 : ((one & 2) / 2 ^ ((one ^ two) & 1)) + 1;
elias
1
Esta é a melhor resposta, pois qualquer novo programador que o leia realmente entenderá a mágica que está por trás de todos esses números mágicos.
Bezmax
20

Para ser sincero, todo mundo tem seu próprio estilo de código. Eu não teria pensado que o desempenho seria afetado demais. Se você entende isso melhor do que usar uma versão de gabinete, continue usando isso.

Você pode aninhar os ifs, portanto, potencialmente, haveria um ligeiro aumento de desempenho para as suas últimas verificações se, pois não teriam passado por tantas declarações if. Mas no seu contexto de um curso básico de java, provavelmente não será beneficiado.

else if(one == 3 && two == 3) { result = 3; }

Então, ao invés de ...

if(one == 0 && two == 0) { result = 0; }
else if(one == 0 && two == 1) { result = 0; }
else if(one == 0 && two == 2) { result = 1; }
else if(one == 0 && two == 3) { result = 2; }

Você faria ...

if(one == 0) 
{ 
    if(two == 0) { result = 0; }
    else if(two == 1) { result = 0; }
    else if(two == 2) { result = 1; }
    else if(two == 3) { result = 2; }
}

E apenas reformate-o como preferir.

Isso não faz o código parecer melhor, mas potencialmente o acelera um pouco, acredito.

Joe Harper
fonte
3
Não sei se é realmente uma boa prática, mas, nesse caso, eu provavelmente usaria instruções de switch aninhadas. Seria necessário mais espaço, mas seria muito claro.
dyesdyes
Isso também funcionaria, mas acho que é uma questão de preferência. Na verdade, eu prefiro as instruções if, pois está praticamente dizendo o que o código está fazendo. Não descartando sua opinião, é claro, o que for melhor para você :). Voto a favor para a sugestão alternativa!
Joe Harper
12

Vamos ver o que sabemos

1: suas respostas são simétricas para P1 (jogador um) e P2 (jogador dois). Isso faz sentido para um jogo de luta, mas também é algo que você pode aproveitar para melhorar sua lógica.

2: 3 batidas 0 bate 2 batidas 1 batida 3. Os únicos casos não cobertos por esses casos são combinações de 0 vs 1 e 2 vs 3. Para colocar de outra maneira, a tabela de vitórias única é assim: 0 batida 2, 1 batida 3, 2 tempos 1, 3 tempos 0.

3: Se 0/1 subir um contra o outro, haverá um empate sem sucesso, mas se 2/3 subirem um contra o outro, ambos baterão

Primeiro, vamos criar uma função unidirecional nos dizendo se vencemos:

// returns whether we beat our opponent
public boolean doesBeat(int attacker, int defender) {
  int[] beats = {2, 3, 1, 0};
  return defender == beats[attacker];
}

Podemos então usar esta função para compor o resultado final:

// returns the overall fight result
// bit 0 = one hits
// bit 1 = two hits
public int fightMath(int one, int two)
{
  // Check to see whether either has an outright winning combo
  if (doesBeat(one, two))
    return 1;

  if (doesBeat(two, one))
    return 2;

  // If both have 0/1 then its hitless draw but if both have 2/3 then they both hit.
  // We can check this by seeing whether the second bit is set and we need only check
  // one's value as combinations where they don't both have 0/1 or 2/3 have already
  // been dealt with 
  return (one & 2) ? 3 : 0;
}

Embora isso seja indiscutivelmente mais complexo e provavelmente mais lento do que a pesquisa de tabela oferecida em muitas respostas, acredito que seja um método superior, porque na verdade encapsula a lógica do seu código e a descreve para qualquer pessoa que esteja lendo o código. Eu acho que isso torna uma implementação melhor.

(Já faz um tempo desde que eu fiz algum Java para pedir desculpas se a sintaxe estiver desativada, espero que ainda seja inteligível se eu entendi um pouco errado)

A propósito, 0-3 claramente significa alguma coisa; eles não são valores arbitrários, portanto, seria útil nomeá-los.

Jack Aidley
fonte
11

Espero entender a lógica corretamente. Que tal algo como:

public int fightMath (int one, int two)
{
    int oneHit = ((one == 3 && two != 1) || (one == 2 && two != 0)) ? 1 : 0;
    int twoHit = ((two == 3 && one != 1) || (two == 2 && one != 0)) ? 2 : 0;

    return oneHit+twoHit;
}

Verificar um golpe alto ou um golpe baixo não é bloqueado e o mesmo para o jogador dois.

Edit: Algoritmo não foi totalmente compreendido, "hit" concedido ao bloquear o que eu não percebi (Thx elias):

public int fightMath (int one, int two)
{
    int oneAttack = ((one == 3 && two != 1) || (one == 2 && two != 0)) ? 1 : (one >= 2) ? 2 : 0;
    int twoAttack = ((two == 3 && one != 1) || (two == 2 && one != 0)) ? 2 : (two >= 2) ? 1 : 0;

    return oneAttack | twoAttack;
}
Chris
fonte
Maneira de encontrar o excelente resultado do alinhador longitudinal!
Chad
1
Gosto da abordagem, mas receio que esta solução não tenha a possibilidade de ser acertada ao bloquear um ataque (por exemplo, se um = 0 e dois = 2, retorna 0, mas 1 é esperado de acordo com a especificação). Talvez você possa trabalhar nisso para acertar, mas não tenho certeza de que o código resultante ainda seja tão elegante, pois isso significa que as linhas crescerão um pouco mais.
elias 20/03
Não percebeu que um "hit" foi concedido por bloco. Obrigado por apontar isso. Ajustado com uma correção muito simples.
Chris
10

Como não tenho experiência com Java, pode haver erros de digitação. Por favor, considere o código como pseudo-código.

Eu iria com um simples interruptor. Para isso, você precisaria de uma avaliação de número único. No entanto, para este caso, uma vez que 0 <= one < 4 <= 9e 0 <= two < 4 <= 9, podemos converter ambas as entradas em um int simples, multiplicando onepor 10 e adicionando two. Em seguida, use uma opção no número resultante como este:

public int fightMath(int one, int two) {
    // Convert one and two to a single variable in base 10
    int evaluate = one * 10 + two;

    switch(evaluate) {
        // I'd consider a comment in each line here and in the original code
        // for clarity
        case 0: result = 0; break;
        case 1: result = 0; break;
        case 1: result = 0; break;
        case 2: result = 1; break;
        case 3: result = 2; break;
        case 10: result = 0; break;
        case 11: result = 0; break;
        case 12: result = 2; break;
        case 13: result = 1; break;
        case 20: result = 2; break;
        case 21: result = 1; break;
        case 22: result = 3; break;
        case 23: result = 3; break;
        case 30: result = 1; break;
        case 31: result = 2; break;
        case 32: result = 3; break;
        case 33: result = 3; break;
    }

    return result;
}

Há outro método curto que eu apenas quero apontar como um código teórico. No entanto, eu não o usaria porque tem uma complexidade extra com a qual você normalmente não deseja lidar. A complexidade extra vem da base 4 , porque a contagem é 0, 1, 2, 3, 10, 11, 12, 13, 20, ...

public int fightMath(int one, int two) {
    // Convert one and two to a single variable in base 4
    int evaluate = one * 4 + two;

    allresults = new int[] { 0, 0, 1, 2, 0, 0, 2, 1, 2, 1, 3, 3, 1, 2, 3, 3 };

    return allresults[evaluate];
}

Realmente apenas uma observação adicional, caso eu esteja perdendo algo do Java. No PHP eu faria:

function fightMath($one, $two) {
    // Convert one and two to a single variable in base 4
    $evaluate = $one * 10 + $two;

    $allresults = array(
         0 => 0,  1 => 0,  2 => 1,  3 => 2,
        10 => 0, 11 => 0, 12 => 2, 13 => 1,
        20 => 2, 21 => 1, 22 => 3, 23 => 3,
        30 => 1, 31 => 2, 32 => 3, 33 => 3 );

    return $allresults[$evaluate];
}
Francisco Presencia
fonte
Java não tem lamdas antes versão 8º
Kirill Gamazkov
1
Este. Para um número tão pequeno de entradas, eu usaria um comutador com um valor composto (embora possa ser mais legível com um multiplicador maior que 10, como 100 ou 1000).
Medinoc 24/03
7

Como você prefere ifcondicionais aninhados , aqui está outra maneira.
Observe que ele não usa o resultmembro e não altera nenhum estado.

public int fightMath(int one, int two) {
    if (one == 0) {
      if (two == 0) { return 0; }
      if (two == 1) { return 0; }
      if (two == 2) { return 1; }
      if (two == 3) { return 2; }
    }   
    if (one == 1) {
      if (two == 0) { return 0; }
      if (two == 1) { return 0; }
      if (two == 2) { return 2; }
      if (two == 3) { return 1; }
    }
    if (one == 2) {
      if (two == 0) { return 2; }
      if (two == 1) { return 1; }
      if (two == 2) { return 3; }
      if (two == 3) { return 3; }
    }
    if (one == 3) {
      if (two == 0) { return 1; }
      if (two == 1) { return 2; }
      if (two == 2) { return 3; }
      if (two == 3) { return 3; }
    }
    return DEFAULT_RESULT;
}
Nick Dandoulakis
fonte
por que você não tem mais nada?
FDinoff
3
@FDinoff Eu poderia ter usado elsecorrentes, mas não fez diferença.
Nick Dandoulakis 19/03/2014
1
Eu sei que é trivial, mas a adição de outras cadeias não seria mais rápida? em 3 de 4 casos? Eu sempre tenho o hábito de escrever código para executar o mais rápido possível, mesmo que sejam apenas alguns ciclos.
precisa
2
@BrandonBearden aqui eles não farão diferença (assumindo que a entrada esteja sempre no intervalo 0..3). O compilador provavelmente otimizará o código de qualquer maneira. Se tivermos longas séries de else ifinstruções, podemos acelerar o código switchou consultar tabelas.
Nick Dandoulakis 20/03/2014
Como é isso? Se one==0ele executar o código, será necessário verificar one==1seone==2 e finalmente, se one==3- Se houvesse outro if, não faria as últimas três verificações, porque sairia da instrução após a primeira partida. E sim, você poderia otimizar ainda mais usando uma instrução switch no lugar das if (one...instruções e, em seguida, usando outra opção dentro do caso da one's. No entanto, essa não é minha pergunta.
precisa
6

Experimente com a caixa do interruptor...

Dê uma olhada aqui ou aqui para obter mais informações sobre ele

switch (expression)
{ 
  case constant:
        statements;
        break;
  [ case constant-2:
        statements;
        break;  ] ...
  [ default:
        statements;
        break;  ] ...
}

Você pode adicionar várias condições (não simultaneamente) a ele e até ter uma opção padrão onde nenhum outro caso foi satisfeito.

PS: Somente se uma condição for atendida.

Se duas condições surgirem simultaneamente ... não acho que o switch possa ser usado. Mas você pode reduzir seu código aqui.

Instrução do switch Java vários casos

Nevin Madhukar K
fonte
6

A primeira coisa que me ocorreu foi essencialmente a mesma resposta dada por Francisco Presencia, mas um pouco otimizada:

public int fightMath(int one, int two)
{
    switch (one*10 + two)
    {
    case  0:
    case  1:
    case 10:
    case 11:
        return 0;
    case  2:
    case 13:
    case 21:
    case 30:
        return 1;
    case  3:
    case 12:
    case 20:
    case 31:
        return 2;
    case 22:
    case 23:
    case 32:
    case 33:
        return 3;
    }
}

Você pode otimizá-lo ainda mais, tornando o último caso (para 3) o caso padrão:

    //case 22:
    //case 23:
    //case 32:
    //case 33:
    default:
        return 3;

A vantagem desse método é que é mais fácil ver quais valores onee twocorrespondem a quais valores retornados do que alguns dos outros métodos sugeridos.

David R Tribble
fonte
Esta é uma variação de outra resposta minha aqui .
precisa
6
((two&2)*(1+((one^two)&1))+(one&2)*(2-((one^two)&1)))/2
Dawood ibn Kareem
fonte
4
Quanto tempo você levou para chegar a isso?
mbatchkarov
2
@mbatchkarov Aproximadamente 10 minutos lendo as outras respostas, depois 10 minutos rabiscando com lápis e papel.
Dawood ibn Kareem
7
Eu ficaria muito triste se tivesse que manter isso.
Meryovi
uhmmm ... Há um erro: você está faltando; --unicorns
Alberto
Concordo com @Meryovi, adereços para ser conciso, mas terrível como código APL
Ed Griebel
3

Ao desenhar uma tabela entre um / dois e o resultado, vejo um padrão,

if(one<2 && two <2) result=0; return;

O exposto acima reduziria pelo menos 3 declarações se. Não vejo um padrão definido nem sou capaz de captar muito do código fornecido - mas se essa lógica puder ser derivada, isso reduziria várias instruções if.

Espero que isto ajude.

AnonNihcas
fonte
3

Um bom ponto seria definir as regras como texto, então você pode derivar mais facilmente a fórmula correta. Isso é extraído da bela representação de matriz do laalto:

{ 0, 0, 1, 2 },
{ 0, 0, 2, 1 },
{ 2, 1, 3, 3 },
{ 1, 2, 3, 3 }

E aqui vamos nós com alguns comentários gerais, mas você deve descrevê-los em termos de regra:

if(one<2) // left half
{
    if(two<2) // upper left half
    {
        result = 0; //neither hits
    }
    else // lower left half
    {
        result = 1+(one+two)%2; //p2 hits if sum is even
    }
}
else // right half
{
    if(two<2) // upper right half
    {
        result = 1+(one+two+1)%2; //p1 hits if sum is even
    }
    else // lower right half
    {
        return 3; //both hit
    }
}

É claro que você pode reduzir isso para menos código, mas geralmente é uma boa idéia entender o que você codifica, em vez de encontrar uma solução compacta.

if((one<2)&&(two<2)) result = 0; //top left
else if((one>1)&&(two>1)) result = 3; //bottom right
else result = 1+(one+two+((one>1)?1:0))%2; //no idea what that means

Alguma explicação sobre os complicados acessos p1 / p2 seria ótima, parece interessante!

Marcellus
fonte
3

A solução mais curta e ainda legível:

static public int fightMath(int one, int two)
{
    if (one < 2 && two < 2) return 0;
    if (one > 1 && two > 1) return 3;
    int n = (one + two) % 2;
    return one < two ? 1 + n : 2 - n;
}

ou ainda mais curto:

static public int fightMath(int one, int two)
{
    if (one / 2 == two / 2) return (one / 2) * 3;
    return 1 + (one + two + one / 2) % 2;
}

Não contém números "mágicos";) Espero que ajude.

PW
fonte
2
Fórmulas como essas tornarão impossível alterar (atualizar) o resultado de uma combinação posteriormente. A única maneira seria refazer a fórmula inteira.
Senão
2
@ SNag: Eu concordo com isso. A solução mais flexível é usar a matriz 2D. Mas o autor deste post queria uma fórmula e essa é a melhor que você pode obter apenas com ifs e matemática simples.
PW
2

static int val(int i, int u){ int q = (i & 1) ^ (u & 1); return ((i >> 1) << (1 ^ q))|((u >> 1) << q); }

user1837841
fonte
1

Pessoalmente, gosto de colocar em cascata operadores ternários:

int result = condition1
    ? result1
    : condition2
    ? result2
    : condition3
    ? result3
    : resultElse;

Mas no seu caso, você pode usar:

final int[] result = new int[/*16*/] {
    0, 0, 1, 2,
    0, 0, 2, 1,
    2, 1, 3, 3,
    1, 2, 3, 3
};

public int fightMath(int one, int two) {
    return result[one*4 + two];
}

Ou você pode notar um padrão em bits:

one   two   result

section 1: higher bits are equals =>
both result bits are equals to that higher bits

00    00    00
00    01    00
01    00    00
01    01    00
10    10    11
10    11    11
11    10    11
11    11    11

section 2: higher bits are different =>
lower result bit is inverse of lower bit of 'two'
higher result bit is lower bit of 'two'

00    10    01
00    11    10
01    10    10
01    11    01
10    00    10
10    01    01
11    00    01
11    01    10

Então você pode usar magia:

int fightMath(int one, int two) {
    int b1 = one & 2, b2 = two & 2;
    if (b1 == b2)
        return b1 | (b1 >> 1);

    b1 = two & 1;

    return (b1 << 1) | (~b1);
}
Kirill Gamazkov
fonte
1

Aqui está uma versão bastante concisa, semelhante à resposta do JAB . Isso utiliza um mapa para armazenar que move triunfo sobre os outros.

public enum Result {
  P1Win, P2Win, BothWin, NeitherWin;
}

public enum Move {
  BLOCK_HIGH, BLOCK_LOW, ATTACK_HIGH, ATTACK_LOW;

  static final Map<Move, List<Move>> beats = new EnumMap<Move, List<Move>>(
      Move.class);

  static {
    beats.put(BLOCK_HIGH, new ArrayList<Move>());
    beats.put(BLOCK_LOW, new ArrayList<Move>());
    beats.put(ATTACK_HIGH, Arrays.asList(ATTACK_LOW, BLOCK_LOW));
    beats.put(ATTACK_LOW, Arrays.asList(ATTACK_HIGH, BLOCK_HIGH));
  }

  public static Result compare(Move p1Move, Move p2Move) {
    boolean p1Wins = beats.get(p1Move).contains(p2Move);
    boolean p2Wins = beats.get(p2Move).contains(p1Move);

    if (p1Wins) {
      return (p2Wins) ? Result.BothWin : Result.P1Win;
    }
    if (p2Wins) {
      return (p1Wins) ? Result.BothWin : Result.P2Win;
    }

    return Result.NeitherWin;
  }
} 

Exemplo:

System.out.println(Move.compare(Move.ATTACK_HIGH, Move.BLOCK_LOW));

Impressões:

P1Win
Duncan Jones
fonte
Eu recomendaria static final Map<Move, List<Move>> beats = new java.util.EnumMap<>();, em vez disso, deve ser um pouco mais eficiente.
JAB
@ JAB Sim, boa ideia. Eu sempre esqueço que esse tipo existe. E ... quão constrangedor é construir um!
Duncan Jones
1

Eu usaria um mapa, um HashMap ou um TreeMap

Especialmente se os parâmetros não estiverem no formulário 0 <= X < N

Como um conjunto de números inteiros positivos aleatórios ..

Código

public class MyMap
{
    private TreeMap<String,Integer> map;

    public MyMap ()
    {
        map = new TreeMap<String,Integer> ();
    }

    public void put (int key1, int key2, Integer value)
    {
        String key = (key1+":"+key2);

        map.put(key, new Integer(value));
    }

    public Integer get (int key1, int key2)
    {
        String key = (key1+":"+key2);

        return map.get(key);
    }
}
Khaled.K
fonte
1

Graças a @Joe Harper, como acabei usando uma variação de sua resposta. Para reduzi-lo ainda mais, pois 2 resultados por 4 eram os mesmos, diminuí-lo ainda mais.

Eu posso voltar a isso em algum momento, mas se não houver grande resistência causada por várias ifdeclarações, vou manter isso por enquanto. Examinarei a matriz da tabela e alternarei ainda mais as soluções de instrução.

public int fightMath(int one, int two) {
  if (one === 0) {
    if (two === 2) { return 1; }
    else if(two === 3) { return 2; }
    else { return 0; }
  } else if (one === 1) {
    if (two === 2) { return 2; }
    else if (two === 3) { return 1; }
    else { return 0; }
  } else if (one === 2) {
    if (two === 0) { return 2; }
    else if (two === 1) { return 1; }
    else { return 3; }
  } else if (one === 3) {
    if (two === 0) { return 1; }
    else if (two === 1) { return 2; }
    else { return 3; }
  }
}
TomFirth
fonte
13
Esta é realmente menos legível do que o original e não reduz o número de if ...
Chad
@Chad A idéia disso era aumentar a velocidade do processo e, embora pareça horrível, é facilmente atualizado, devo adicionar mais ações no futuro. Dizendo isso, agora estou usando uma resposta anterior que não entendi completamente antes.
TomFirth #
3
@ TomFirth84 Existe um motivo para você não estar seguindo as convenções de codificação apropriadas para suas declarações if?
21714
@ylun: eu reduzi as linhas antes de colar no SO, não por legibilidade, mas por puro espaço de spam. Existem variações de prática nesta página e, infelizmente, é do jeito que aprendi e me sinto à vontade.
21414 TomFirth
2
@ TomFirth84 Eu não acho que isso seja facilmente atualizado, o número de linhas cresce algo como o produto do número de valores permitidos.
Andrew Lazarus
0
  1. Use constantes ou enumerações para tornar o código mais legível
  2. Tente dividir o código em mais funções
  3. Tente usar a simetria do problema

Aqui está uma sugestão de como isso pode parecer, mas o uso de um ints aqui ainda é meio feio:

static final int BLOCK_HIGH = 0;
static final int BLOCK_LOW = 1;
static final int ATTACK_HIGH = 2;
static final int ATTACK_LOW = 3;

public static int fightMath(int one, int two) {
    boolean player1Wins = handleAttack(one, two);
    boolean player2Wins = handleAttack(two, one);
    return encodeResult(player1Wins, player2Wins); 
}



private static boolean handleAttack(int one, int two) {
     return one == ATTACK_HIGH && two != BLOCK_HIGH
        || one == ATTACK_LOW && two != BLOCK_LOW
        || one == BLOCK_HIGH && two == ATTACK_HIGH
        || one == BLOCK_LOW && two == ATTACK_LOW;

}

private static int encodeResult(boolean player1Wins, boolean player2Wins) {
    return (player1Wins ? 1 : 0) + (player2Wins ? 2 : 0);
}

Seria melhor usar um tipo estruturado para a entrada e a saída. A entrada realmente tem dois campos: a posição e o tipo (bloqueio ou ataque). A saída também possui dois campos: player1Wins e player2Wins. Codificar isso em um único inteiro torna mais difícil a leitura do código.

class PlayerMove {
    PlayerMovePosition pos;
    PlayerMoveType type;
}

enum PlayerMovePosition {
    HIGH,LOW
}

enum PlayerMoveType {
    BLOCK,ATTACK
}

class AttackResult {
    boolean player1Wins;
    boolean player2Wins;

    public AttackResult(boolean player1Wins, boolean player2Wins) {
        this.player1Wins = player1Wins;
        this.player2Wins = player2Wins;
    }
}

AttackResult fightMath(PlayerMove a, PlayerMove b) {
    return new AttackResult(isWinningMove(a, b), isWinningMove(b, a));
}

boolean isWinningMove(PlayerMove a, PlayerMove b) {
    return a.type == PlayerMoveType.ATTACK && !successfulBlock(b, a)
            || successfulBlock(a, b);
}

boolean successfulBlock(PlayerMove a, PlayerMove b) {
    return a.type == PlayerMoveType.BLOCK 
            && b.type == PlayerMoveType.ATTACK 
            && a.pos == b.pos;
}

Infelizmente, Java não é muito bom para expressar esses tipos de tipos de dados.

peq
fonte
-2

Em vez disso, faça algo parecido com isto

   public int fightMath(int one, int two) {
    return Calculate(one,two)

    }


    private int Calculate(int one,int two){

    if (one==0){
        if(two==0){
     //return value}
    }else if (one==1){
   // return value as per condtiion
    }

    }
onkar
fonte
4
Você acabou de criar uma função privada envolvida pela pública. Por que não apenas implementar isso na função pública?
Martin Ueding 19/03/14
5
E você não reduziu o número de instruções if.
Chad